diff --git a/packages/validator/hardhat.config.ts b/packages/validator/hardhat.config.ts index 858c0ff..e8ec028 100644 --- a/packages/validator/hardhat.config.ts +++ b/packages/validator/hardhat.config.ts @@ -1,9 +1,9 @@ import "@nomiclabs/hardhat-ethers"; import "@nomiclabs/hardhat-waffle"; import "@nomiclabs/hardhat-web3"; -import "hardhat-change-network"; import "@openzeppelin/hardhat-upgrades"; import "@typechain/hardhat"; +import "hardhat-change-network"; import "hardhat-gas-reporter"; import "solidity-coverage"; import "solidity-docgen"; @@ -35,39 +35,6 @@ function getAccounts() { accounts.push(process.env.FEE); } - if ( - process.env.LINK_VALIDATOR1 !== undefined && - process.env.LINK_VALIDATOR1.trim() !== "" && - reg_bytes64.test(process.env.LINK_VALIDATOR1) - ) { - accounts.push(process.env.LINK_VALIDATOR1); - } else { - process.env.LINK_VALIDATOR1 = Wallet.createRandom().privateKey; - accounts.push(process.env.LINK_VALIDATOR1); - } - - if ( - process.env.LINK_VALIDATOR2 !== undefined && - process.env.LINK_VALIDATOR2.trim() !== "" && - reg_bytes64.test(process.env.LINK_VALIDATOR2) - ) { - accounts.push(process.env.LINK_VALIDATOR2); - } else { - process.env.LINK_VALIDATOR2 = Wallet.createRandom().privateKey; - accounts.push(process.env.LINK_VALIDATOR2); - } - - if ( - process.env.LINK_VALIDATOR3 !== undefined && - process.env.LINK_VALIDATOR3.trim() !== "" && - reg_bytes64.test(process.env.LINK_VALIDATOR3) - ) { - accounts.push(process.env.LINK_VALIDATOR3); - } else { - process.env.LINK_VALIDATOR3 = Wallet.createRandom().privateKey; - accounts.push(process.env.LINK_VALIDATOR3); - } - if ( process.env.BRIDGE_VALIDATOR1 !== undefined && process.env.BRIDGE_VALIDATOR1.trim() !== "" && diff --git a/packages/validator/package.json b/packages/validator/package.json index 12344a7..7cc14fe 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -18,7 +18,8 @@ "start": "TESTING=false NODE_ENV=production hardhat run src/main.ts --network production_net", "formatting:check": "prettier '**/*.{json,sol,ts,js,yaml}' -c", "formatting:write": "prettier '**/*.{json,sol,ts,js,yaml}' --write", - "test:01-Collector": "TESTING=true hardhat test test/01-Collector.test.ts" + "test:01-Collector": "TESTING=true hardhat test test/01-Collector.test.ts", + "test:02-Bridge": "TESTING=true hardhat test test/02-Bridge.test.ts" }, "repository": { "type": "git", diff --git a/packages/validator/src/DefaultServer.ts b/packages/validator/src/DefaultServer.ts index c2ea5ea..04b797b 100644 --- a/packages/validator/src/DefaultServer.ts +++ b/packages/validator/src/DefaultServer.ts @@ -8,6 +8,7 @@ import { WebService } from "./service/WebService"; import { ValidatorStorage } from "./storage/ValidatorStorage"; import { register } from "prom-client"; +import { Validator } from "./scheduler/Validator"; export class DefaultServer extends WebService { /** @@ -20,6 +21,7 @@ export class DefaultServer extends WebService { public readonly defaultRouter: DefaultRouter; public readonly storage: ValidatorStorage; + public readonly validators: Validator[]; /** * Constructor @@ -36,6 +38,7 @@ export class DefaultServer extends WebService { this.config = config; this.storage = storage; this.defaultRouter = new DefaultRouter(this, this.metrics); + this.validators = this.config.bridge.validators.map((m) => new Validator(this.config, this.storage, m)); if (!schedules) schedules = []; schedules.forEach((m) => this.schedules.push(m)); @@ -43,6 +46,7 @@ export class DefaultServer extends WebService { m.setOption({ config: this.config, storage: this.storage, + validators: this.validators, }) ); } diff --git a/packages/validator/src/routers/DefaultRouter.ts b/packages/validator/src/routers/DefaultRouter.ts index 5ca8c34..331a75c 100644 --- a/packages/validator/src/routers/DefaultRouter.ts +++ b/packages/validator/src/routers/DefaultRouter.ts @@ -4,18 +4,9 @@ import express from "express"; import { Metrics } from "../metrics/Metrics"; export class DefaultRouter { - /** - * - * @private - */ private _web_service: WebService; private readonly _metrics: Metrics; - /** - * - * @param service WebService - * @param metrics Metrics - */ constructor(service: WebService, metrics: Metrics) { this._web_service = service; this._metrics = metrics; @@ -34,6 +25,7 @@ export class DefaultRouter { private async getHealthStatus(req: express.Request, res: express.Response) { return res.status(200).json("OK"); } + private makeResponseData(code: number, data: any, error?: any): any { return { code, @@ -42,10 +34,6 @@ export class DefaultRouter { }; } - /** - * GET /metrics - * @private - */ private async getMetrics(req: express.Request, res: express.Response) { res.set("Content-Type", this._metrics.contentType()); this._metrics.add("status", 1); diff --git a/packages/validator/src/scheduler/BridgeScheduler.ts b/packages/validator/src/scheduler/BridgeScheduler.ts index 30085dc..b43f818 100644 --- a/packages/validator/src/scheduler/BridgeScheduler.ts +++ b/packages/validator/src/scheduler/BridgeScheduler.ts @@ -5,15 +5,13 @@ import { ValidatorStorage } from "../storage/ValidatorStorage"; import { Scheduler } from "./Scheduler"; // @ts-ignore -import { BlockNumber } from "web3-core"; import { EventCollector } from "./EventCollector"; +import { Validator } from "./Validator"; export class BridgeScheduler extends Scheduler { private _config: Config | undefined; private _storage: ValidatorStorage | undefined; - - private _collectorA: EventCollector | undefined; - private _collectorB: EventCollector | undefined; + private _validators: Validator[] | undefined; constructor(expression: string) { super(expression); @@ -35,51 +33,27 @@ export class BridgeScheduler extends Scheduler { } } - public setOption(options: any) { - if (options) { - if (options.config && options.config instanceof Config) this._config = options.config; - if (options.storage && options.storage instanceof ValidatorStorage) this._storage = options.storage; - } - - if (this._config !== undefined && this._storage !== undefined) { - this._collectorA = new EventCollector( - this._config, - this._storage, - this._config.bridge.networkAName, - this._config.bridge.networkAContractAddress, - 1n - ); - - this._collectorB = new EventCollector( - this._config, - this._storage, - this._config.bridge.networkBName, - this._config.bridge.networkBContractAddress, - 1n - ); - } - } - - public get collectorA() { - if (this._collectorA !== undefined) return this._collectorA; + private get validators(): Validator[] { + if (this._validators !== undefined) return this._validators; else { - logger.error("collectorA is not ready yet."); + logger.error("Validators is not ready yet."); process.exit(1); } } - public get collectorB() { - if (this._collectorB !== undefined) return this._collectorB; - else { - logger.error("_collectorB is not ready yet."); - process.exit(1); + public setOption(options: any) { + if (options) { + if (options.config && options.config instanceof Config) this._config = options.config; + if (options.storage && options.storage instanceof ValidatorStorage) this._storage = options.storage; + if (options.validators) this._validators = options.validators; } } protected async work() { try { - await this.collectorA.work(); - await this.collectorB.work(); + for (const validator of this.validators) { + await validator.work(); + } } catch (error) { logger.error(`Failed to execute the BridgeScheduler: ${error}`); } diff --git a/packages/validator/src/scheduler/EventCollector.ts b/packages/validator/src/scheduler/EventCollector.ts index 51a7f34..7e58bb2 100644 --- a/packages/validator/src/scheduler/EventCollector.ts +++ b/packages/validator/src/scheduler/EventCollector.ts @@ -1,56 +1,59 @@ import { IBridge__factory } from "../../typechain-types"; -import { Config } from "../common/Config"; -import { logger } from "../common/Logger"; import { ValidatorStorage } from "../storage/ValidatorStorage"; -import { BigNumber } from "ethers"; +import { BigNumber, Wallet } from "ethers"; import * as hre from "hardhat"; +import { logger } from "../common/Logger"; +import { ValidatorType } from "../types"; export class EventCollector { + private wallet: Wallet; + private readonly type: ValidatorType; private readonly network: string; private readonly contractAddress: string; private readonly startNumber: bigint; private contract: any; - private config: Config | undefined; private storage: ValidatorStorage; constructor( - config: Config, storage: ValidatorStorage, + type: ValidatorType, network: string, contractAddress: string, - startBlockNumber: bigint + startBlockNumber: bigint, + wallet: Wallet ) { - this.config = config; this.storage = storage; + this.type = type; this.network = network; this.contractAddress = contractAddress; this.startNumber = startBlockNumber; + this.wallet = wallet; } - private async getLastBlockNumber(): Promise { + private async getLatestBlockNumber(): Promise { const block = await hre.ethers.provider.getBlock("latest"); - return BigNumber.from(block.number); + return BigInt(block.number); } public async work() { hre.changeNetwork(this.network); - let latestCollectedNumber = await this.storage.getLatestNumber(this.network); + const latestBlockNumber = await this.getLatestBlockNumber(); + + let from: BigInt; + const latestCollectedNumber = await this.storage.getLatestNumber(this.wallet.address, this.type, this.network); if (latestCollectedNumber === undefined) { - latestCollectedNumber = this.startNumber - 1n; + from = this.startNumber; + } else { + from = latestCollectedNumber + 1n; + if (from > latestBlockNumber) from = this.startNumber; } - const block = await hre.ethers.provider.getBlock("latest"); - const lastBlockNumber = BigInt(block.number); - - let from = latestCollectedNumber + 1n; - if (from > lastBlockNumber) from = this.startNumber; - this.contract = new hre.web3.eth.Contract(IBridge__factory.abi as any, this.contractAddress); const events = await this.contract.getPastEvents("BridgeDeposited", { fromBlock: Number(from), - toBlock: Number(lastBlockNumber), + toBlock: Number(latestBlockNumber), }); const depositEvents = events.map((m: any) => { @@ -65,8 +68,8 @@ export class EventCollector { }; }); - if (depositEvents.length > 0) await this.storage.postEvents(depositEvents); + if (depositEvents.length > 0) await this.storage.postEvents(depositEvents, this.wallet.address, this.type); - await this.storage.setLatestNumber(this.network, lastBlockNumber); + await this.storage.setLatestNumber(this.wallet.address, this.type, this.network, latestBlockNumber); } } diff --git a/packages/validator/src/scheduler/Executor.ts b/packages/validator/src/scheduler/Executor.ts new file mode 100644 index 0000000..46ae37c --- /dev/null +++ b/packages/validator/src/scheduler/Executor.ts @@ -0,0 +1,114 @@ +import { IBridge, IBridge__factory } from "../../typechain-types"; +import { logger } from "../common/Logger"; +import { GasPriceManager } from "../contract/GasPriceManager"; +import { ValidatorStorage } from "../storage/ValidatorStorage"; +import { ValidatorType, WithdrawStatus } from "../types"; +import { ResponseMessage } from "../utils/Errors"; + +import { Wallet } from "ethers"; +import * as hre from "hardhat"; + +import { NonceManager } from "@ethersproject/experimental"; +import { ContractUtils } from "../utils/ContractUtils"; + +export class Executor { + private storage: ValidatorStorage; + private wallet: Wallet; + private readonly sourceType: ValidatorType; + private readonly targetType: ValidatorType; + private readonly sourceNetwork: string; + private readonly targetNetwork: string; + private readonly targetContractAddress: string; + + constructor( + storage: ValidatorStorage, + sourceType: ValidatorType, + sourceNetwork: string, + targetType: ValidatorType, + targetNetwork: string, + targetContractAddress: string, + wallet: Wallet + ) { + this.storage = storage; + this.sourceType = sourceType; + this.targetType = targetType; + this.sourceNetwork = sourceNetwork; + this.targetNetwork = targetNetwork; + this.targetContractAddress = targetContractAddress; + this.wallet = new Wallet(wallet.privateKey); + } + + public async work() { + const events = await this.storage.getNotExecutedEvents( + this.wallet.address, + this.sourceType, + this.sourceNetwork + ); + if (events.length === 0) return; + + hre.changeNetwork(this.targetNetwork); + const signer = new NonceManager(new GasPriceManager(this.wallet.connect(hre.ethers.provider))); + + const contract = new hre.ethers.Contract( + this.targetContractAddress, + IBridge__factory.createInterface(), + hre.ethers.provider + ) as IBridge; + + for (const event of events) { + const status = await contract.getWithdrawInfo(event.depositId); + if (!status.executed) { + const confirmed = await contract.isConfirmedOf(event.depositId, this.wallet.address); + if (!confirmed) { + if ( + event.withdrawStatus < WithdrawStatus.Sent || + (event.withdrawStatus === WithdrawStatus.Sent && + ContractUtils.getTimeStampBigInt() - event.withdrawTimestamp > 30n) + ) { + try { + logger.info( + `[${this.wallet.address}]-[${this.targetNetwork}]: Starting Withdraw [${event.depositId}]` + ); + const tx = await contract + .connect(signer) + .withdrawFromBridge(event.tokenId, event.depositId, event.account, event.amount); + + logger.info( + `[${this.wallet.address}]-[${this.targetNetwork}]: Sent Withdraw [${event.depositId}]` + ); + await this.storage.setSent( + this.wallet.address, + this.sourceType, + this.sourceNetwork, + event.depositId + ); + } catch (error) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error( + `Failed Executor: [${this.wallet.address}]-[${this.targetNetwork}]: ${msg.code}, ${msg.error.message}` + ); + } + } + } else { + logger.info( + `[${this.wallet.address}]-[${this.targetNetwork}]: Confirmed Withdraw [${event.depositId}]` + ); + await this.storage.setConfirmed( + this.wallet.address, + this.sourceType, + this.sourceNetwork, + event.depositId + ); + } + } else { + logger.info(`[${this.wallet.address}]-[${this.targetNetwork}]: Executed Withdraw [${event.depositId}]`); + await this.storage.setExecuted( + this.wallet.address, + this.sourceType, + this.sourceNetwork, + event.depositId + ); + } + } + } +} diff --git a/packages/validator/src/scheduler/Validator.ts b/packages/validator/src/scheduler/Validator.ts new file mode 100644 index 0000000..8ed57be --- /dev/null +++ b/packages/validator/src/scheduler/Validator.ts @@ -0,0 +1,70 @@ +import { Config } from "../common/Config"; +import { ValidatorStorage } from "../storage/ValidatorStorage"; +import { EventCollector } from "./EventCollector"; + +import { Wallet } from "ethers"; +import { Executor } from "./Executor"; +import { ValidatorType } from "../types"; + +export class Validator { + private config: Config; + private storage: ValidatorStorage; + private readonly wallet: Wallet; + private eventCollectorA: EventCollector; + private eventCollectorB: EventCollector; + + private executorA: Executor; + private executorB: Executor; + + constructor(config: Config, storage: ValidatorStorage, key: string) { + this.config = config; + this.storage = storage; + this.wallet = new Wallet(key); + + this.eventCollectorA = new EventCollector( + storage, + ValidatorType.A, + config.bridge.networkAName, + config.bridge.networkAContractAddress, + 1n, + this.wallet + ); + + this.eventCollectorB = new EventCollector( + storage, + ValidatorType.B, + config.bridge.networkBName, + config.bridge.networkBContractAddress, + 1n, + this.wallet + ); + + this.executorA = new Executor( + storage, + ValidatorType.A, + config.bridge.networkAName, + ValidatorType.B, + config.bridge.networkBName, + config.bridge.networkBContractAddress, + this.wallet + ); + + this.executorB = new Executor( + storage, + ValidatorType.B, + config.bridge.networkBName, + ValidatorType.A, + config.bridge.networkAName, + config.bridge.networkAContractAddress, + this.wallet + ); + } + + public async work() { + // logger.info(`[${this.wallet.address}]: Validator Work`); + await this.eventCollectorA.work(); + await this.eventCollectorB.work(); + await this.executorA.work(); + await this.executorB.work(); + } +} diff --git a/packages/validator/src/storage/ValidatorStorage.ts b/packages/validator/src/storage/ValidatorStorage.ts index 715afd3..380d775 100644 --- a/packages/validator/src/storage/ValidatorStorage.ts +++ b/packages/validator/src/storage/ValidatorStorage.ts @@ -1,5 +1,5 @@ import { IDatabaseConfig } from "../common/Config"; -import { IBridgeDepositedEvent } from "../types"; +import { IBridgeDepositedEvent, ValidatorType } from "../types"; import { Utils } from "../utils/Utils"; import { Storage } from "./Storage"; @@ -8,7 +8,7 @@ import MybatisMapper from "mybatis-mapper"; import { BigNumber } from "ethers"; import path from "path"; -import { BigNumberish } from "@ethersproject/bignumber"; +import { ContractUtils } from "../utils/ContractUtils"; /** * The class that inserts and reads the ledger into the database. @@ -44,9 +44,11 @@ export class ValidatorStorage extends Storage { await this.queryForMapper("table", "drop_table", {}); } - public setLatestNumber(network: string, blockNumber: bigint): Promise { + public setLatestNumber(validator: string, type: ValidatorType, network: string, blockNumber: bigint): Promise { return new Promise(async (resolve, reject) => { this.queryForMapper("latest_block_number", "set", { + validator, + type, network, blockNumber: blockNumber.toString(10), }) @@ -60,9 +62,11 @@ export class ValidatorStorage extends Storage { }); } - public getLatestNumber(network: string): Promise { + public getLatestNumber(validator: string, type: ValidatorType, network: string): Promise { return new Promise(async (resolve, reject) => { this.queryForMapper("latest_block_number", "get", { + validator, + type, network, }) .then((result) => { @@ -79,11 +83,13 @@ export class ValidatorStorage extends Storage { }); } - public postEvents(events: IBridgeDepositedEvent[]): Promise { + public postEvents(events: IBridgeDepositedEvent[], validator: string, type: ValidatorType): Promise { return new Promise(async (resolve, reject) => { this.queryForMapper("events", "postEvents", { events: events.map((m) => { return { + validator, + type, network: m.network, depositId: m.depositId, tokenId: m.tokenId, @@ -104,9 +110,16 @@ export class ValidatorStorage extends Storage { }); } - public getEvents(network: string, from: bigint): Promise { + public getEvents( + validator: string, + type: ValidatorType, + network: string, + from: bigint + ): Promise { return new Promise(async (resolve, reject) => { this.queryForMapper("events", "getEvents", { + validator, + type, network, from: from.toString(10), }) @@ -121,6 +134,8 @@ export class ValidatorStorage extends Storage { amount: BigNumber.from(m.amount), blockNumber: BigInt(m.blockNumber), transactionHash: m.transactionHash, + withdrawStatus: m.withdrawStatus, + withdrawTimestamp: m.withdrawTimestamp, }; }) ); @@ -131,4 +146,129 @@ export class ValidatorStorage extends Storage { }); }); } + + public getNotConfirmedEvents( + validator: string, + type: ValidatorType, + network: string + ): Promise { + return new Promise(async (resolve, reject) => { + this.queryForMapper("events", "getNotConfirmedEvents", { + validator, + type, + network, + }) + .then((result) => { + resolve( + result.rows.map((m) => { + return { + network: m.network, + depositId: m.depositId, + tokenId: m.tokenId, + account: m.account, + amount: BigNumber.from(m.amount), + blockNumber: BigInt(m.blockNumber), + transactionHash: m.transactionHash, + withdrawStatus: m.withdrawStatus, + withdrawTimestamp: m.withdrawTimestamp, + }; + }) + ); + }) + .catch((reason) => { + if (reason instanceof Error) return reject(reason); + return reject(new Error(reason)); + }); + }); + } + + public getNotExecutedEvents( + validator: string, + type: ValidatorType, + network: string + ): Promise { + return new Promise(async (resolve, reject) => { + this.queryForMapper("events", "getNotExecutedEvents", { + validator, + type, + network, + }) + .then((result) => { + resolve( + result.rows.map((m) => { + return { + network: m.network, + depositId: m.depositId, + tokenId: m.tokenId, + account: m.account, + amount: BigNumber.from(m.amount), + blockNumber: BigInt(m.blockNumber), + transactionHash: m.transactionHash, + withdrawStatus: m.withdrawStatus, + withdrawTimestamp: m.withdrawTimestamp, + }; + }) + ); + }) + .catch((reason) => { + if (reason instanceof Error) return reject(reason); + return reject(new Error(reason)); + }); + }); + } + + public setConfirmed(validator: string, type: ValidatorType, network: string, depositId: string): Promise { + return new Promise(async (resolve, reject) => { + this.queryForMapper("events", "setConfirmed", { + validator, + type, + network, + depositId, + }) + .then(() => { + resolve(); + }) + .catch((reason) => { + if (reason instanceof Error) return reject(reason); + return reject(new Error(reason)); + }); + }); + } + + public setExecuted(validator: string, type: ValidatorType, network: string, depositId: string): Promise { + return new Promise(async (resolve, reject) => { + this.queryForMapper("events", "setExecuted", { + validator, + type, + network, + depositId, + }) + .then(() => { + resolve(); + }) + .catch((reason) => { + if (reason instanceof Error) return reject(reason); + return reject(new Error(reason)); + }); + }); + } + + public setSent(validator: string, type: ValidatorType, network: string, depositId: string): Promise { + return new Promise(async (resolve, reject) => { + this.queryForMapper("events", "setSent", { + validator, + type, + network, + depositId, + withdrawTimestamp: ContractUtils.getTimeStampBigInt().toString(10), + }) + .then(() => { + resolve(); + }) + .catch((reason) => { + if (reason instanceof Error) return reject(reason); + return reject(new Error(reason)); + }); + }); + } } diff --git a/packages/validator/src/storage/mapper/events.xml b/packages/validator/src/storage/mapper/events.xml index 0b9d315..54da41d 100644 --- a/packages/validator/src/storage/mapper/events.xml +++ b/packages/validator/src/storage/mapper/events.xml @@ -3,10 +3,12 @@ - INSERT INTO events ("network", "depositId", "tokenId", "account", "amount", "blockNumber", "transactionHash") + INSERT INTO events ("validator", "type", "network", "depositId", "tokenId", "account", "amount", "blockNumber", "transactionHash") VALUES ( + #{item.validator}, + ${item.type}, #{item.network}, #{item.depositId}, #{item.tokenId}, @@ -16,11 +18,46 @@ #{item.transactionHash} ) - ON CONFLICT ("network", "depositId") DO NOTHING; + ON CONFLICT ("validator", "type", "network", "depositId") DO NOTHING; + + + + + + + + + + diff --git a/packages/validator/src/storage/mapper/latest_block_number.xml b/packages/validator/src/storage/mapper/latest_block_number.xml index 28f1607..d935906 100644 --- a/packages/validator/src/storage/mapper/latest_block_number.xml +++ b/packages/validator/src/storage/mapper/latest_block_number.xml @@ -1,25 +1,28 @@ - INSERT INTO latest_block_number ( + "validator", + "type", "network", "blockNumber" ) VALUES - ( - #{network}, - #{blockNumber} - ) - ON CONFLICT ("network") + ( + #{validator}, + ${type}, + #{network}, + #{blockNumber} + ) + ON CONFLICT ("validator", "type", "network") DO UPDATE - SET + SET "blockNumber" = EXCLUDED."blockNumber"; diff --git a/packages/validator/src/storage/mapper/table.xml b/packages/validator/src/storage/mapper/table.xml index 2cd0677..1746ce7 100644 --- a/packages/validator/src/storage/mapper/table.xml +++ b/packages/validator/src/storage/mapper/table.xml @@ -1,19 +1,22 @@ - CREATE TABLE IF NOT EXISTS latest_block_number ( + "validator" VARCHAR(42) NOT NULL, + "type" INT NOT NULL, "network" VARCHAR(32) NOT NULL, "blockNumber" BIGINT NOT NULL, - PRIMARY KEY ("network") + PRIMARY KEY ("validator", "type", "network") ); CREATE TABLE IF NOT EXISTS events ( + "validator" VARCHAR(42) NOT NULL, + "type" INT NOT NULL, "network" VARCHAR(32) NOT NULL, "depositId" VARCHAR(66) NOT NULL, "tokenId" VARCHAR(66) NOT NULL, @@ -21,17 +24,9 @@ "amount" VARCHAR(256) NOT NULL, "blockNumber" BIGINT NOT NULL, "transactionHash" VARCHAR(66) NOT NULL, - PRIMARY KEY ("network", "depositId") - ); - - - - CREATE TABLE IF NOT EXISTS validators - ( - "validators" VARCHAR(42) NOT NULL, - "depositId" VARCHAR(66) NOT NULL, - "done" VARCHAR(1) DEFAULT 'N', - PRIMARY KEY ("validators", "depositId") + "withdrawStatus" INT DEFAULT 0, + "withdrawTimestamp" BIGINT DEFAULT 0, + PRIMARY KEY ("validator", "type", "network", "depositId") ); diff --git a/packages/validator/src/types/index.ts b/packages/validator/src/types/index.ts index 5c14c87..7a7abf0 100644 --- a/packages/validator/src/types/index.ts +++ b/packages/validator/src/types/index.ts @@ -9,6 +9,20 @@ export interface IBridgeDepositedEvent { tokenId: string; account: string; amount: BigNumber; - blockNumber: BigInt; + blockNumber: bigint; transactionHash: string; + withdrawStatus: WithdrawStatus; + withdrawTimestamp: bigint; +} + +export enum WithdrawStatus { + None = 0, + Sent = 1, + Confirmed = 2, + Executed = 3, +} + +export enum ValidatorType { + A, + B, } diff --git a/packages/validator/test/01-Collector.test.ts b/packages/validator/test/01-Collector.test.ts index 14100e4..cd4b66e 100644 --- a/packages/validator/test/01-Collector.test.ts +++ b/packages/validator/test/01-Collector.test.ts @@ -24,6 +24,7 @@ import * as hre from "hardhat"; import * as assert from "assert"; import path from "path"; import { EventCollector } from "../src/scheduler/EventCollector"; +import { ValidatorType } from "../src/types"; chai.use(solidity); @@ -34,7 +35,7 @@ interface IShopData { wallet: Wallet; } -describe("Test for Ledger", () => { +describe("Test for EventCollector", () => { const deployments = new Deployments(); let tokenContract: BIP20DelegatedTransfer; let bridgeContract: Bridge; @@ -45,6 +46,7 @@ describe("Test for Ledger", () => { const fee = Amount.make(5, 18).value; let collector: EventCollector; + let validatorWallet: Wallet; const deployAllContract = async (shopData: IShopData[]) => { await deployments.doDeployAll(); @@ -55,11 +57,12 @@ describe("Test for Ledger", () => { before("Create Config", async () => { config = new Config(); config.readFromFile(path.resolve(process.cwd(), "config", "config_test.yaml")); + validatorWallet = new Wallet(config.bridge.validators[0]); storage = await ValidatorStorage.make(config.database); await storage.clearTestDB(); }); - after("Stop TestServer", async () => { + after("Stop DB", async () => { await storage.dropTestDB(); }); @@ -71,12 +74,19 @@ describe("Test for Ledger", () => { }); it("Create EventCollector", async () => { - collector = new EventCollector(config, storage, "hardhat", bridgeContract.address, 1n); + collector = new EventCollector( + storage, + ValidatorType.A, + "hardhat", + bridgeContract.address, + 1n, + validatorWallet + ); }); it("EventCollector.work()", async () => { await collector.work(); - const events = await storage.getEvents("hardhat", 0n); + const events = await storage.getEvents(validatorWallet.address, ValidatorType.A, "hardhat", 0n); assert.deepStrictEqual(events.length, 0); }); @@ -139,7 +149,7 @@ describe("Test for Ledger", () => { it("EventCollector.work()", async () => { await collector.work(); - const events = await storage.getEvents("hardhat", 0n); + const events = await storage.getEvents(validatorWallet.address, ValidatorType.A, "hardhat", 0n); assert.deepStrictEqual(events.length, 1); assert.deepStrictEqual(events[0].network, "hardhat"); assert.deepStrictEqual(events[0].tokenId, tokenId0); @@ -179,7 +189,7 @@ describe("Test for Ledger", () => { it("EventCollector.work()", async () => { await collector.work(); - const events = await storage.getEvents("hardhat", 0n); + const events = await storage.getEvents(validatorWallet.address, ValidatorType.A, "hardhat", 0n); assert.deepStrictEqual(events.length, 2); assert.deepStrictEqual(events[1].network, "hardhat"); assert.deepStrictEqual(events[1].tokenId, tokenId1); diff --git a/packages/validator/test/02-Bridge.test.ts b/packages/validator/test/02-Bridge.test.ts new file mode 100644 index 0000000..44514f9 --- /dev/null +++ b/packages/validator/test/02-Bridge.test.ts @@ -0,0 +1,222 @@ +import "@nomiclabs/hardhat-ethers"; +import "@nomiclabs/hardhat-waffle"; +import "@nomiclabs/hardhat-web3"; +import "@openzeppelin/hardhat-upgrades"; + +import { Amount } from "../src/common/Amount"; +import { Config } from "../src/common/Config"; +import { Scheduler } from "../src/scheduler/Scheduler"; +import { ValidatorStorage } from "../src/storage/ValidatorStorage"; +import { ContractUtils } from "../src/utils/ContractUtils"; +import { BIP20DelegatedTransfer, Bridge } from "../typechain-types"; +import { Deployments } from "./helper/Deployments"; +import { TestServer } from "./helper/Utility"; + +import chai, { expect } from "chai"; +import { solidity } from "ethereum-waffle"; + +// tslint:disable-next-line:no-implicit-dependencies +import { arrayify } from "@ethersproject/bytes"; +import { AddressZero, HashZero } from "@ethersproject/constants"; + +import * as assert from "assert"; +import { Wallet } from "ethers"; +import * as hre from "hardhat"; +import path from "path"; + +import { URL } from "url"; +import { BridgeScheduler } from "../src/scheduler/BridgeScheduler"; + +chai.use(solidity); + +interface IShopData { + shopId: string; + name: string; + currency: string; + wallet: Wallet; +} + +describe("Test for Bridge", () => { + const deployments = new Deployments(); + let tokenContract: BIP20DelegatedTransfer; + let bridgeAContract: Bridge; + let bridgeBContract: Bridge; + let config: Config; + let storage: ValidatorStorage; + let server: TestServer; + let serverURL: URL; + + const amount = Amount.make(100_000, 18).value; + + let tokenId0: string; + let tokenId1: string; + let depositId: string; + + before("Deploy", async () => { + await deployments.doDeployAll(); + tokenContract = deployments.getContract("TestKIOS") as BIP20DelegatedTransfer; + bridgeAContract = deployments.getContract("BridgeA") as Bridge; + bridgeBContract = deployments.getContract("BridgeB") as Bridge; + }); + + before("Create Config", async () => { + config = new Config(); + config.readFromFile(path.resolve(process.cwd(), "config", "config_test.yaml")); + config.bridge.networkAContractAddress = bridgeAContract.address; + config.bridge.networkBContractAddress = bridgeBContract.address; + }); + + before("Create TestServer", async () => { + serverURL = new URL(`http://127.0.0.1:${config.server.port}`); + storage = await ValidatorStorage.make(config.database); + await storage.clearTestDB(); + + const schedulers: Scheduler[] = []; + schedulers.push(new BridgeScheduler("*/1 * * * * *")); + server = new TestServer(config, storage, schedulers); + }); + + before("Register token", async () => { + // Native Token + tokenId0 = HashZero; + await bridgeAContract.connect(deployments.accounts.deployer).registerToken(HashZero, AddressZero); + await bridgeBContract.connect(deployments.accounts.deployer).registerToken(HashZero, AddressZero); + // BIP20 Token + tokenId1 = ContractUtils.getTokenId(await tokenContract.name(), await tokenContract.symbol()); + await bridgeAContract.connect(deployments.accounts.deployer).registerToken(tokenId1, tokenContract.address); + await bridgeBContract.connect(deployments.accounts.deployer).registerToken(tokenId1, tokenContract.address); + }); + + before("Deposit Native Liquidity at Bridge A", async () => { + const liquidityAmount = Amount.make(1_000_000_000, 18).value; + const signature = await ContractUtils.signMessage(deployments.accounts.deployer, arrayify(HashZero)); + const tx1 = await bridgeAContract + .connect(deployments.accounts.deployer) + .depositLiquidity(tokenId0, liquidityAmount, signature, { value: liquidityAmount }); + await tx1.wait(); + }); + + before("Deposit Native Liquidity at Bridge B", async () => { + const liquidityAmount = Amount.make(1_000_000_000, 18).value; + const signature = await ContractUtils.signMessage(deployments.accounts.deployer, arrayify(HashZero)); + const tx1 = await bridgeBContract + .connect(deployments.accounts.deployer) + .depositLiquidity(tokenId0, liquidityAmount, signature, { value: liquidityAmount }); + await tx1.wait(); + }); + + before("Deposit BIP20 Liquidity at Bridge A", async () => { + const liquidityAmount = Amount.make(1_000_000_000, 18).value; + const nonce = await (deployments.getContract("TestKIOS") as BIP20DelegatedTransfer).nonceOf( + deployments.accounts.deployer.address + ); + const message = ContractUtils.getTransferMessage( + deployments.accounts.deployer.address, + bridgeAContract.address, + liquidityAmount, + nonce + ); + const signature = await ContractUtils.signMessage(deployments.accounts.deployer, message); + const tx1 = await bridgeAContract + .connect(deployments.accounts.deployer) + .depositLiquidity(tokenId1, liquidityAmount, signature); + await tx1.wait(); + }); + + before("Deposit BIP20 Liquidity at Bridge B", async () => { + const liquidityAmount = Amount.make(1_000_000_000, 18).value; + const nonce = await (deployments.getContract("TestKIOS") as BIP20DelegatedTransfer).nonceOf( + deployments.accounts.deployer.address + ); + const message = ContractUtils.getTransferMessage( + deployments.accounts.deployer.address, + bridgeBContract.address, + liquidityAmount, + nonce + ); + const signature = await ContractUtils.signMessage(deployments.accounts.deployer, message); + const tx1 = await bridgeBContract + .connect(deployments.accounts.deployer) + .depositLiquidity(tokenId1, liquidityAmount, signature); + await tx1.wait(); + }); + + before("Start TestServer", async () => { + await server.start(); + }); + + after("Stop TestServer", async () => { + await server.stop(); + await storage.dropTestDB(); + }); + + it("Deposit native token to Main Bridge", async () => { + const oldLiquidity = await hre.ethers.provider.getBalance(bridgeAContract.address); + depositId = ContractUtils.getRandomId(deployments.accounts.users[0].address); + const signature = await ContractUtils.signMessage(deployments.accounts.users[0], arrayify(HashZero)); + await expect( + bridgeAContract + .connect(deployments.accounts.users[0]) + .depositToBridge(tokenId0, depositId, AddressZero, 0, signature, { + value: amount, + }) + ) + .to.emit(bridgeAContract, "BridgeDeposited") + .withNamedArgs({ + tokenId: tokenId0, + depositId, + account: deployments.accounts.users[0].address, + amount, + }); + expect(await hre.ethers.provider.getBalance(bridgeAContract.address)).to.deep.equal(oldLiquidity.add(amount)); + }); + + it("Waiting", async () => { + const t1 = ContractUtils.getTimeStamp(); + while (true) { + const info = await bridgeBContract.getWithdrawInfo(depositId); + if (info.executed) break; + else if (ContractUtils.getTimeStamp() - t1 > 60) break; + await ContractUtils.delay(1000); + } + }); + + it("Deposit BIB20 token to Main Bridge", async () => { + const oldLiquidity = await tokenContract.balanceOf(bridgeBContract.address); + const oldTokenBalance = await tokenContract.balanceOf(deployments.accounts.users[0].address); + const nonce = await tokenContract.nonceOf(deployments.accounts.users[0].address); + const message = ContractUtils.getTransferMessage( + deployments.accounts.users[0].address, + bridgeBContract.address, + amount, + nonce + ); + depositId = ContractUtils.getRandomId(deployments.accounts.users[0].address); + const signature = await ContractUtils.signMessage(deployments.accounts.users[0], message); + await expect( + bridgeBContract + .connect(deployments.accounts.deployer) + .depositToBridge(tokenId1, depositId, deployments.accounts.users[0].address, amount, signature) + ) + .to.emit(bridgeBContract, "BridgeDeposited") + .withNamedArgs({ + depositId, + account: deployments.accounts.users[0].address, + amount, + }); + expect(await tokenContract.balanceOf(deployments.accounts.users[0].address)).to.deep.equal( + oldTokenBalance.sub(amount) + ); + expect(await tokenContract.balanceOf(bridgeBContract.address)).to.deep.equal(oldLiquidity.add(amount)); + }); + + it("Waiting", async () => { + const t1 = ContractUtils.getTimeStamp(); + while (true) { + const info = await bridgeAContract.getWithdrawInfo(depositId); + if (info.executed) break; + else if (ContractUtils.getTimeStamp() - t1 > 60) break; + await ContractUtils.delay(1000); + } + }); +}); diff --git a/packages/validator/test/helper/Deployments.ts b/packages/validator/test/helper/Deployments.ts index 3689d3e..6e6f987 100644 --- a/packages/validator/test/helper/Deployments.ts +++ b/packages/validator/test/helper/Deployments.ts @@ -112,7 +112,13 @@ export class Deployments { } public async doDeployAll() { - const deployers: FnDeployer[] = [deployToken, deployBridgeValidator, deployBridge]; + const deployers: FnDeployer[] = [ + deployToken, + deployBridgeValidator, + deployBridge, + deployBridgeA, + deployBridgeB, + ]; for (const elem of deployers) { try { await elem(this.accounts, this); @@ -164,7 +170,7 @@ async function deployBridgeValidator(accounts: IAccount, deployment: Deployments const factory = await ethers.getContractFactory("BridgeValidator"); const contract = (await upgrades.deployProxy( factory.connect(accounts.deployer), - [accounts.bridgeValidators.map((m) => m.address), 2], + [accounts.bridgeValidators.map((m) => m.address), 3], { initializer: "initialize", kind: "uups", @@ -198,3 +204,49 @@ async function deployBridge(accounts: IAccount, deployment: Deployments) { deployment.addContract(contractName, contract.address, contract); console.log(`Deployed ${contractName} to ${contract.address}`); } + +async function deployBridgeA(accounts: IAccount, deployment: Deployments) { + const contractName = "BridgeA"; + console.log(`Deploy ${contractName}...`); + + if (deployment.getContract("BridgeValidator") === undefined || deployment.getContract("TestKIOS") === undefined) { + console.error("Contract is not deployed!"); + return; + } + const factory = await ethers.getContractFactory("Bridge"); + const contract = (await upgrades.deployProxy( + factory.connect(accounts.deployer), + [await deployment.getContractAddress("BridgeValidator"), accounts.fee.address], + { + initializer: "initialize", + kind: "uups", + } + )) as Bridge; + await contract.deployed(); + await contract.deployTransaction.wait(); + deployment.addContract(contractName, contract.address, contract); + console.log(`Deployed ${contractName} to ${contract.address}`); +} + +async function deployBridgeB(accounts: IAccount, deployment: Deployments) { + const contractName = "BridgeB"; + console.log(`Deploy ${contractName}...`); + + if (deployment.getContract("BridgeValidator") === undefined || deployment.getContract("TestKIOS") === undefined) { + console.error("Contract is not deployed!"); + return; + } + const factory = await ethers.getContractFactory("Bridge"); + const contract = (await upgrades.deployProxy( + factory.connect(accounts.deployer), + [await deployment.getContractAddress("BridgeValidator"), accounts.fee.address], + { + initializer: "initialize", + kind: "uups", + } + )) as Bridge; + await contract.deployed(); + await contract.deployTransaction.wait(); + deployment.addContract(contractName, contract.address, contract); + console.log(`Deployed ${contractName} to ${contract.address}`); +} diff --git a/packages/validator/test/helper/Utility.ts b/packages/validator/test/helper/Utility.ts new file mode 100644 index 0000000..d01e5f1 --- /dev/null +++ b/packages/validator/test/helper/Utility.ts @@ -0,0 +1,69 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; +import { DefaultServer } from "../../src/DefaultServer"; +import { handleNetworkError } from "../../src/network/ErrorTypes"; + +export class TestServer extends DefaultServer {} + +/** + * This is a client for testing. + * Test codes can easily access error messages received from the server. + */ +export class TestClient { + private client: AxiosInstance; + + constructor() { + this.client = axios.create(); + } + + public get(url: string, config?: AxiosRequestConfig): Promise { + return new Promise((resolve, reject) => { + this.client + .get(url, config) + .then((response: AxiosResponse) => { + resolve(response); + }) + .catch((reason: any) => { + reject(handleNetworkError(reason)); + }); + }); + } + + public delete(url: string, config?: AxiosRequestConfig): Promise { + return new Promise((resolve, reject) => { + this.client + .delete(url, config) + .then((response: AxiosResponse) => { + resolve(response); + }) + .catch((reason: any) => { + reject(handleNetworkError(reason)); + }); + }); + } + + public post(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return new Promise((resolve, reject) => { + this.client + .post(url, data, config) + .then((response: AxiosResponse) => { + resolve(response); + }) + .catch((reason: any) => { + reject(handleNetworkError(reason)); + }); + }); + } + + public put(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return new Promise((resolve, reject) => { + this.client + .put(url, data, config) + .then((response: AxiosResponse) => { + resolve(response); + }) + .catch((reason: any) => { + reject(handleNetworkError(reason)); + }); + }); + } +}