diff --git a/ibc.ts/.editorconfig b/ibc.ts/.editorconfig new file mode 100644 index 0000000000..e9149aad0b --- /dev/null +++ b/ibc.ts/.editorconfig @@ -0,0 +1,5 @@ +[src/**.{ts,json,js}] +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 diff --git a/ibc.ts/.env.default b/ibc.ts/.env.default new file mode 100644 index 0000000000..cff08b4f12 --- /dev/null +++ b/ibc.ts/.env.default @@ -0,0 +1,6 @@ +CHAIN_A_RPC_URL="http://localhost:8080" +CHAIN_A_NETWORK_ID="tc" +CHAIN_A_FAUCET_ADDRESS="tccqym6znlgc48qeelrzccehkcaut7yz39wwq96q3y7" +CHAIN_B_RPC_URL="http://localhost:8081" +CHAIN_B_NETWORK_ID="fc" +CHAIN_B_FAUCET_ADDRESS="fccqyd6clszl2aeq4agrk8sgq8whkty6ktljuemc9y3" diff --git a/ibc.ts/.gitignore b/ibc.ts/.gitignore new file mode 100644 index 0000000000..5b04d32945 --- /dev/null +++ b/ibc.ts/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +yarn-error.log +yarn.lock +build/ +.env diff --git a/ibc.ts/README.md b/ibc.ts/README.md new file mode 100644 index 0000000000..a7e0834812 --- /dev/null +++ b/ibc.ts/README.md @@ -0,0 +1,4 @@ +IBC relayer and scenario +========================= + +This directory contains IBC relayer implementation and IBC demo scenario script. diff --git a/ibc.ts/package.json b/ibc.ts/package.json new file mode 100644 index 0000000000..e7aa525942 --- /dev/null +++ b/ibc.ts/package.json @@ -0,0 +1,26 @@ +{ + "name": "ibc", + "version": "0.0.0", + "scripts": { + "build": "tsc -p .", + "fmt": "tslint -p . --fix && prettier 'src/**/*.{ts, json}' --write", + "lint": "tsc -p . --noEmit && tslint -p . && prettier 'src/**/*.{ts, json}' -l", + "scenario": "yarn build && node build/scenario/index.js", + "relayer": "yarn build && node build/relayer/index.js" + }, + "devDependencies": { + "@types/debug": "^4.1.5", + "@types/node": "^13.7.4", + "prettier": "^1.19.1", + "tslint": "^6.0.0", + "tslint-config-prettier": "^1.18.0", + "typescript": "^3.8.2" + }, + "dependencies": { + "codechain-primitives": "^1.0.4", + "codechain-sdk": "^2.0.1", + "debug": "^4.1.1", + "dotenv": "^8.2.0", + "rlp": "^2.0.0" + } +} diff --git a/ibc.ts/src/common/chain.ts b/ibc.ts/src/common/chain.ts new file mode 100644 index 0000000000..1f28bd541e --- /dev/null +++ b/ibc.ts/src/common/chain.ts @@ -0,0 +1,60 @@ +import { Datagram } from "./datagram/index"; +import { SDK } from "codechain-sdk"; +import { H256, PlatformAddress } from "codechain-primitives"; +import { IBC } from "./foundry/transaction"; +import { delay } from "./util"; +import Debug from "debug"; + +const debug = Debug("common:tx"); + +export interface ChainConfig { + /** + * Example: "http://localhost:8080" + */ + server: string; + networkId: string; + faucetAddress: PlatformAddress; +} + +export class Chain { + private readonly sdk: SDK; + private readonly faucetAddress: PlatformAddress; + + public constructor(config: ChainConfig) { + this.sdk = new SDK({ + server: config.server, + networkId: config.networkId + }); + this.faucetAddress = config.faucetAddress; + } + + public async submitDatagram(datagram: Datagram): Promise { + const ibcAction = new IBC(this.sdk.networkId, datagram.rlpBytes()); + + const seq = await this.sdk.rpc.chain.getSeq(this.faucetAddress); + const signedTx = await this.sdk.key.signTransaction(ibcAction, { + account: this.faucetAddress, + fee: 100, + seq + }); + + const txHash = await this.sdk.rpc.chain.sendSignedTransaction(signedTx); + waitForTx(this.sdk, txHash); + } +} + +async function waitForTx(sdk: SDK, txHash: H256) { + const timeout = delay(10 * 1000).then(() => { + throw new Error("Timeout"); + }); + const wait = (async () => { + while (true) { + debug(`wait tx: ${txHash.toString()}`); + if (sdk.rpc.chain.containsTransaction(txHash)) { + return; + } + await delay(500); + } + })(); + return Promise.race([timeout, wait]); +} diff --git a/ibc.ts/src/common/datagram/createClient.ts b/ibc.ts/src/common/datagram/createClient.ts new file mode 100644 index 0000000000..fb0a513358 --- /dev/null +++ b/ibc.ts/src/common/datagram/createClient.ts @@ -0,0 +1,33 @@ +const RLP = require("rlp"); + +export class CreateClientDatagram { + private id: string; + private kind: number; + private consensusState: Buffer; + private data: Buffer; + + public constructor({ + id, + kind, + consensusState, + data + }: { + id: string; + kind: number; + consensusState: Buffer; + data: Buffer; + }) { + this.id = id; + this.kind = kind; + this.consensusState = consensusState; + this.data = data; + } + + public rlpBytes(): Buffer { + return RLP.encode(this.toEncodeObject()); + } + + public toEncodeObject(): any[] { + return [this.id, this.kind, this.consensusState, this.data]; + } +} diff --git a/ibc.ts/src/common/datagram/index.ts b/ibc.ts/src/common/datagram/index.ts new file mode 100644 index 0000000000..4b10241349 --- /dev/null +++ b/ibc.ts/src/common/datagram/index.ts @@ -0,0 +1,4 @@ +export interface Datagram { + rlpBytes(): Buffer; + toEncodeObject(): any[]; +} diff --git a/ibc.ts/src/common/example.ts b/ibc.ts/src/common/example.ts new file mode 100644 index 0000000000..5a027ec8e9 --- /dev/null +++ b/ibc.ts/src/common/example.ts @@ -0,0 +1 @@ +export let HelloWorld = "HelloWorld"; diff --git a/ibc.ts/src/common/foundry/transaction.ts b/ibc.ts/src/common/foundry/transaction.ts new file mode 100644 index 0000000000..f90fdfd4ed --- /dev/null +++ b/ibc.ts/src/common/foundry/transaction.ts @@ -0,0 +1,30 @@ +import { Transaction } from "codechain-sdk/lib/core/classes"; +import { NetworkId } from "codechain-sdk/lib/core/types"; + +export interface IBCActionJSON { + bytes: Buffer; +} + +export class IBC extends Transaction { + private readonly bytes: Buffer; + + public constructor(networkId: NetworkId, bytes: Buffer) { + super(networkId); + this.bytes = bytes; + } + + public type(): string { + return "ibc"; + } + + protected actionToEncodeObject(): any[] { + return [0x20, this.bytes]; + } + + // Since the result type is hard-coded in the SDK, we should use any type here. + protected actionToJSON(): any { + return { + bytes: this.bytes + }; + } +} diff --git a/ibc.ts/src/common/util.ts b/ibc.ts/src/common/util.ts new file mode 100644 index 0000000000..56654f312d --- /dev/null +++ b/ibc.ts/src/common/util.ts @@ -0,0 +1,7 @@ +export async function delay(ms: number): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, ms); + }); +} diff --git a/ibc.ts/src/relayer/config.ts b/ibc.ts/src/relayer/config.ts new file mode 100644 index 0000000000..18344b0d94 --- /dev/null +++ b/ibc.ts/src/relayer/config.ts @@ -0,0 +1,38 @@ +export interface Config { + chainA: FoundryChainConfig; + chainB: FoundryChainConfig; +} + +interface FoundryChainConfig { + /** + * Foundry RPC URL + * ex) http://localhost:8080 + */ + rpcURL: string; + networkId: string; + faucetAddress: string; +} + +export function getConfig(): Config { + return { + chainA: { + rpcURL: getEnv("CHAIN_A_RPC_URL"), + networkId: getEnv("CHAIN_A_NETWORK_ID"), + faucetAddress: getEnv("CHAIN_A_FAUCET_ADDRESS") + }, + chainB: { + rpcURL: getEnv("CHAIN_B_RPC_URL"), + networkId: getEnv("CHAIN_B_NETWORK_ID"), + faucetAddress: getEnv("CHAIN_B_FAUCET_ADDRESS") + } + }; +} + +function getEnv(key: string): string { + const result = process.env[key]; + if (result) { + return result; + } else { + throw new Error(`Environment variable ${key} is not set`); + } +} diff --git a/ibc.ts/src/relayer/index.ts b/ibc.ts/src/relayer/index.ts new file mode 100644 index 0000000000..ce55775d92 --- /dev/null +++ b/ibc.ts/src/relayer/index.ts @@ -0,0 +1,64 @@ +import Debug from "debug"; +import { Chain } from "../common/chain"; +import { Datagram } from "../common/datagram/index"; +import { delay } from "../common/util"; +import { getConfig } from "./config"; +import { PlatformAddress } from "codechain-primitives/lib"; + +require("dotenv").config(); + +const debug = Debug("relayer:main"); + +async function main() { + const config = getConfig(); + const chainA = new Chain({ + server: config.chainA.rpcURL, + networkId: config.chainA.networkId, + faucetAddress: PlatformAddress.fromString(config.chainA.faucetAddress) + }); + const chainB = new Chain({ + server: config.chainB.rpcURL, + networkId: config.chainB.networkId, + faucetAddress: PlatformAddress.fromString(config.chainB.faucetAddress) + }); + + while (true) { + debug("Run relay"); + await relay(chainA, chainB); + await delay(1000); + } +} + +main().catch(console.error); + +async function relay(chainA: Chain, chainB: Chain) { + await relayFromTo({ chain: chainA, counterpartyChain: chainB }); + await relayFromTo({ chain: chainB, counterpartyChain: chainA }); +} + +async function relayFromTo({ + chain, + counterpartyChain +}: { + chain: Chain; + counterpartyChain: Chain; +}) { + const { localDatagrams, counterpartyDatagrams } = await pendingDatagrams({ + chain, + counterpartyChain + }); + + for (const localDiagram of localDatagrams) { + await chain.submitDatagram(localDiagram); + } + + for (const counterpartyDatagram of counterpartyDatagrams) { + await counterpartyChain.submitDatagram(counterpartyDatagram); + } +} + +async function pendingDatagrams( + args: any +): Promise<{ localDatagrams: Datagram[]; counterpartyDatagrams: Datagram[] }> { + return { localDatagrams: [], counterpartyDatagrams: [] }; +} diff --git a/ibc.ts/src/scenario/index.ts b/ibc.ts/src/scenario/index.ts new file mode 100644 index 0000000000..f120bc805c --- /dev/null +++ b/ibc.ts/src/scenario/index.ts @@ -0,0 +1,7 @@ +import { HelloWorld } from "../common/example"; + +async function main() { + console.log(HelloWorld); +} + +main().catch(console.error); diff --git a/ibc.ts/tsconfig.json b/ibc.ts/tsconfig.json new file mode 100644 index 0000000000..33c839d21c --- /dev/null +++ b/ibc.ts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2018", + "types": ["node"], + "module": "commonjs", + "lib": ["es2018"], + "declaration": true, + "outDir": "build", + "strict": true + }, + "include": ["./src/**/*"], + "exclude": ["./src/**/*.test.ts"] +} diff --git a/ibc.ts/tslint.json b/ibc.ts/tslint.json new file mode 100644 index 0000000000..b941d3b734 --- /dev/null +++ b/ibc.ts/tslint.json @@ -0,0 +1,27 @@ +{ + "extends": ["tslint:recommended", "tslint-config-prettier"], + "rules": { + "interface-name": false, + "variable-name": [ + true, + "check-format", + "allow-leading-underscore", + "allow-pascal-case" + ], + "no-console": false, + "object-literal-sort-keys": false, + "only-arrow-functions": false, + "no-var-requires": false, + "max-classes-per-file": false, + "triple-equals": [true, "allow-null-check", "allow-undefined-check"], + "no-bitwise": false, + "array-type": false + }, + "jsRules": { + "no-console": false, + "object-literal-sort-keys": false + }, + "linterOptions": { + "exclude": ["node_modules/**/*.ts", "/build/*"] + } +}