diff --git a/modules/node/src/appInstance/appInstance.repository.ts b/modules/node/src/appInstance/appInstance.repository.ts index 50ef581a53..a668ccd27a 100644 --- a/modules/node/src/appInstance/appInstance.repository.ts +++ b/modules/node/src/appInstance/appInstance.repository.ts @@ -107,6 +107,16 @@ export class AppInstanceRepository extends Repository { .getMany(); } + async findInstalledAppsAcrossChannelsByAppDefinition( + appDefinition: string, + ): Promise { + return this.createQueryBuilder("app_instances") + .leftJoinAndSelect("app_instances.channel", "channel") + .where("app_instances.type = :type", { type: AppType.INSTANCE }) + .andWhere("app_instances.appDefinition = :appDefinition", { appDefinition }) + .getMany(); + } + async findTransferAppsByAppDefinitionPaymentIdAndType( paymentId: string, appDefinition: string, diff --git a/modules/node/src/test/e2e/hashlockTransfer.spec.ts b/modules/node/src/test/e2e/hashlockTransfer.spec.ts new file mode 100644 index 0000000000..1c7f4c392c --- /dev/null +++ b/modules/node/src/test/e2e/hashlockTransfer.spec.ts @@ -0,0 +1,155 @@ +import { ColorfulLogger, logTime, getRandomBytes32 } from "@connext/utils"; +import { INestApplication } from "@nestjs/common"; +import { Test, TestingModule } from "@nestjs/testing"; +import { + IConnextClient, + ConditionalTransferCreatedEventData, + ConditionalTransferTypes, + EventNames, + CONVENTION_FOR_ETH_ASSET_ID, +} from "@connext/types"; +import { utils, BigNumber } from "ethers"; + +import { AppModule } from "../../app.module"; +import { ConfigService } from "../../config/config.service"; +import { + env, + ethProviderUrl, + expect, + MockConfigService, + getClient, + AssetOptions, + fundChannel, + ethProvider, + ETH_AMOUNT_SM, +} from "../utils"; +import { TIMEOUT_BUFFER } from "../../constants"; +import { TransferService } from "../../transfer/transfer.service"; + +const { soliditySha256 } = utils; + +// Define helper functions +const sendHashlockTransfer = async ( + sender: IConnextClient, + receiver: IConnextClient, + transfer: AssetOptions & { preImage: string; timelock: string }, +): Promise> => { + // Fund sender channel + await fundChannel(sender, transfer.amount, transfer.assetId); + + // Create transfer parameters + const expiry = BigNumber.from(transfer.timelock).add(await ethProvider.getBlockNumber()); + const lockHash = soliditySha256(["bytes32"], [transfer.preImage]); + + const receiverPromise = receiver.waitFor(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, 10_000); + // sender result + const senderResult = await sender.conditionalTransfer({ + amount: transfer.amount.toString(), + conditionType: ConditionalTransferTypes.HashLockTransfer, + lockHash, + timelock: transfer.timelock, + assetId: transfer.assetId, + meta: { foo: "bar" }, + recipient: receiver.publicIdentifier, + }); + const receiverEvent = await receiverPromise; + const paymentId = soliditySha256(["address", "bytes32"], [transfer.assetId, lockHash]); + const expectedVals = { + amount: transfer.amount, + assetId: transfer.assetId, + paymentId, + recipient: receiver.publicIdentifier, + sender: sender.publicIdentifier, + transferMeta: { + timelock: transfer.timelock, + lockHash, + expiry: expiry.sub(TIMEOUT_BUFFER), + }, + }; + // verify the receiver event + expect(receiverEvent).to.containSubset({ + ...expectedVals, + type: ConditionalTransferTypes.HashLockTransfer, + }); + + // verify sender return value + expect(senderResult).to.containSubset({ + ...expectedVals, + transferMeta: { + ...expectedVals.transferMeta, + expiry, + }, + }); + return receiverEvent as ConditionalTransferCreatedEventData<"HashLockTransferApp">; +}; + +describe.only("Hashlock Transfer", () => { + const log = new ColorfulLogger("TestStartup", env.logLevel, true, "T"); + + let app: INestApplication; + let configService: ConfigService; + let transferService: TransferService; + let senderClient: IConnextClient; + let receiverClient: IConnextClient; + + before(async () => { + const start = Date.now(); + const currBlock = await ethProvider.getBlockNumber(); + if (currBlock <= TIMEOUT_BUFFER) { + log.warn(`Mining until there are at least ${TIMEOUT_BUFFER} blocks`); + for (let index = currBlock; index <= TIMEOUT_BUFFER + 1; index++) { + await ethProvider.send("evm_mine", []); + } + } + + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(ConfigService) + .useClass(MockConfigService) + .compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + configService = moduleFixture.get(ConfigService); + await app.listen(configService.getPort()); + + log.info(`node: ${await configService.getSignerAddress()}`); + log.info(`ethProviderUrl: ${ethProviderUrl}`); + + senderClient = await getClient("A"); + + receiverClient = await getClient("B"); + + logTime(log, start, "Done setting up test env"); + transferService = moduleFixture.get(TransferService); + log.warn(`Finished before() in ${Date.now() - start}ms`); + }); + + after(async () => { + try { + await app.close(); + log.info(`Application was shutdown successfully`); + } catch (e) { + log.warn(`Application was shutdown unsuccessfully: ${e.message}`); + } + }); + + it("cleans up expired hashlock transfers ", async () => { + const transfer: AssetOptions = { amount: ETH_AMOUNT_SM, assetId: CONVENTION_FOR_ETH_ASSET_ID }; + const preImage = getRandomBytes32(); + const timelock = (101).toString(); + const opts = { ...transfer, preImage, timelock }; + + const { paymentId } = await sendHashlockTransfer(senderClient, receiverClient, opts); + + expect(paymentId).to.be.ok; + + const appsBeforePrune = await receiverClient.getAppInstances(); + expect(appsBeforePrune.length).to.eq(1); + await transferService.pruneChannels(); + const appsAfterPrune = await receiverClient.getAppInstances(); + console.log("receiverClientappsAfterPrune: ", appsAfterPrune[0]); + expect(appsAfterPrune.length).to.eq(0); + }); +}); diff --git a/modules/node/src/test/utils/client.ts b/modules/node/src/test/utils/client.ts new file mode 100644 index 0000000000..05c095427e --- /dev/null +++ b/modules/node/src/test/utils/client.ts @@ -0,0 +1,82 @@ +import { connect } from "@connext/client"; +import { ColorfulLogger, getRandomChannelSigner } from "@connext/utils"; +import { BigNumber } from "ethers"; +import { getMemoryStore } from "@connext/store"; +import { + ClientOptions, + IConnextClient, + AssetId, + CONVENTION_FOR_ETH_ASSET_ID, +} from "@connext/types"; + +import { env, ethProviderUrl, expect, ethProvider, sugarDaddy } from "."; +import { parseEther } from "ethers/lib/utils"; + +export const TEN = "10"; +export const TWO = "2"; +export const ONE = "1"; +export const ZERO_ONE = "0.1"; +export const ZERO_ZERO_TWO = "0.02"; +export const ZERO_ZERO_ONE = "0.01"; +export const ZERO_ZERO_ZERO_FIVE = "0.005"; +export const ZERO_ZERO_ZERO_ONE = "0.001"; + +export const TEN_ETH = parseEther(TEN); +export const TWO_ETH = parseEther(TWO); +export const ONE_ETH = parseEther(ONE); +export const ZERO_ONE_ETH = parseEther(ZERO_ONE); +export const ZERO_ZERO_TWO_ETH = parseEther(ZERO_ZERO_TWO); +export const ZERO_ZERO_ONE_ETH = parseEther(ZERO_ZERO_ONE); +export const ZERO_ZERO_ZERO_FIVE_ETH = parseEther(ZERO_ZERO_ZERO_FIVE); +export const ZERO_ZERO_ZERO_ONE_ETH = parseEther(ZERO_ZERO_ZERO_ONE); + +export const ETH_AMOUNT_SM = ZERO_ZERO_ONE_ETH; +export const ETH_AMOUNT_MD = ZERO_ONE_ETH; +export const ETH_AMOUNT_LG = ONE_ETH; +export const TOKEN_AMOUNT = TEN_ETH; +export const TOKEN_AMOUNT_SM = ONE_ETH; + +export const getClient = async ( + id: string = "", + overrides: Partial = {}, + fundAmount: BigNumber = ETH_AMOUNT_MD, +): Promise => { + const log = new ColorfulLogger("getClient", env.logLevel, true, "T"); + const client = await connect({ + store: getMemoryStore(), + signer: getRandomChannelSigner(ethProvider), + ethProviderUrl, + messagingUrl: env.messagingUrl, + nodeUrl: env.nodeUrl, + loggerService: new ColorfulLogger("", env.logLevel, true, id), + ...overrides, + }); + + if (fundAmount.gt(0)) { + const tx = await sugarDaddy.sendTransaction({ + to: client.signerAddress, + value: fundAmount, + }); + await ethProvider.waitForTransaction(tx.hash); + + log.info(`Created client: ${client.publicIdentifier}`); + expect(client.signerAddress).to.be.a("string"); + } + + return client; +}; + +export const fundChannel = async ( + client: IConnextClient, + amount: BigNumber = ETH_AMOUNT_SM, + assetId: AssetId = CONVENTION_FOR_ETH_ASSET_ID, +) => { + const { [client.signerAddress]: preBalance } = await client.getFreeBalance(assetId); + const depositRes = await client.deposit({ + assetId, + amount, + }); + expect(depositRes.transaction).to.be.ok; + const postBalance = await depositRes.completed(); + expect(preBalance.add(amount)).to.eq(postBalance.freeBalance[client.signerAddress]); +}; diff --git a/modules/node/src/test/utils/config.ts b/modules/node/src/test/utils/config.ts index aa70fc9bcd..0077f771b8 100644 --- a/modules/node/src/test/utils/config.ts +++ b/modules/node/src/test/utils/config.ts @@ -25,6 +25,9 @@ export const env = { export const ethProviderUrl = env.chainProviders[env.defaultChain]; +export const ethProvider = new providers.JsonRpcProvider(ethProviderUrl); +export const sugarDaddy = Wallet.fromMnemonic(env.mnemonic!).connect(ethProvider); + export const defaultSigner = new ChannelSigner( Wallet.fromMnemonic(env.mnemonic!).privateKey, ethProviderUrl, diff --git a/modules/node/src/test/utils/index.ts b/modules/node/src/test/utils/index.ts index 141f69a812..8dd5204725 100644 --- a/modules/node/src/test/utils/index.ts +++ b/modules/node/src/test/utils/index.ts @@ -1,4 +1,6 @@ export * from "./cfCore"; +export * from "./client"; export * from "./config"; export * from "./eth"; export * from "./expect"; +export * from "./transfer"; diff --git a/modules/node/src/test/utils/transfer.ts b/modules/node/src/test/utils/transfer.ts new file mode 100644 index 0000000000..0b74e20e0a --- /dev/null +++ b/modules/node/src/test/utils/transfer.ts @@ -0,0 +1,6 @@ +import { BigNumber } from "ethers"; + +export interface AssetOptions { + amount: BigNumber; + assetId: string; +} diff --git a/modules/node/src/transfer/transfer.service.ts b/modules/node/src/transfer/transfer.service.ts index 5bbddab703..81432166c2 100644 --- a/modules/node/src/transfer/transfer.service.ts +++ b/modules/node/src/transfer/transfer.service.ts @@ -122,6 +122,10 @@ export class TransferService { @Interval(3600_000) async pruneChannels() { const channels = await this.channelRepository.findAll(); + const addresses = this.configService.getAddressBook(); + Object.entries(addresses).map((addrs) => addrs); + // const hashLockApps = await this.appInstanceRepository.findInstalledAppsByAppDefinition(); + console.log("channels: ", channels); for (const channel of channels) { await this.pruneExpiredApps(channel); }