diff --git a/.circleci/config.yml b/.circleci/config.yml index 3916847720..28e6a7176b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,6 +5,7 @@ workflows: jobs: - build - lint + - faucet_docker jobs: build: @@ -53,3 +54,18 @@ jobs: - ~/.cache/yarn - run: command: yarn lint + faucet_docker: + docker: + - image: circleci/node:10 + steps: + - checkout + - setup_remote_docker: + docker_layer_caching: true + - run: + name: Build docker image + command: docker build -t cosmwasm/faucet:manual --file faucet.Dockerfile . + - run: + name: Test docker image + command: | + docker run --read-only --rm cosmwasm/faucet:manual help + docker run --read-only --rm cosmwasm/faucet:manual version diff --git a/.eslintrc.json b/.eslintrc.json index cbbaf4b502..738b64b772 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -50,12 +50,6 @@ "rules": { "@typescript-eslint/camelcase": ["error", { "properties": "never" }] } - }, - { - "files": "packages/{iov-bns,iov-tendermint-rpc}/**/*.ts", - "rules": { - "@typescript-eslint/camelcase": ["error", { "allow": ["v0_[0-9]+"] }] - } } ] } diff --git a/NOTICE b/NOTICE index 24ba00c883..216009de48 100644 --- a/NOTICE +++ b/NOTICE @@ -2,6 +2,9 @@ This repository was forked from the folders packages/iov-cosmos and scripts/cosm of https://github.com/iov-one/iov-core at tag v2.0.0-alpha.7. It was repurposed and heavily modified from there on. +The code in packages/faucet was forked from https://github.com/iov-one/iov-faucet on +2020-01-29 at commit 33e2d707e7. + Copyright 2018-2020 IOV SAS Copyright 2020 Confio UO Copyright 2020 Simon Warta diff --git a/faucet.Dockerfile b/faucet.Dockerfile new file mode 100644 index 0000000000..51cbb8d687 --- /dev/null +++ b/faucet.Dockerfile @@ -0,0 +1,23 @@ +# Start the build environment +# https://hub.docker.com/_/node/ +FROM node:12.14-alpine AS build-env + +ADD package.json yarn.lock tsconfig.json lerna.json /build_repo_root/ +ADD packages/bcp /build_repo_root/packages/bcp +ADD packages/faucet /build_repo_root/packages/faucet + +WORKDIR /build_repo_root +RUN yarn install --frozen-lockfile +RUN yarn build + +# Start the runtime environment +FROM node:12.14-alpine +COPY --from=build-env /build_repo_root/package.json /run_repo_root/ +COPY --from=build-env /build_repo_root/yarn.lock /run_repo_root/ +COPY --from=build-env /build_repo_root/packages /run_repo_root/packages +WORKDIR /run_repo_root +RUN yarn install --frozen-lockfile --production + +EXPOSE 8000 +ENTRYPOINT ["/run_repo_root/packages/faucet/bin/cosm-faucet"] +CMD [""] diff --git a/packages/bcp/src/cosmosconnector.ts b/packages/bcp/src/cosmosconnector.ts deleted file mode 100644 index fd9c82e41e..0000000000 --- a/packages/bcp/src/cosmosconnector.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ChainConnector, ChainId } from "@iov/bcp"; - -import { CosmosBech32Prefix } from "./address"; -import { cosmosCodec } from "./cosmoscodec"; -import { CosmosConnection } from "./cosmosconnection"; -import { TokenInfos } from "./types"; - -/** - * A helper to connect to a cosmos-based chain at a given url - */ -export function createCosmosConnector( - url: string, - prefix: CosmosBech32Prefix, - tokenInfo: TokenInfos, - expectedChainId?: ChainId, -): ChainConnector { - return { - establishConnection: async () => CosmosConnection.establish(url, prefix, tokenInfo), - codec: cosmosCodec, - expectedChainId: expectedChainId, - }; -} diff --git a/packages/bcp/src/cosmoscodec.spec.ts b/packages/bcp/src/cosmwasmcodec.spec.ts similarity index 69% rename from packages/bcp/src/cosmoscodec.spec.ts rename to packages/bcp/src/cosmwasmcodec.spec.ts index a4a046ece1..1829b9364f 100644 --- a/packages/bcp/src/cosmoscodec.spec.ts +++ b/packages/bcp/src/cosmwasmcodec.spec.ts @@ -1,12 +1,12 @@ import { PostableBytes, PrehashType } from "@iov/bcp"; import { Encoding } from "@iov/encoding"; -import { cosmosCodec } from "./cosmoscodec"; +import { cosmWasmCodec } from "./cosmwasmcodec"; import { chainId, nonce, sendTxJson, signedTxBin, signedTxJson, txId } from "./testdata.spec"; const { toUtf8 } = Encoding; -describe("cosmoscodec", () => { +describe("cosmWasmCodec", () => { it("properly generates bytes to sign", () => { const expected = { bytes: toUtf8( @@ -14,36 +14,36 @@ describe("cosmoscodec", () => { ), prehashType: PrehashType.Sha256, }; - const bytesToSign = cosmosCodec.bytesToSign(sendTxJson, nonce); + const bytesToSign = cosmWasmCodec.bytesToSign(sendTxJson, nonce); expect(bytesToSign).toEqual(expected); }); it("properly encodes transactions", () => { - const encoded = cosmosCodec.bytesToPost(signedTxJson); + const encoded = cosmWasmCodec.bytesToPost(signedTxJson); expect(encoded).toEqual(signedTxBin); }); it("throws when trying to decode a transaction without a nonce", () => { - expect(() => cosmosCodec.parseBytes(signedTxBin as PostableBytes, chainId)).toThrowError( + expect(() => cosmWasmCodec.parseBytes(signedTxBin as PostableBytes, chainId)).toThrowError( /nonce is required/i, ); }); it("properly decodes transactions", () => { - const decoded = cosmosCodec.parseBytes(signedTxBin as PostableBytes, chainId, nonce); + const decoded = cosmWasmCodec.parseBytes(signedTxBin as PostableBytes, chainId, nonce); expect(decoded).toEqual(signedTxJson); }); it("generates transaction id", () => { - const id = cosmosCodec.identifier(signedTxJson); + const id = cosmWasmCodec.identifier(signedTxJson); expect(id).toMatch(/^[0-9A-F]{64}$/); expect(id).toEqual(txId); }); it("round trip works", () => { - const encoded = cosmosCodec.bytesToPost(signedTxJson); - const decoded = cosmosCodec.parseBytes(encoded, chainId, nonce); + const encoded = cosmWasmCodec.bytesToPost(signedTxJson); + const decoded = cosmWasmCodec.parseBytes(encoded, chainId, nonce); expect(decoded).toEqual(signedTxJson); }); }); diff --git a/packages/bcp/src/cosmoscodec.ts b/packages/bcp/src/cosmwasmcodec.ts similarity index 94% rename from packages/bcp/src/cosmoscodec.ts rename to packages/bcp/src/cosmwasmcodec.ts index f4b3619d5c..3d89c5891b 100644 --- a/packages/bcp/src/cosmoscodec.ts +++ b/packages/bcp/src/cosmwasmcodec.ts @@ -44,7 +44,7 @@ function sortJson(json: any): any { return result; } -export class CosmosCodec implements TxCodec { +export class CosmWasmCodec implements TxCodec { private readonly prefix: CosmosBech32Prefix; private readonly tokens: TokenInfos; @@ -113,4 +113,5 @@ const defaultTokens: TokenInfos = [ }, ]; -export const cosmosCodec = new CosmosCodec(defaultPrefix, defaultTokens); +/** Unconfigured codec is useful for testing only */ +export const cosmWasmCodec = new CosmWasmCodec(defaultPrefix, defaultTokens); diff --git a/packages/bcp/src/cosmosconnection.spec.ts b/packages/bcp/src/cosmwasmconnection.spec.ts similarity index 90% rename from packages/bcp/src/cosmosconnection.spec.ts rename to packages/bcp/src/cosmwasmconnection.spec.ts index fc916c5647..3bb665fcf2 100644 --- a/packages/bcp/src/cosmosconnection.spec.ts +++ b/packages/bcp/src/cosmwasmconnection.spec.ts @@ -15,8 +15,8 @@ import { Encoding } from "@iov/encoding"; import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; import { CosmosBech32Prefix } from "./address"; -import { CosmosCodec, cosmosCodec } from "./cosmoscodec"; -import { CosmosConnection } from "./cosmosconnection"; +import { CosmWasmCodec, cosmWasmCodec } from "./cosmwasmcodec"; +import { CosmWasmConnection } from "./cosmwasmconnection"; import { nonceToSequence, TokenInfos } from "./types"; const { fromBase64, toHex } = Encoding; @@ -27,7 +27,7 @@ function pendingWithoutCosmos(): void { } } -describe("CosmosConnection", () => { +describe("CosmWasmConnection", () => { const cosm = "COSM" as TokenTicker; const httpUrl = "http://localhost:1317"; const defaultChainId = "cosmos:testing" as ChainId; @@ -63,7 +63,7 @@ describe("CosmosConnection", () => { describe("establish", () => { it("can connect to Cosmos via http", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); expect(connection).toBeTruthy(); connection.disconnect(); }); @@ -72,7 +72,7 @@ describe("CosmosConnection", () => { describe("chainId", () => { it("displays the chain ID", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const chainId = connection.chainId(); expect(chainId).toEqual(defaultChainId); connection.disconnect(); @@ -82,7 +82,7 @@ describe("CosmosConnection", () => { describe("height", () => { it("displays the current height", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const height = await connection.height(); expect(height).toBeGreaterThan(0); connection.disconnect(); @@ -92,7 +92,7 @@ describe("CosmosConnection", () => { describe("getToken", () => { it("displays a given token", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const token = await connection.getToken("COSM" as TokenTicker); expect(token).toEqual({ fractionalDigits: 6, @@ -104,7 +104,7 @@ describe("CosmosConnection", () => { it("resolves to undefined if the token is not supported", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const token = await connection.getToken("whatever" as TokenTicker); expect(token).toBeUndefined(); connection.disconnect(); @@ -114,7 +114,7 @@ describe("CosmosConnection", () => { describe("getAllTokens", () => { it("resolves to a list of all supported tokens", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const tokens = await connection.getAllTokens(); // TODO: make this more flexible expect(tokens).toEqual([ @@ -136,7 +136,7 @@ describe("CosmosConnection", () => { describe("getAccount", () => { it("gets an empty account by address", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const account = await connection.getAccount({ address: defaultEmptyAddress }); expect(account).toBeUndefined(); connection.disconnect(); @@ -144,7 +144,7 @@ describe("CosmosConnection", () => { it("gets an account by address", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const account = await connection.getAccount({ address: defaultAddress }); if (account === undefined) { throw new Error("Expected account not to be undefined"); @@ -161,7 +161,7 @@ describe("CosmosConnection", () => { it("gets an account by pubkey", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const account = await connection.getAccount({ pubkey: defaultPubkey }); if (account === undefined) { throw new Error("Expected account not to be undefined"); @@ -180,11 +180,11 @@ describe("CosmosConnection", () => { describe("integration tests", () => { it("can post and get a transaction", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const profile = new UserProfile(); const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic)); const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath); - const faucetAddress = cosmosCodec.identityToAddress(faucet); + const faucetAddress = cosmWasmCodec.identityToAddress(faucet); const unsigned = await connection.withDefaultFee({ kind: "bcp/send", @@ -200,7 +200,7 @@ describe("CosmosConnection", () => { }); const nonce = await connection.getNonce({ address: faucetAddress }); // TODO: we need to use custom codecs everywhere - const codec = new CosmosCodec(defaultPrefix, defaultTokens); + const codec = new CosmWasmCodec(defaultPrefix, defaultTokens); const signed = await profile.signTransaction(faucet, unsigned, codec, nonce); const postableBytes = codec.bytesToPost(signed); const response = await connection.postTx(postableBytes); @@ -243,11 +243,11 @@ describe("CosmosConnection", () => { it("can post and search for a transaction", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); const profile = new UserProfile(); const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic)); const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath); - const faucetAddress = cosmosCodec.identityToAddress(faucet); + const faucetAddress = cosmWasmCodec.identityToAddress(faucet); const unsigned = await connection.withDefaultFee({ kind: "bcp/send", @@ -263,7 +263,7 @@ describe("CosmosConnection", () => { }); const nonce = await connection.getNonce({ address: faucetAddress }); // TODO: we need to use custom codecs everywhere - const codec = new CosmosCodec(defaultPrefix, defaultTokens); + const codec = new CosmWasmCodec(defaultPrefix, defaultTokens); const signed = await profile.signTransaction(faucet, unsigned, codec, nonce); const postableBytes = codec.bytesToPost(signed); const response = await connection.postTx(postableBytes); diff --git a/packages/bcp/src/cosmosconnection.ts b/packages/bcp/src/cosmwasmconnection.ts similarity index 98% rename from packages/bcp/src/cosmosconnection.ts rename to packages/bcp/src/cosmwasmconnection.ts index 3c10fa6fbb..c527721286 100644 --- a/packages/bcp/src/cosmosconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -68,16 +68,16 @@ function buildQueryString({ return components.filter(Boolean).join("&"); } -export class CosmosConnection implements BlockchainConnection { +export class CosmWasmConnection implements BlockchainConnection { // we must know prefix and tokens a priori to understand the chain public static async establish( url: string, prefix: CosmosBech32Prefix, tokenInfo: TokenInfos, - ): Promise { + ): Promise { const restClient = new RestClient(url); const chainData = await this.initialize(restClient); - return new CosmosConnection(restClient, chainData, prefix, tokenInfo); + return new CosmWasmConnection(restClient, chainData, prefix, tokenInfo); } private static async initialize(restClient: RestClient): Promise { diff --git a/packages/bcp/src/cosmwasmconnector.ts b/packages/bcp/src/cosmwasmconnector.ts new file mode 100644 index 0000000000..295aee0aa2 --- /dev/null +++ b/packages/bcp/src/cosmwasmconnector.ts @@ -0,0 +1,23 @@ +import { ChainConnector, ChainId } from "@iov/bcp"; + +import { CosmosBech32Prefix } from "./address"; +import { CosmWasmCodec } from "./cosmwasmcodec"; +import { CosmWasmConnection } from "./cosmwasmconnection"; +import { TokenInfo } from "./types"; + +/** + * A helper to connect to a cosmos-based chain at a given url + */ +export function createCosmWasmConnector( + url: string, + prefix: CosmosBech32Prefix, + tokenInfo: readonly TokenInfo[], + expectedChainId?: ChainId, +): ChainConnector { + const codec = new CosmWasmCodec(prefix, tokenInfo); + return { + establishConnection: async () => CosmWasmConnection.establish(url, prefix, tokenInfo), + codec: codec, + expectedChainId: expectedChainId, + }; +} diff --git a/packages/bcp/src/index.ts b/packages/bcp/src/index.ts index 4c607b18e0..81e10d6e4a 100644 --- a/packages/bcp/src/index.ts +++ b/packages/bcp/src/index.ts @@ -1,3 +1,4 @@ -export { cosmosCodec, CosmosCodec } from "./cosmoscodec"; -export { CosmosConnection } from "./cosmosconnection"; -export { createCosmosConnector } from "./cosmosconnector"; +export { CosmWasmCodec } from "./cosmwasmcodec"; +export { CosmWasmConnection } from "./cosmwasmconnection"; +export { createCosmWasmConnector } from "./cosmwasmconnector"; +export { TokenInfo } from "./types"; diff --git a/packages/bcp/types/cosmoscodec.d.ts b/packages/bcp/types/cosmwasmcodec.d.ts similarity index 82% rename from packages/bcp/types/cosmoscodec.d.ts rename to packages/bcp/types/cosmwasmcodec.d.ts index 64de7f385a..1373ef9927 100644 --- a/packages/bcp/types/cosmoscodec.d.ts +++ b/packages/bcp/types/cosmwasmcodec.d.ts @@ -12,7 +12,7 @@ import { } from "@iov/bcp"; import { CosmosBech32Prefix } from "./address"; import { TokenInfos } from "./types"; -export declare class CosmosCodec implements TxCodec { +export declare class CosmWasmCodec implements TxCodec { private readonly prefix; private readonly tokens; constructor(prefix: CosmosBech32Prefix, tokens: TokenInfos); @@ -23,4 +23,5 @@ export declare class CosmosCodec implements TxCodec { identityToAddress(identity: Identity): Address; isValidAddress(address: string): boolean; } -export declare const cosmosCodec: CosmosCodec; +/** Unconfigured codec is useful for testing only */ +export declare const cosmWasmCodec: CosmWasmCodec; diff --git a/packages/bcp/types/cosmosconnection.d.ts b/packages/bcp/types/cosmwasmconnection.d.ts similarity index 90% rename from packages/bcp/types/cosmosconnection.d.ts rename to packages/bcp/types/cosmwasmconnection.d.ts index de0cf74ad7..5144df6719 100644 --- a/packages/bcp/types/cosmosconnection.d.ts +++ b/packages/bcp/types/cosmwasmconnection.d.ts @@ -22,8 +22,12 @@ import { import { Stream } from "xstream"; import { CosmosBech32Prefix } from "./address"; import { TokenInfos } from "./types"; -export declare class CosmosConnection implements BlockchainConnection { - static establish(url: string, prefix: CosmosBech32Prefix, tokenInfo: TokenInfos): Promise; +export declare class CosmWasmConnection implements BlockchainConnection { + static establish( + url: string, + prefix: CosmosBech32Prefix, + tokenInfo: TokenInfos, + ): Promise; private static initialize; private readonly restClient; private readonly chainData; diff --git a/packages/bcp/types/cosmosconnector.d.ts b/packages/bcp/types/cosmwasmconnector.d.ts similarity index 52% rename from packages/bcp/types/cosmosconnector.d.ts rename to packages/bcp/types/cosmwasmconnector.d.ts index 05a3d52830..2a2ba3e702 100644 --- a/packages/bcp/types/cosmosconnector.d.ts +++ b/packages/bcp/types/cosmwasmconnector.d.ts @@ -1,13 +1,13 @@ import { ChainConnector, ChainId } from "@iov/bcp"; import { CosmosBech32Prefix } from "./address"; -import { CosmosConnection } from "./cosmosconnection"; -import { TokenInfos } from "./types"; +import { CosmWasmConnection } from "./cosmwasmconnection"; +import { TokenInfo } from "./types"; /** * A helper to connect to a cosmos-based chain at a given url */ -export declare function createCosmosConnector( +export declare function createCosmWasmConnector( url: string, prefix: CosmosBech32Prefix, - tokenInfo: TokenInfos, + tokenInfo: readonly TokenInfo[], expectedChainId?: ChainId, -): ChainConnector; +): ChainConnector; diff --git a/packages/bcp/types/index.d.ts b/packages/bcp/types/index.d.ts index 4c607b18e0..81e10d6e4a 100644 --- a/packages/bcp/types/index.d.ts +++ b/packages/bcp/types/index.d.ts @@ -1,3 +1,4 @@ -export { cosmosCodec, CosmosCodec } from "./cosmoscodec"; -export { CosmosConnection } from "./cosmosconnection"; -export { createCosmosConnector } from "./cosmosconnector"; +export { CosmWasmCodec } from "./cosmwasmcodec"; +export { CosmWasmConnection } from "./cosmwasmconnection"; +export { createCosmWasmConnector } from "./cosmwasmconnector"; +export { TokenInfo } from "./types"; diff --git a/packages/faucet/.eslintignore b/packages/faucet/.eslintignore new file mode 120000 index 0000000000..86039baf54 --- /dev/null +++ b/packages/faucet/.eslintignore @@ -0,0 +1 @@ +../../.eslintignore \ No newline at end of file diff --git a/packages/faucet/.gitignore b/packages/faucet/.gitignore new file mode 100644 index 0000000000..68bf373524 --- /dev/null +++ b/packages/faucet/.gitignore @@ -0,0 +1,3 @@ +build/ +dist/ +docs/ diff --git a/packages/faucet/README.md b/packages/faucet/README.md new file mode 100644 index 0000000000..63ba5f9826 --- /dev/null +++ b/packages/faucet/README.md @@ -0,0 +1,126 @@ +# @cosmwasm/faucet + +The faucet is built as part of the monorepo. In the repo root do: + +``` +yarn install +yarn build +``` + +Then start it for a Wasmd development blockchain using: + +``` +cd packages/faucet +yarn dev-start +``` + +Advanced users that want to provide their custom config can start as follows: + +``` +FAUCET_CREDIT_AMOUNT_COSM=10 \ + FAUCET_CREDIT_AMOUNT_STAKE=5 \ + FAUCET_CONCURRENCY=3 \ + FAUCET_MNEMONIC="economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone" \ + ./bin/cosm-faucet start cosmwasm "http://localhost:1317" +``` + +## Usage + +``` +sage: cosmwasm-faucet action [arguments...] + +Positional arguments per action are listed below. Arguments in parentheses are optional. + +help Shows a help text and exits + +version Prints the version and exits + +generate Generates a random mnemonic, shows derived faucet addresses and exits + 1 Codec + 2 Chain ID + +start Starts the faucet + 1 Codec + 2 Node base URL, e.g. wss://bov.friendnet-fast.iov.one + +Environment variables + +FAUCET_CONCURRENCY Number of distributor accounts. Defaults to 5. +FAUCET_PORT Port of the webserver. Defaults to 8000. +FAUCET_MNEMONIC Secret mnemonic that serves as the base secret for the + faucet HD accounts +FAUCET_CREDIT_AMOUNT_TKN Send this amount of TKN to a user requesting TKN. TKN is + a placeholder for the token ticker. Defaults to 10. +FAUCET_REFILL_FACTOR Send factor times credit amount on refilling. Defauls to 8. +FAUCET_REFILL_THRESHOLD Refill when balance gets below factor times credit amount. + Defaults to 20. +``` + +### Faucet HD wallet + +The faucet uses standard HD paths for each blockchain, e.g. + +``` +IOV m/44'/234'/a' +Lisk m/44'/134'/a' +CosmWasm m/44'/118'/0'/0/a +``` + +where `a` is a 0-based index of the account. Account 0 is the token holder and +account 1...FAUCET_CONCURRENCY are the distributor accounts. + +This means the token holder account can be accessed using the Neuma wallet when +the same mnemonic is used. Accessing the distributor accounts will be possible +as soon as there is +[multi account support](https://github.com/iov-one/ponferrada/milestone/3). + +### Working with docker + +- Build an artifact () + +```sh +docker build -t cosmwasm/faucet:manual --file faucet.Dockerfile . +``` + +- Version and help + +```sh +docker run --read-only --rm cosmwasm/faucet:manual version +docker run --read-only --rm cosmwasm/faucet:manual help +``` + +- Run faucet locally + +```sh +DOCKER_HOST_IP=$(docker run --read-only --rm alpine ip route | awk 'NR==1 {print $3}') \ + FAUCET_CONCURRENCY=3 FAUCET_MNEMONIC="economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone" \ + docker run --read-only --rm \ + -e FAUCET_MNEMONIC \ + -e FAUCET_CONCURRENCY \ + -p 8000:8000 \ + cosmwasm/faucet:manual \ + start cosmwasm "http://$DOCKER_HOST_IP:1317" +``` + +### Using the faucet + +Now that the faucet has been started up, you can send credit requests to it. +This can be done with a simple http POST request. These commands assume the +faucet is running locally, be sure to change it from `localhost` if your +situation is different. + +``` +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"ticker":"CASH","address":"tiov1k898u78hgs36uqw68dg7va5nfkgstu5z0fhz3f"}' \ + http://localhost:8000/credit +``` + +### Checking the faucets status + +The faucet provides a simple status check in the form of an http GET request. As +above, make sure to adjust the URL as necessary. + +``` +curl http://localhost:8000/status +``` diff --git a/packages/faucet/bin/cosm-faucet b/packages/faucet/bin/cosm-faucet new file mode 100755 index 0000000000..c44ac6401d --- /dev/null +++ b/packages/faucet/bin/cosm-faucet @@ -0,0 +1,6 @@ +#!/usr/bin/env node +const path = require("path"); + +// attempt to call in main file.... +const faucet = require(path.join(__dirname, "..", "build", "faucet.js")); +faucet.main(process.argv.slice(2)); diff --git a/packages/faucet/jasmine-spec-reporter.config.json b/packages/faucet/jasmine-spec-reporter.config.json new file mode 100644 index 0000000000..3a99755608 --- /dev/null +++ b/packages/faucet/jasmine-spec-reporter.config.json @@ -0,0 +1,12 @@ +{ + "suite": { + "displayNumber": true + }, + "spec": { + "displayDuration": true + }, + "summary": { + "displayPending": false, + "displayStacktrace": true + } +} diff --git a/packages/faucet/jasmine-testrunner.js b/packages/faucet/jasmine-testrunner.js new file mode 100755 index 0000000000..55b2df2912 --- /dev/null +++ b/packages/faucet/jasmine-testrunner.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +require("source-map-support").install(); +const defaultSpecReporterConfig = require("./jasmine-spec-reporter.config.json"); + +// setup Jasmine +const Jasmine = require("jasmine"); +const jasmine = new Jasmine(); +jasmine.loadConfig({ + spec_dir: "build", + spec_files: ["**/*.spec.js"], + helpers: [], + random: false, + seed: null, + stopSpecOnExpectationFailure: false, +}); +jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 1000; + +// setup reporter +const { SpecReporter } = require("jasmine-spec-reporter"); +const reporter = new SpecReporter({ ...defaultSpecReporterConfig }); + +// initialize and execute +jasmine.env.clearReporters(); +jasmine.addReporter(reporter); +jasmine.execute(); diff --git a/packages/faucet/nonces/README.txt b/packages/faucet/nonces/README.txt new file mode 100644 index 0000000000..092fe732f1 --- /dev/null +++ b/packages/faucet/nonces/README.txt @@ -0,0 +1 @@ +Directory used to trigger lerna package updates for all packages diff --git a/packages/faucet/package.json b/packages/faucet/package.json new file mode 100644 index 0000000000..4c234aee21 --- /dev/null +++ b/packages/faucet/package.json @@ -0,0 +1,56 @@ +{ + "name": "@cosmwasm/faucet", + "version": "0.0.1", + "description": "The faucet", + "author": "Ethan Frey ", + "license": "Apache-2.0", + "main": "build/index.js", + "types": "types/index.d.ts", + "files": [ + "build/", + "types/", + "*.md", + "!*.spec.*", + "!**/testdata/" + ], + "repository": { + "type": "git", + "url": "https://github.com/confio/cosm-js/tree/master/packages/faucet" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev-start": "FAUCET_CREDIT_AMOUNT_COSM=10 FAUCET_CREDIT_AMOUNT_STAKE=5 FAUCET_CONCURRENCY=3 FAUCET_MNEMONIC=\"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone\" ./bin/cosm-faucet start cosmwasm \"http://localhost:1317\"", + "docs": "shx rm -rf docs && typedoc --options typedoc.js", + "format": "prettier --write --loglevel warn \"./src/**/*.ts\"", + "lint": "eslint --max-warnings 0 \"**/*.{js,ts}\" && tslint -t verbose --project .", + "lint-fix": "eslint --max-warnings 0 \"**/*.{js,ts}\" --fix", + "build": "shx rm -rf ./build && tsc", + "build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build", + "test-node": "node jasmine-testrunner.js", + "test": "yarn build-or-skip && yarn test-node" + }, + "dependencies": { + "@cosmwasm/bcp": "^0.0.1", + "@iov/bcp": "^2.0.0-alpha.7", + "@iov/crypto": "^2.0.0-alpha.7", + "@iov/encoding": "^2.0.0-alpha.7", + "@iov/lisk": "^2.0.0-alpha.7", + "@iov/multichain": "^2.0.0-alpha.7", + "@koa/cors": "^3.0.0", + "axios": "^0.19.0", + "bn.js": "^5.1.1", + "fast-deep-equal": "^3.1.1", + "koa": "^2.11.0", + "koa-bodyparser": "^4.2.1", + "readonly-date": "^1.0.0", + "xstream": "^11.11.0" + }, + "devDependencies": { + "@types/bn.js": "^4.11.6", + "@types/koa": "^2.11.0", + "@types/koa-bodyparser": "^4.3.0", + "@types/koa__cors": "^3.0.1" + } +} diff --git a/packages/faucet/src/actions/generate.ts b/packages/faucet/src/actions/generate.ts new file mode 100644 index 0000000000..3ca673d265 --- /dev/null +++ b/packages/faucet/src/actions/generate.ts @@ -0,0 +1,20 @@ +import { ChainId } from "@iov/bcp"; +import { Bip39, Random } from "@iov/crypto"; +import { UserProfile } from "@iov/keycontrol"; + +import { codecFromString } from "../codec"; +import { setSecretAndCreateIdentities } from "../profile"; + +export async function generate(args: ReadonlyArray): Promise { + if (args.length < 2) { + throw Error(`Not enough arguments for action 'generate'. See 'iov-faucet help' or README for arguments.`); + } + const codecName = codecFromString(args[0]); + const chainId = args[1] as ChainId; + + const mnemonic = Bip39.encode(await Random.getBytes(16)).toString(); + console.info(`FAUCET_MNEMONIC="${mnemonic}"`); + + const profile = new UserProfile(); + await setSecretAndCreateIdentities(profile, mnemonic, chainId, codecName); +} diff --git a/packages/faucet/src/actions/help.ts b/packages/faucet/src/actions/help.ts new file mode 100644 index 0000000000..039655cc15 --- /dev/null +++ b/packages/faucet/src/actions/help.ts @@ -0,0 +1,35 @@ +const binaryName = "cosmwasm-faucet"; + +export function help(): void { + const out = ` +Usage: ${binaryName} action [arguments...] + +Positional arguments per action are listed below. Arguments in parentheses are optional. + +help Shows a help text and exits + +version Prints the version and exits + +generate Generates a random mnemonic, shows derived faucet addresses and exits + 1 Codec + 2 Chain ID + +start Starts the faucet + 1 Codec + 2 Node base URL, e.g. wss://bov.friendnet-fast.iov.one + +Environment variables + +FAUCET_CONCURRENCY Number of distributor accounts. Defaults to 5. +FAUCET_PORT Port of the webserver. Defaults to 8000. +FAUCET_MNEMONIC Secret mnemonic that serves as the base secret for the + faucet HD accounts +FAUCET_CREDIT_AMOUNT_TKN Send this amount of TKN to a user requesting TKN. TKN is + a placeholder for the token ticker. Defaults to 10. +FAUCET_REFILL_FACTOR Send factor times credit amount on refilling. Defauls to 8. +FAUCET_REFILL_THRESHOLD Refill when balance gets below factor times credit amount. + Defaults to 20. +`.trim(); + + process.stdout.write(`${out}\n`); +} diff --git a/packages/faucet/src/actions/index.ts b/packages/faucet/src/actions/index.ts new file mode 100644 index 0000000000..036bb34846 --- /dev/null +++ b/packages/faucet/src/actions/index.ts @@ -0,0 +1,4 @@ +export { generate } from "./generate"; +export { help } from "./help"; +export { start } from "./start"; +export { version } from "./version"; diff --git a/packages/faucet/src/actions/start/httperror.spec.ts b/packages/faucet/src/actions/start/httperror.spec.ts new file mode 100644 index 0000000000..fb3e61c6d8 --- /dev/null +++ b/packages/faucet/src/actions/start/httperror.spec.ts @@ -0,0 +1,18 @@ +import { HttpError } from "./httperror"; + +describe("HttpError", () => { + it("can be constructed", () => { + { + const error = new HttpError(400, "Invalid name field"); + expect(error.message).toEqual("Invalid name field"); + expect(error.status).toEqual(400); + expect(error.expose).toEqual(true); + } + { + const error = new HttpError(500, "Out of memory", false); + expect(error.message).toEqual("Out of memory"); + expect(error.status).toEqual(500); + expect(error.expose).toEqual(false); + } + }); +}); diff --git a/packages/faucet/src/actions/start/httperror.ts b/packages/faucet/src/actions/start/httperror.ts new file mode 100644 index 0000000000..4bf0223720 --- /dev/null +++ b/packages/faucet/src/actions/start/httperror.ts @@ -0,0 +1,5 @@ +export class HttpError extends Error { + constructor(public readonly status: number, text: string, public readonly expose: boolean = true) { + super(text); + } +} diff --git a/packages/faucet/src/actions/start/index.ts b/packages/faucet/src/actions/start/index.ts new file mode 100644 index 0000000000..56e0e1212c --- /dev/null +++ b/packages/faucet/src/actions/start/index.ts @@ -0,0 +1 @@ +export { start } from "./start"; diff --git a/packages/faucet/src/actions/start/requestparser.spec.ts b/packages/faucet/src/actions/start/requestparser.spec.ts new file mode 100644 index 0000000000..84be6d2cb0 --- /dev/null +++ b/packages/faucet/src/actions/start/requestparser.spec.ts @@ -0,0 +1,46 @@ +import { RequestParser } from "./requestparser"; + +describe("RequestParser", () => { + it("can process valid credit request", () => { + const body = { address: "abc", ticker: "CASH" }; + expect(RequestParser.parseCreditBody(body)).toEqual({ address: "abc", ticker: "CASH" }); + }); + + it("throws for invalid credit requests", () => { + // address unset + { + const body = { ticker: "CASH" }; + expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'address' must be a string/i); + } + + // address wrong type + { + const body = { address: true, ticker: "CASH" }; + expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'address' must be a string/i); + } + + // address empty + { + const body = { address: "", ticker: "CASH" }; + expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'address' must not be empty/i); + } + + // ticker unset + { + const body = { address: "abc" }; + expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'ticker' must be a string/i); + } + + // ticker wrong type + { + const body = { address: "abc", ticker: true }; + expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'ticker' must be a string/i); + } + + // ticker empty + { + const body = { address: "abc", ticker: "" }; + expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'ticker' must not be empty/i); + } + }); +}); diff --git a/packages/faucet/src/actions/start/requestparser.ts b/packages/faucet/src/actions/start/requestparser.ts new file mode 100644 index 0000000000..2c9cfb4acc --- /dev/null +++ b/packages/faucet/src/actions/start/requestparser.ts @@ -0,0 +1,35 @@ +import { Address, TokenTicker } from "@iov/bcp"; + +import { HttpError } from "./httperror"; + +export interface CreditRequestBodyData { + readonly ticker: TokenTicker; + readonly address: Address; +} + +export class RequestParser { + public static parseCreditBody(body: any): CreditRequestBodyData { + const { address, ticker } = body; + + if (typeof address !== "string") { + throw new HttpError(400, "Property 'address' must be a string."); + } + + if (address.length === 0) { + throw new HttpError(400, "Property 'address' must not be empty."); + } + + if (typeof ticker !== "string") { + throw new HttpError(400, "Property 'ticker' must be a string"); + } + + if (ticker.length === 0) { + throw new HttpError(400, "Property 'ticker' must not be empty."); + } + + return { + address: address as Address, + ticker: ticker as TokenTicker, + }; + } +} diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts new file mode 100644 index 0000000000..beb9ea1d3c --- /dev/null +++ b/packages/faucet/src/actions/start/start.ts @@ -0,0 +1,155 @@ +// tslint:disable: no-object-mutation +import { UserProfile } from "@iov/keycontrol"; +import { MultiChainSigner } from "@iov/multichain"; +import cors = require("@koa/cors"); +import Koa from "koa"; +import bodyParser from "koa-bodyparser"; + +import { creditAmount, setFractionalDigits } from "../../cashflow"; +import { + codecDefaultFractionalDigits, + codecFromString, + codecImplementation, + createChainConnector, +} from "../../codec"; +import * as constants from "../../constants"; +import { logAccountsState, logSendJob } from "../../debugging"; +import { + accountsOfFirstChain, + availableTokensFromHolder, + identitiesOfFirstWallet, + refillFirstChain, + sendOnFirstChain, + tokenTickersOfFirstChain, +} from "../../multichainhelpers"; +import { setSecretAndCreateIdentities } from "../../profile"; +import { SendJob } from "../../types"; +import { HttpError } from "./httperror"; +import { RequestParser } from "./requestparser"; + +let count = 0; + +/** returns an integer >= 0 that increments and is unique in module scope */ +function getCount(): number { + return count++; +} + +export async function start(args: ReadonlyArray): Promise { + if (args.length < 2) { + throw Error(`Not enough arguments for action 'start'. See 'iov-faucet help' or README for arguments.`); + } + const codec = codecFromString(args[0]); + const blockchainBaseUrl: string = args[1]; + + const port = constants.port; + + const profile = new UserProfile(); + if (!constants.mnemonic) { + throw new Error("The FAUCET_MNEMONIC environment variable is not set"); + } + const signer = new MultiChainSigner(profile); + console.info(`Connecting to blockchain ${blockchainBaseUrl} ...`); + const connection = (await signer.addChain(createChainConnector(codec, blockchainBaseUrl))).connection; + + const connectedChainId = connection.chainId(); + console.info(`Connected to network: ${connectedChainId}`); + + setFractionalDigits(codecDefaultFractionalDigits(codec)); + await setSecretAndCreateIdentities(profile, constants.mnemonic, connectedChainId, codec); + + const chainTokens = await tokenTickersOfFirstChain(signer); + console.info("Chain tokens:", chainTokens); + + const accounts = await accountsOfFirstChain(profile, signer); + logAccountsState(accounts); + + let availableTokens = availableTokensFromHolder(accounts[0]); + console.info("Available tokens:", availableTokens); + setInterval(async () => { + const updatedAccounts = await accountsOfFirstChain(profile, signer); + availableTokens = availableTokensFromHolder(updatedAccounts[0]); + console.info("Available tokens:", availableTokens); + }, 60_000); + + const distibutorIdentities = identitiesOfFirstWallet(profile).slice(1); + + await refillFirstChain(profile, signer); + setInterval(async () => refillFirstChain(profile, signer), 60_000); // ever 60 seconds + + console.info("Creating webserver ..."); + const api = new Koa(); + api.use(cors()); + api.use(bodyParser()); + + api.use(async context => { + switch (context.path) { + case "/": + case "/healthz": + context.response.body = + "Welcome to the faucet!\n" + + "\n" + + "Check the full status via the /status endpoint.\n" + + "You can get tokens from here by POSTing to /credit.\n" + + "See https://github.com/iov-one/iov-faucet for all further information.\n"; + break; + case "/status": { + const updatedAccounts = await accountsOfFirstChain(profile, signer); + context.response.body = { + status: "ok", + nodeUrl: blockchainBaseUrl, + chainId: connectedChainId, + chainTokens: chainTokens, + availableTokens: availableTokens, + holder: updatedAccounts[0], + distributors: updatedAccounts.slice(1), + }; + break; + } + case "/credit": { + if (context.request.method !== "POST") { + throw new HttpError(405, "This endpoint requires a POST request"); + } + + if (context.request.type !== "application/json") { + throw new HttpError(415, "Content-type application/json expected"); + } + + // context.request.body is set by the bodyParser() plugin + const requestBody = (context.request as any).body; + const { address, ticker } = RequestParser.parseCreditBody(requestBody); + + if (!codecImplementation(codec).isValidAddress(address)) { + throw new HttpError(400, "Address is not in the expected format for this chain."); + } + + if (availableTokens.indexOf(ticker) === -1) { + const tokens = JSON.stringify(availableTokens); + throw new HttpError(422, `Token is not available. Available tokens are: ${tokens}`); + } + + const sender = distibutorIdentities[getCount() % distibutorIdentities.length]; + + try { + const job: SendJob = { + sender: sender, + recipient: address, + amount: creditAmount(ticker), + tokenTicker: ticker, + }; + logSendJob(signer, job); + await sendOnFirstChain(profile, signer, job); + } catch (e) { + console.error(e); + throw new HttpError(500, "Sending tokens failed"); + } + + context.response.body = "ok"; + break; + } + default: + // koa sends 404 by default + } + }); + console.info(`Starting webserver on port ${port} ...`); + api.listen(port); +} diff --git a/packages/faucet/src/actions/version.ts b/packages/faucet/src/actions/version.ts new file mode 100644 index 0000000000..2863d78654 --- /dev/null +++ b/packages/faucet/src/actions/version.ts @@ -0,0 +1,15 @@ +import fs from "fs"; + +export async function version(): Promise { + return new Promise((resolve, reject) => { + fs.readFile(__dirname + "/../../package.json", { encoding: "utf8" }, (error, data) => { + if (error) { + reject(error); + } else { + const packagejson = JSON.parse(data); + process.stdout.write(`${packagejson.version}\n`); + resolve(); + } + }); + }); +} diff --git a/packages/faucet/src/cashflow.spec.ts b/packages/faucet/src/cashflow.spec.ts new file mode 100644 index 0000000000..6cd56c281e --- /dev/null +++ b/packages/faucet/src/cashflow.spec.ts @@ -0,0 +1,125 @@ +// tslint:disable: no-object-mutation +import { TokenTicker } from "@iov/bcp"; + +import { creditAmount, refillAmount, refillThreshold, setFractionalDigits } from "./cashflow"; + +describe("Cashflow", () => { + beforeAll(() => { + setFractionalDigits(3); + }); + + describe("creditAmount", () => { + it("returns '10' + '000' by default", () => { + expect(creditAmount("TOKENZ" as TokenTicker)).toEqual({ + quantity: "10000", + fractionalDigits: 3, + tokenTicker: "TOKENZ", + }); + expect(creditAmount("TRASH" as TokenTicker)).toEqual({ + quantity: "10000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + + it("returns value from env variable + '000' when set", () => { + process.env.FAUCET_CREDIT_AMOUNT_WTF = "22"; + expect(creditAmount("WTF" as TokenTicker)).toEqual({ + quantity: "22000", + fractionalDigits: 3, + tokenTicker: "WTF", + }); + }); + + it("returns default from env variable + '000' when set to empty", () => { + process.env.FAUCET_CREDIT_AMOUNT_WTF = ""; + expect(creditAmount("WTF" as TokenTicker)).toEqual({ + quantity: "10000", + fractionalDigits: 3, + tokenTicker: "WTF", + }); + }); + }); + + describe("refillAmount", () => { + beforeEach(() => { + process.env.FAUCET_REFILL_FACTOR = ""; + }); + it("returns 20*10 + '000' by default", () => { + expect(refillAmount("TOKENZ" as TokenTicker)).toEqual({ + quantity: "200000", + fractionalDigits: 3, + tokenTicker: "TOKENZ", + }); + }); + + it("returns 20*22 + '000' when credit amount is 22", () => { + process.env.FAUCET_CREDIT_AMOUNT_WTF = "22"; + expect(refillAmount("WTF" as TokenTicker)).toEqual({ + quantity: "440000", + fractionalDigits: 3, + tokenTicker: "WTF", + }); + }); + + it("returns 30*10 + '000' when refill factor is 30", () => { + process.env.FAUCET_REFILL_FACTOR = "30"; + expect(refillAmount("TOKENZ" as TokenTicker)).toEqual({ + quantity: "300000", + fractionalDigits: 3, + tokenTicker: "TOKENZ", + }); + }); + + it("returns 30*22 + '000' when refill factor is 30 and credit amount is 22", () => { + process.env.FAUCET_REFILL_FACTOR = "30"; + process.env.FAUCET_CREDIT_AMOUNT_WTF = "22"; + expect(refillAmount("WTF" as TokenTicker)).toEqual({ + quantity: "660000", + fractionalDigits: 3, + tokenTicker: "WTF", + }); + }); + }); + + describe("refillThreshold", () => { + beforeEach(() => { + process.env.FAUCET_REFILL_THRESHOLD = ""; + }); + it("returns 8*10 + '000' by default", () => { + expect(refillThreshold("TOKENZ" as TokenTicker)).toEqual({ + quantity: "80000", + fractionalDigits: 3, + tokenTicker: "TOKENZ", + }); + }); + + it("returns 8*22 + '000' when credit amount is 22", () => { + process.env.FAUCET_CREDIT_AMOUNT_WTF = "22"; + expect(refillThreshold("WTF" as TokenTicker)).toEqual({ + quantity: "176000", + fractionalDigits: 3, + tokenTicker: "WTF", + }); + }); + + it("returns 5*10 + '000' when refill threshold is 5", () => { + process.env.FAUCET_REFILL_THRESHOLD = "5"; + expect(refillThreshold("TOKENZ" as TokenTicker)).toEqual({ + quantity: "50000", + fractionalDigits: 3, + tokenTicker: "TOKENZ", + }); + }); + + it("returns 5*22 + '000' when refill threshold is 5 and credit amount is 22", () => { + process.env.FAUCET_REFILL_THRESHOLD = "5"; + process.env.FAUCET_CREDIT_AMOUNT_WTF = "22"; + expect(refillThreshold("WTF" as TokenTicker)).toEqual({ + quantity: "110000", + fractionalDigits: 3, + tokenTicker: "WTF", + }); + }); + }); +}); diff --git a/packages/faucet/src/cashflow.ts b/packages/faucet/src/cashflow.ts new file mode 100644 index 0000000000..987b34e1d8 --- /dev/null +++ b/packages/faucet/src/cashflow.ts @@ -0,0 +1,60 @@ +import BN = require("bn.js"); + +import { Account, Amount, TokenTicker } from "@iov/bcp"; +import { Int53 } from "@iov/encoding"; + +/** Send `factor` times credit amount on refilling */ +const defaultRefillFactor = 20; + +/** refill when balance gets below `factor` times credit amount */ +const defaultRefillThresholdFactor = 8; + +// Load this from connection? +let globalFractionalDigits: number | undefined; + +export function setFractionalDigits(input: number): void { + globalFractionalDigits = input; +} + +export function getFractionalDigits(): number { + if (globalFractionalDigits === undefined) { + throw new Error("Fractional digits not set"); + } + return globalFractionalDigits; +} + +/** The amount of tokens that will be sent to the user */ +export function creditAmount(token: TokenTicker, factor = 1): Amount { + const amountFromEnv = process.env[`FAUCET_CREDIT_AMOUNT_${token}`]; + const wholeNumber = amountFromEnv ? Int53.fromString(amountFromEnv).toNumber() : 10; + const total = wholeNumber * factor; + const fractionalDigits = getFractionalDigits(); + // replace BN with BigInt with TypeScript 3.2 and node 11 + const quantity = new BN(total).imul(new BN(10).pow(new BN(fractionalDigits))).toString(); + return { + quantity: quantity, + fractionalDigits: fractionalDigits, + tokenTicker: token, + }; +} + +export function refillAmount(token: TokenTicker): Amount { + const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_FACTOR || "0", 10) || undefined; + const factor = factorFromEnv || defaultRefillFactor; + return creditAmount(token, factor); +} + +export function refillThreshold(token: TokenTicker): Amount { + const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_THRESHOLD || "0", 10) || undefined; + const factor = factorFromEnv || defaultRefillThresholdFactor; + return creditAmount(token, factor); +} + +/** true iff the distributor account needs a refill */ +export function needsRefill(account: Account, token: TokenTicker): boolean { + const coin = account.balance.find(balance => balance.tokenTicker === token); + + const tokenBalance = coin ? coin.quantity : "0"; + const refillQty = new BN(refillThreshold(token).quantity); + return new BN(tokenBalance).lt(refillQty); +} diff --git a/packages/faucet/src/codec.spec.ts b/packages/faucet/src/codec.spec.ts new file mode 100644 index 0000000000..2176ec6375 --- /dev/null +++ b/packages/faucet/src/codec.spec.ts @@ -0,0 +1,14 @@ +import { Codec, codecFromString } from "./codec"; + +describe("Codec", () => { + it("can convert string to codec", () => { + expect(codecFromString("lisk")).toEqual(Codec.Lisk); + expect(codecFromString("cosmwasm")).toEqual(Codec.CosmWasm); + + expect(() => codecFromString("")).toThrowError(/not supported/i); + expect(() => codecFromString("bns")).toThrowError(/not supported/i); + expect(() => codecFromString("abc")).toThrowError(/not supported/i); + expect(() => codecFromString("LISK")).toThrowError(/not supported/i); + expect(() => codecFromString("CosmWasm")).toThrowError(/not supported/i); + }); +}); diff --git a/packages/faucet/src/codec.ts b/packages/faucet/src/codec.ts new file mode 100644 index 0000000000..dab2a0929c --- /dev/null +++ b/packages/faucet/src/codec.ts @@ -0,0 +1,76 @@ +import { createCosmWasmConnector, TokenInfo } from "@cosmwasm/bcp"; +import { ChainConnector, TokenTicker, TxCodec } from "@iov/bcp"; +import { Slip10RawIndex } from "@iov/crypto"; +import { HdPaths } from "@iov/keycontrol"; +import { createLiskConnector } from "@iov/lisk"; + +export const enum Codec { + Lisk, + CosmWasm, +} + +export function codecFromString(input: string): Codec { + switch (input) { + case "lisk": + return Codec.Lisk; + case "cosmwasm": + return Codec.CosmWasm; + default: + throw new Error(`Codec '${input}' not supported`); + } +} + +export function createPathBuilderForCodec(codec: Codec): (derivation: number) => readonly Slip10RawIndex[] { + const pathBuilder = (accountIndex: number): readonly Slip10RawIndex[] => { + switch (codec) { + case Codec.Lisk: + return HdPaths.bip44Like(134, accountIndex); + case Codec.CosmWasm: + return HdPaths.cosmos(accountIndex); + default: + throw new Error("No path builder for this codec found"); + } + }; + return pathBuilder; +} + +export function createChainConnector(codec: Codec, url: string): ChainConnector { + switch (codec) { + case Codec.Lisk: + return createLiskConnector(url); + case Codec.CosmWasm: { + const tokens: readonly TokenInfo[] = [ + { + fractionalDigits: 6, + tokenName: "Fee Token", + tokenTicker: "COSM" as TokenTicker, + denom: "cosm", + }, + { + fractionalDigits: 6, + tokenName: "Staking Token", + tokenTicker: "STAKE" as TokenTicker, + denom: "stake", + }, + ]; + return createCosmWasmConnector(url, "cosmos", tokens); + } + default: + throw new Error("No connector for this codec found"); + } +} + +export function codecImplementation(codec: Codec): TxCodec { + return createChainConnector(codec, "unused dummy url").codec; +} + +export function codecDefaultFractionalDigits(codec: Codec): number { + switch (codec) { + case Codec.Lisk: + return 8; + case Codec.CosmWasm: + return 6; + default: + throw new Error("Unknown codec"); + } +} diff --git a/packages/faucet/src/constants.ts b/packages/faucet/src/constants.ts new file mode 100644 index 0000000000..a28481d1a9 --- /dev/null +++ b/packages/faucet/src/constants.ts @@ -0,0 +1,3 @@ +export const concurrency: number = Number.parseInt(process.env.FAUCET_CONCURRENCY || "", 10) || 5; +export const port: number = Number.parseInt(process.env.FAUCET_PORT || "", 10) || 8000; +export const mnemonic: string | undefined = process.env.FAUCET_MNEMONIC; diff --git a/packages/faucet/src/crypto.ts b/packages/faucet/src/crypto.ts new file mode 100644 index 0000000000..cfa370e423 --- /dev/null +++ b/packages/faucet/src/crypto.ts @@ -0,0 +1,14 @@ +import { Ed25519HdWallet, Secp256k1HdWallet, Wallet } from "@iov/keycontrol"; + +import { Codec } from "./codec"; + +export function createWalletForCodec(input: Codec, mnemonic: string): Wallet { + switch (input) { + case Codec.Lisk: + return Ed25519HdWallet.fromMnemonic(mnemonic); + case Codec.CosmWasm: + return Secp256k1HdWallet.fromMnemonic(mnemonic); + default: + throw new Error(`Codec '${input}' not supported`); + } +} diff --git a/packages/faucet/src/debugging.ts b/packages/faucet/src/debugging.ts new file mode 100644 index 0000000000..5b5adc09ab --- /dev/null +++ b/packages/faucet/src/debugging.ts @@ -0,0 +1,58 @@ +import { Account, Amount } from "@iov/bcp"; +import { MultiChainSigner } from "@iov/multichain"; + +import { SendJob } from "./types"; + +export function amountToNumber(amount: Amount): number { + const { quantity, fractionalDigits } = amount; + if (!quantity.match(/^[0-9]+$/)) { + throw new Error(`quantity must be a number, got ${quantity}`); + } + if (fractionalDigits < 0) { + throw new Error(`invalid fractional digits: ${fractionalDigits}`); + } + // let's remove those leading zeros... + const temp = quantity.replace(/^0+/, ""); + // unless we need them to reach a decimal point + const pad = fractionalDigits - temp.length; + const trimmed = pad > 0 ? "0".repeat(pad) + temp : temp; + + const cut = trimmed.length - fractionalDigits; + const whole = cut === 0 ? "0" : trimmed.slice(0, cut); + const decimal = fractionalDigits === 0 ? "" : `.${trimmed.slice(cut)}`; + const value = `${whole}${decimal}`; + + return Number(value); +} + +/** A string representation of a coin in a human-readable format that can change at any time */ +export function debugCoin(coin: Amount): string { + return `${amountToNumber(coin)} ${coin.tokenTicker}`; +} + +/** A string representation of a balance in a human-readable format that can change at any time */ +export function debugBalance(data: ReadonlyArray): string { + return `[${data.map(debugCoin).join(", ")}]`; +} + +/** A string representation of an account in a human-readable format that can change at any time */ +export function debugAccount(account: Account): string { + return `${account.address}: ${debugBalance(account.balance)}`; +} + +export function logAccountsState(accounts: ReadonlyArray): void { + if (accounts.length < 2) { + throw new Error("List of accounts must contain at least one token holder and one distributor"); + } + const holder = accounts[0]; + const distributors = accounts.slice(1); + console.info("Holder:\n" + ` ${debugAccount(holder)}`); + console.info("Distributors:\n" + distributors.map(r => ` ${debugAccount(r)}`).join("\n")); +} + +export function logSendJob(signer: MultiChainSigner, job: SendJob): void { + const from = signer.identityToAddress(job.sender); + const to = job.recipient; + const amount = debugCoin(job.amount); + console.info(`Sending ${amount} from ${from} to ${to} ...`); +} diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts new file mode 100644 index 0000000000..3587566a71 --- /dev/null +++ b/packages/faucet/src/faucet.ts @@ -0,0 +1,38 @@ +import { generate, help, start, version } from "./actions"; + +export function main(args: ReadonlyArray): void { + if (args.length < 1) { + help(); + process.exit(1); + return; + } + + const action = args[0]; + const restArgs = args.slice(1); + + switch (action) { + case "generate": + generate(restArgs).catch(error => { + console.error(error); + process.exit(1); + }); + break; + case "help": + help(); + break; + case "version": + version().catch(error => { + console.error(error); + process.exit(1); + }); + break; + case "start": + start(restArgs).catch(error => { + console.error(error); + process.exit(1); + }); + break; + default: + throw new Error("Unexpected action argument"); + } +} diff --git a/packages/faucet/src/hdpaths.spec.ts b/packages/faucet/src/hdpaths.spec.ts new file mode 100644 index 0000000000..fb60694a66 --- /dev/null +++ b/packages/faucet/src/hdpaths.spec.ts @@ -0,0 +1,44 @@ +import { Slip10RawIndex } from "@iov/crypto"; + +import { debugPath } from "./hdpaths"; + +describe("hdpaths", () => { + describe("debugPath", () => { + it("works for no component", () => { + // See https://github.com/bitcoin/bips/blob/master/bip-0032/derivation.png from BIP32 + expect(debugPath([])).toEqual("m"); + }); + + it("works for normal components", () => { + const one = Slip10RawIndex.normal(1); + expect(debugPath([one])).toEqual("m/1"); + expect(debugPath([one, one])).toEqual("m/1/1"); + expect(debugPath([one, one, one])).toEqual("m/1/1/1"); + + const min = Slip10RawIndex.normal(0); + expect(debugPath([min])).toEqual("m/0"); + + const max = Slip10RawIndex.normal(2 ** 31 - 1); + expect(debugPath([max])).toEqual("m/2147483647"); + }); + + it("works for hardened components", () => { + const one = Slip10RawIndex.hardened(1); + expect(debugPath([one])).toEqual("m/1'"); + expect(debugPath([one, one])).toEqual("m/1'/1'"); + expect(debugPath([one, one, one])).toEqual("m/1'/1'/1'"); + + const min = Slip10RawIndex.hardened(0); + expect(debugPath([min])).toEqual("m/0'"); + + const max = Slip10RawIndex.hardened(2 ** 31 - 1); + expect(debugPath([max])).toEqual("m/2147483647'"); + }); + + it("works for mixed components", () => { + const one = Slip10RawIndex.normal(1); + const two = Slip10RawIndex.hardened(2); + expect(debugPath([one, two, two, one])).toEqual("m/1/2'/2'/1"); + }); + }); +}); diff --git a/packages/faucet/src/hdpaths.ts b/packages/faucet/src/hdpaths.ts new file mode 100644 index 0000000000..f61e35ba73 --- /dev/null +++ b/packages/faucet/src/hdpaths.ts @@ -0,0 +1,10 @@ +import { Slip10RawIndex } from "@iov/crypto"; + +export function debugPath(path: readonly Slip10RawIndex[]): string { + return path.reduce((current, component): string => { + const componentString = component.isHardened() + ? `${component.toNumber() - 2 ** 31}'` + : component.toString(); + return current + "/" + componentString; + }, "m"); +} diff --git a/packages/faucet/src/multichainhelpers.spec.ts b/packages/faucet/src/multichainhelpers.spec.ts new file mode 100644 index 0000000000..b81c855378 --- /dev/null +++ b/packages/faucet/src/multichainhelpers.spec.ts @@ -0,0 +1,56 @@ +import { Address, Algorithm, PubkeyBundle, PubkeyBytes, TokenTicker } from "@iov/bcp"; + +import { availableTokensFromHolder } from "./multichainhelpers"; + +describe("multichainhelpers", () => { + describe("availableTokensFromHolder", () => { + const defaultPubkey: PubkeyBundle = { + algo: Algorithm.Ed25519, + data: new Uint8Array([0, 1, 2, 3]) as PubkeyBytes, + }; + + it("works for an empty account", () => { + const tickers = availableTokensFromHolder({ + address: "aabbccdd" as Address, + pubkey: defaultPubkey, + balance: [], + }); + expect(tickers).toEqual([]); + }); + + it("works for one token", () => { + const tickers = availableTokensFromHolder({ + address: "aabbccdd" as Address, + pubkey: defaultPubkey, + balance: [ + { + quantity: "1", + fractionalDigits: 9, + tokenTicker: "CASH" as TokenTicker, + }, + ], + }); + expect(tickers).toEqual(["CASH"]); + }); + + it("works for two tokens", () => { + const tickers = availableTokensFromHolder({ + address: "aabbccdd" as Address, + pubkey: defaultPubkey, + balance: [ + { + quantity: "1", + fractionalDigits: 9, + tokenTicker: "CASH" as TokenTicker, + }, + { + quantity: "1", + fractionalDigits: 9, + tokenTicker: "TRASH" as TokenTicker, + }, + ], + }); + expect(tickers).toEqual(["CASH", "TRASH"]); + }); + }); +}); diff --git a/packages/faucet/src/multichainhelpers.ts b/packages/faucet/src/multichainhelpers.ts new file mode 100644 index 0000000000..d54b889153 --- /dev/null +++ b/packages/faucet/src/multichainhelpers.ts @@ -0,0 +1,137 @@ +import { + Account, + Identity, + isBlockInfoFailed, + isBlockInfoPending, + SendTransaction, + TokenTicker, +} from "@iov/bcp"; +import { UserProfile } from "@iov/keycontrol"; +import { MultiChainSigner } from "@iov/multichain"; + +import { needsRefill, refillAmount } from "./cashflow"; +import { debugAccount, logAccountsState, logSendJob } from "./debugging"; +import { SendJob } from "./types"; + +async function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export function identitiesOfFirstWallet(profile: UserProfile): ReadonlyArray { + const wallet = profile.wallets.value[0]; + return profile.getIdentities(wallet.id); +} + +export async function accountsOfFirstChain( + profile: UserProfile, + signer: MultiChainSigner, +): Promise> { + const addresses = identitiesOfFirstWallet(profile).map(identity => signer.identityToAddress(identity)); + const chainId = signer.chainIds()[0]; + + // tslint:disable-next-line: readonly-array + const out: Account[] = []; + for (const address of addresses) { + const response = await signer.connection(chainId).getAccount({ address: address }); + if (response) { + out.push({ + address: response.address, + balance: response.balance, + }); + } else { + out.push({ + address: address, + balance: [], + }); + } + } + + return out; +} + +export async function tokenTickersOfFirstChain( + signer: MultiChainSigner, +): Promise> { + const chainId = signer.chainIds()[0]; + return (await signer.connection(chainId).getAllTokens()).map(token => token.tokenTicker); +} + +/** + * Creates and posts a send transaction. Then waits until the transaction is in a block. + */ +export async function sendOnFirstChain( + profile: UserProfile, + signer: MultiChainSigner, + job: SendJob, +): Promise { + const chainId = signer.chainIds()[0]; + const connection = signer.connection(chainId); + + const sendWithFee = await connection.withDefaultFee({ + kind: "bcp/send", + chainId: chainId, + sender: signer.identityToAddress(job.sender), + senderPubkey: job.sender.pubkey, + recipient: job.recipient, + memo: "We ❤️ developers – iov.one", + amount: job.amount, + }); + + const post = await signer.signAndPost(job.sender, sendWithFee); + const blockInfo = await post.blockInfo.waitFor(info => !isBlockInfoPending(info)); + if (isBlockInfoFailed(blockInfo)) { + throw new Error(`Sending tokens failed. Code: ${blockInfo.code}, message: ${blockInfo.message}`); + } +} + +export function availableTokensFromHolder(holderAccount: Account): ReadonlyArray { + return holderAccount.balance.map(coin => coin.tokenTicker); +} + +export async function refillFirstChain(profile: UserProfile, signer: MultiChainSigner): Promise { + const chainId = signer.chainIds()[0]; + + console.info(`Connected to network: ${chainId}`); + console.info(`Tokens on network: ${(await tokenTickersOfFirstChain(signer)).join(", ")}`); + + const holderIdentity = identitiesOfFirstWallet(profile)[0]; + + const accounts = await accountsOfFirstChain(profile, signer); + logAccountsState(accounts); + const holderAccount = accounts[0]; + const distributorAccounts = accounts.slice(1); + + const availableTokens = availableTokensFromHolder(holderAccount); + console.info("Available tokens:", availableTokens); + + // tslint:disable-next-line: readonly-array + const jobs: SendJob[] = []; + + for (const token of availableTokens) { + const refillDistibutors = distributorAccounts.filter(account => needsRefill(account, token)); + console.info(`Refilling ${token} of:`); + console.info( + refillDistibutors.length ? refillDistibutors.map(r => ` ${debugAccount(r)}`).join("\n") : " none", + ); + for (const refillDistibutor of refillDistibutors) { + jobs.push({ + sender: holderIdentity, + recipient: refillDistibutor.address, + tokenTicker: token, + amount: refillAmount(token), + }); + } + } + if (jobs.length > 0) { + for (const job of jobs) { + logSendJob(signer, job); + await sendOnFirstChain(profile, signer, job); + await sleep(50); + } + + console.info("Done refilling accounts."); + logAccountsState(await accountsOfFirstChain(profile, signer)); + } else { + console.info("Nothing to be done. Anyways, thanks for checking."); + } +} diff --git a/packages/faucet/src/profile.ts b/packages/faucet/src/profile.ts new file mode 100644 index 0000000000..36c0352e16 --- /dev/null +++ b/packages/faucet/src/profile.ts @@ -0,0 +1,34 @@ +import { ChainId } from "@iov/bcp"; +import { UserProfile } from "@iov/keycontrol"; + +import { Codec, codecImplementation, createPathBuilderForCodec } from "./codec"; +import * as constants from "./constants"; +import { createWalletForCodec } from "./crypto"; +import { debugPath } from "./hdpaths"; + +export async function setSecretAndCreateIdentities( + profile: UserProfile, + mnemonic: string, + chainId: ChainId, + codecName: Codec, +): Promise { + if (profile.wallets.value.length !== 0) { + throw new Error("Profile already contains wallets"); + } + const wallet = profile.addWallet(createWalletForCodec(codecName, mnemonic)); + + const pathBuilder = createPathBuilderForCodec(codecName); + + // first account is the token holder + const numberOfIdentities = 1 + constants.concurrency; + for (let i = 0; i < numberOfIdentities; i++) { + // create + const path = pathBuilder(i); + const identity = await profile.createIdentity(wallet.id, chainId, path); + + // log + const role = i === 0 ? "token holder " : `distributor ${i}`; + const address = codecImplementation(codecName).identityToAddress(identity); + console.info(`Created ${role} (${debugPath(path)}): ${address}`); + } +} diff --git a/packages/faucet/src/types.ts b/packages/faucet/src/types.ts new file mode 100644 index 0000000000..28186ee903 --- /dev/null +++ b/packages/faucet/src/types.ts @@ -0,0 +1,8 @@ +import { Address, Amount, Identity, TokenTicker } from "@iov/bcp"; + +export interface SendJob { + readonly sender: Identity; + readonly recipient: Address; + readonly tokenTicker: TokenTicker; + readonly amount: Amount; +} diff --git a/packages/faucet/tsconfig.json b/packages/faucet/tsconfig.json new file mode 100644 index 0000000000..167e8c0226 --- /dev/null +++ b/packages/faucet/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "build", + "declarationDir": "build/types", + "rootDir": "src" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/faucet/tslint.json b/packages/faucet/tslint.json new file mode 100644 index 0000000000..0946f20963 --- /dev/null +++ b/packages/faucet/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tslint.json" +} diff --git a/packages/faucet/typedoc.js b/packages/faucet/typedoc.js new file mode 100644 index 0000000000..e2387c7de4 --- /dev/null +++ b/packages/faucet/typedoc.js @@ -0,0 +1,14 @@ +const packageJson = require("./package.json"); + +module.exports = { + src: ["./src"], + out: "docs", + exclude: "**/*.spec.ts", + target: "es6", + name: `${packageJson.name} Documentation`, + readme: "README.md", + mode: "file", + excludeExternals: true, + excludeNotExported: true, + excludePrivate: true, +}; diff --git a/scripts/iov_faucet_start.sh b/scripts/iov_faucet_start.sh deleted file mode 100755 index 2c8e797288..0000000000 --- a/scripts/iov_faucet_start.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -set -o errexit -o nounset -o pipefail -command -v shellcheck > /dev/null && shellcheck "$0" - -# Choose from https://hub.docker.com/r/iov1/iov-faucet/tags/ -FAUCET_VERSION="v0.8.1" - -TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/faucet_start.XXXXXXXXX") -LOGFILE="$TMP_DIR/faucet.log" - -DOCKER_HOST_IP=$(docker run --rm alpine ip route | awk 'NR==1 {print $3}') - -BLOCKCHAIN_URL="ws://$DOCKER_HOST_IP:23456" -echo "Connecting to $BLOCKCHAIN_URL" - -docker run --rm \ - --read-only \ - --env "FAUCET_CONCURRENCY=3" \ - --env "FAUCET_MNEMONIC=degree tackle suggest window test behind mesh extra cover prepare oak script" \ - -p 8000:8000 \ - "iov1/iov-faucet:${FAUCET_VERSION}" \ - start bns "$BLOCKCHAIN_URL" \ - > "$LOGFILE" & - -echo "Faucet running and logging into $LOGFILE" diff --git a/scripts/iov_faucet_stop.sh b/scripts/iov_faucet_stop.sh deleted file mode 100755 index af68ceca64..0000000000 --- a/scripts/iov_faucet_stop.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -o errexit -o nounset -o pipefail -command -v shellcheck > /dev/null && shellcheck "$0" - -LABEL_PART="iov1/iov-faucet" - -CONTAINER_ID=$(docker container ls | grep -F "$LABEL_PART:" | awk '{print $1}') -echo "Killing $LABEL_PART container '$CONTAINER_ID' ..." -docker container kill "$CONTAINER_ID" diff --git a/yarn.lock b/yarn.lock index fc1ff7545d..612711316f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -130,6 +130,15 @@ bn.js "^4.11.8" readonly-date "^1.0.0" +"@iov/jsonrpc@^2.0.0-alpha.7": + version "2.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/@iov/jsonrpc/-/jsonrpc-2.0.0-alpha.7.tgz#7bff8e1f21d52ff07482212ded8cc00e01dda964" + integrity sha512-eVCfNi3Zg4ZUEXOxhzRb3kcoBupBGQWmU4pYu7OYBi3uvuUz8KP6kcIdsy+51+y/nqrnk3H2Ur7MWCzwsu215w== + dependencies: + "@iov/encoding" "^2.0.0-alpha.7" + "@iov/stream" "^2.0.0-alpha.7" + xstream "^11.10.0" + "@iov/keycontrol@^2.0.0-alpha.7": version "2.0.0-alpha.7" resolved "https://registry.yarnpkg.com/@iov/keycontrol/-/keycontrol-2.0.0-alpha.7.tgz#d115a1b536664afb64b40a6db87aaf19f8d07afd" @@ -150,6 +159,35 @@ type-tagger "^1.0.0" xstream "^11.10.0" +"@iov/lisk@^2.0.0-alpha.7": + version "2.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/@iov/lisk/-/lisk-2.0.0-alpha.7.tgz#5ff6c617ac00e6be736ada34ad68dce0efd64637" + integrity sha512-m5mr2NDU7pxuS8d6SBXCZ1WBtCSXtV+EWqMEIuPSCJwZxnwmWotowX+WhSYn+dGyuvgTd7DpfQ5C6pliAyhcyQ== + dependencies: + "@iov/bcp" "^2.0.0-alpha.7" + "@iov/crypto" "^2.0.0-alpha.7" + "@iov/encoding" "^2.0.0-alpha.7" + "@iov/keycontrol" "^2.0.0-alpha.7" + "@iov/stream" "^2.0.0-alpha.7" + "@types/long" "^4.0.0" + axios "^0.19.0" + fast-deep-equal "^3.1.1" + long "^4.0.0" + readonly-date "^1.0.0" + xstream "^11.10.0" + +"@iov/multichain@^2.0.0-alpha.7": + version "2.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/@iov/multichain/-/multichain-2.0.0-alpha.7.tgz#29ec9f61ec2fa60c17d462c9be48feb7196c28ba" + integrity sha512-hhEyqalADrQa4JS99JqawM7jWLYGqb52Ipq/49Zz5eDG9/nj9QyLn5nayWLTbm60ZXzHvvGr5oJpZx8Xb2PzfA== + dependencies: + "@iov/bcp" "^2.0.0-alpha.7" + "@iov/encoding" "^2.0.0-alpha.7" + "@iov/jsonrpc" "^2.0.0-alpha.7" + "@iov/keycontrol" "^2.0.0-alpha.7" + "@types/long" "^4.0.0" + long "^4.0.0" + "@iov/stream@^2.0.0-alpha.7": version "2.0.0-alpha.7" resolved "https://registry.yarnpkg.com/@iov/stream/-/stream-2.0.0-alpha.7.tgz#212c3f684f592ec04ac43e166183d946a49b895c" @@ -157,6 +195,13 @@ dependencies: xstream "^11.10.0" +"@koa/cors@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.0.0.tgz#df021b4df2dadf1e2b04d7c8ddf93ba2d42519cb" + integrity sha512-hDp+cXj6vTYSwHRJfiSpnf5dTMv3FmqNKh1or9BPJk4SHOviHnK9GoL9dT0ypt/E+hlkRkZ9edHylcosW3Ghrw== + dependencies: + vary "^1.1.2" + "@lerna/add@3.20.0": version "3.20.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.20.0.tgz#bea7edf36fc93fb72ec34cb9ba854c48d4abf309" @@ -950,6 +995,45 @@ resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz#3c7750d0186b954c7f2d2f6acc8c3c7ba0c3412e" integrity sha512-wYxU3kp5zItbxKmeRYCEplS2MW7DzyBnxPGj+GJVHZEUZiK/nn5Ei1sUFgURDh+X051+zsGe28iud3oHjrYWQQ== +"@types/accepts@*": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" + integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ== + dependencies: + "@types/node" "*" + +"@types/bn.js@^4.11.6": + version "4.11.6" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c" + integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg== + dependencies: + "@types/node" "*" + +"@types/body-parser@*": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897" + integrity sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/cookies@*": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.4.tgz#26dedf791701abc0e36b5b79a5722f40e455f87b" + integrity sha512-oTGtMzZZAVuEjTwCjIh8T8FrC8n/uwy+PG0yTvQcdZ7etoel7C7/3MSd7qrukENTgQtotG7gvBlBojuVs7X5rw== + dependencies: + "@types/connect" "*" + "@types/express" "*" + "@types/keygrip" "*" + "@types/node" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -960,6 +1044,23 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/express-serve-static-core@*": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz#f6f41fa35d42e79dbf6610eccbb2637e6008a0cf" + integrity sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express@*": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c" + integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -969,6 +1070,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/http-assert@*": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b" + integrity sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ== + "@types/jasmine@^3.3.7": version "3.5.1" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.1.tgz#e417aa74738aa9e9285016abbfcae68d5be0f827" @@ -979,6 +1085,44 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/keygrip@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" + integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== + +"@types/koa-bodyparser@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@types/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz#54ecd662c45f3a4fa9de849528de5fc8ab269ba5" + integrity sha512-aB/vwwq4G9FAtKzqZ2p8UHTscXxZvICFKVjuckqxCtkX1Ro7F5KHkTCUqTRZFBgDoEkmeca+bFLI1bIsdPPZTA== + dependencies: + "@types/koa" "*" + +"@types/koa-compose@*": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" + integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ== + dependencies: + "@types/koa" "*" + +"@types/koa@*", "@types/koa@^2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.11.0.tgz#394a3e9ec94f796003a6c8374b4dbc2778746f20" + integrity sha512-Hgx/1/rVlJvqYBrdeCsS7PDiR2qbxlMt1RnmNWD4Uxi5FF9nwkYqIldo7urjc+dfNpk+2NRGcnAYd4L5xEhCcQ== + dependencies: + "@types/accepts" "*" + "@types/cookies" "*" + "@types/http-assert" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + +"@types/koa__cors@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/koa__cors/-/koa__cors-3.0.1.tgz#a8cf8535f0fe682c9421f1b9379837c585f8b66b" + integrity sha512-loqZNXliley8kncc4wrX9KMqLGN6YfiaO3a3VFX+yVkkXJwOrZU4lipdudNjw5mFyC+5hd7h9075hQWcVVpeOg== + dependencies: + "@types/koa" "*" + "@types/levelup@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@types/levelup/-/levelup-3.1.1.tgz#f7cc08f248f14cb6c92914e91bceb8761020e8f0" @@ -987,6 +1131,11 @@ "@types/abstract-leveldown" "*" "@types/node" "*" +"@types/long@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + "@types/memdown@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/memdown/-/memdown-3.0.0.tgz#2d909cb507afd341e3132d77dafa213347e47455" @@ -994,6 +1143,11 @@ dependencies: "@types/abstract-leveldown" "*" +"@types/mime@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -1019,6 +1173,19 @@ resolved "https://registry.yarnpkg.com/@types/random-js/-/random-js-1.0.31.tgz#18a8bcc075afa504421e638fcbe021f27e802941" integrity sha512-EAM56DrKw3VhcE4HV0/YlVKeJI07We4Mz1ra6TNtZpaMoiBVMA2bkLEcoFpYOyxoDXfVZWojxkR617LTqtRI0A== +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.3" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" + integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + "@typescript-eslint/eslint-plugin@^2.10.0": version "2.17.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.17.0.tgz#880435a9f9bdd50b45fa286ba63fed723d73c837" @@ -1257,7 +1424,7 @@ abstract-leveldown@~6.2.1: level-supports "~1.0.0" xtend "~4.0.0" -accepts@~1.3.4: +accepts@^1.3.5, accepts@~1.3.4: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== @@ -1365,7 +1532,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -any-promise@^1.0.0: +any-promise@^1.0.0, any-promise@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= @@ -1687,6 +1854,11 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.4.0: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== +bn.js@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.1.tgz#48efc4031a9c4041b9c99c6941d903463ab62eb5" + integrity sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA== + body-parser@^1.16.1: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1906,6 +2078,14 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cache-content-type@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" + integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== + dependencies: + mime-types "^2.1.18" + ylru "^1.2.0" + call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -2102,6 +2282,21 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +co-body@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/co-body/-/co-body-6.0.0.tgz#965b9337d7f5655480787471f4237664820827e3" + integrity sha512-9ZIcixguuuKIptnY8yemEOuhb71L/lLf+Rl5JfJEUiDNJk0e02MBt7BPxR2GEh5mw8dPthQYR4jPI/BnS1MQgw== + dependencies: + inflation "^2.0.0" + qs "^6.5.2" + raw-body "^2.3.3" + type-is "^1.6.16" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -2253,7 +2448,14 @@ contains-path@^0.1.0: resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= -content-type@~1.0.4: +content-disposition@~0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@^1.0.4, content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== @@ -2346,6 +2548,14 @@ cookie@0.3.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= +cookies@~0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" + integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== + dependencies: + depd "~2.0.0" + keygrip "~1.1.0" + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -2363,6 +2573,11 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +copy-to@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5" + integrity sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU= + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2534,6 +2749,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -2593,11 +2813,16 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -depd@~1.1.2: +depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + deprecation@^2.0.0: version "2.3.1" resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" @@ -2611,6 +2836,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destroy@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + detect-file@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" @@ -2760,7 +2990,7 @@ emojis-list@^2.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= -encodeurl@~1.0.2: +encodeurl@^1.0.2, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= @@ -2871,6 +3101,11 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error-inject@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" + integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc= + es-abstract@^1.17.0, es-abstract@^1.17.0-next.1: version "1.17.4" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" @@ -2914,7 +3149,7 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= @@ -3393,6 +3628,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + from2@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" @@ -3809,6 +4049,14 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== +http-assert@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878" + integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw== + dependencies: + deep-equal "~1.0.1" + http-errors "~1.7.2" + http-cache-semantics@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" @@ -3825,6 +4073,17 @@ http-errors@1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-errors@1.7.3, http-errors@^1.6.3, http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-proxy-agent@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" @@ -3956,6 +4215,11 @@ infer-owner@^1.0.3, infer-owner@^1.0.4: resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== +inflation@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f" + integrity sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -3964,7 +4228,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4184,6 +4448,11 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-function@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== + is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -4514,6 +4783,13 @@ karma@^4.1.0: tmp "0.0.33" useragent "2.3.0" +keygrip@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" + integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== + dependencies: + tsscmp "1.0.6" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -4538,6 +4814,64 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +koa-bodyparser@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/koa-bodyparser/-/koa-bodyparser-4.2.1.tgz#4d7dacb5e6db1106649b595d9e5ccb158b6f3b29" + integrity sha512-UIjPAlMZfNYDDe+4zBaOAUKYqkwAGcIU6r2ARf1UOXPAlfennQys5IiShaVeNf7KkVBlf88f2LeLvBFvKylttw== + dependencies: + co-body "^6.0.0" + copy-to "^2.0.1" + +koa-compose@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" + integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec= + dependencies: + any-promise "^1.1.0" + +koa-compose@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" + integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== + +koa-convert@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" + integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA= + dependencies: + co "^4.6.0" + koa-compose "^3.0.0" + +koa@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.11.0.tgz#fe5a51c46f566d27632dd5dc8fd5d7dd44f935a4" + integrity sha512-EpR9dElBTDlaDgyhDMiLkXrPwp6ZqgAIBvhhmxQ9XN4TFgW+gEz6tkcsNI6BnUbUftrKDjVFj4lW2/J2aNBMMA== + dependencies: + accepts "^1.3.5" + cache-content-type "^1.0.0" + content-disposition "~0.5.2" + content-type "^1.0.4" + cookies "~0.8.0" + debug "~3.1.0" + delegates "^1.0.0" + depd "^1.1.2" + destroy "^1.0.4" + encodeurl "^1.0.2" + error-inject "^1.0.0" + escape-html "^1.0.3" + fresh "~0.5.2" + http-assert "^1.3.0" + http-errors "^1.6.3" + is-generator-function "^1.0.7" + koa-compose "^4.1.0" + koa-convert "^1.2.0" + on-finished "^2.3.0" + only "~0.0.2" + parseurl "^1.3.2" + statuses "^1.5.0" + type-is "^1.6.16" + vary "^1.1.2" + lcid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" @@ -5004,7 +5338,7 @@ mime-db@1.43.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== -mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.26" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== @@ -5479,7 +5813,7 @@ octokit-pagination-methods@^1.1.0: resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== -on-finished@~2.3.0: +on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -5507,6 +5841,11 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +only@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= + optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -5746,7 +6085,7 @@ parseuri@0.0.5: dependencies: better-assert "~1.0.0" -parseurl@~1.3.3: +parseurl@^1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -6034,6 +6373,11 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.5.2: + version "6.9.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" + integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -6089,6 +6433,16 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^2.3.3: + version "2.4.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" + integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== + dependencies: + bytes "3.1.0" + http-errors "1.7.3" + iconv-lite "0.4.24" + unpipe "1.0.0" + read-cmd-shim@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" @@ -6443,16 +6797,16 @@ rxjs@^6.4.0, rxjs@^6.5.3: dependencies: tslib "^1.9.0" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -6808,7 +7162,7 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.5.0 < 2", statuses@~1.5.0: +"statuses@>= 1.5.0 < 2", statuses@^1.5.0, statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -7251,6 +7605,11 @@ tslint@^5.19.0: tslib "^1.8.0" tsutils "^2.29.0" +tsscmp@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + "tsutils@^2.28.0 || ^3.0.0", tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -7299,7 +7658,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.17: +type-is@^1.6.16, type-is@~1.6.17: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -7498,6 +7857,11 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" +vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -7808,3 +8172,8 @@ yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + +ylru@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f" + integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==