Merge pull request #18 from confio/faucet

Add faucet 💸
This commit is contained in:
Simon Warta 2020-01-30 07:58:31 +01:00 committed by GitHub
commit 8fa2a15ed1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1777 additions and 126 deletions

View File

@ -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

View File

@ -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]+"] }]
}
}
]
}

3
NOTICE
View File

@ -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

23
faucet.Dockerfile Normal file
View File

@ -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 [""]

View File

@ -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<CosmosConnection> {
return {
establishConnection: async () => CosmosConnection.establish(url, prefix, tokenInfo),
codec: cosmosCodec,
expectedChainId: expectedChainId,
};
}

View File

@ -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);
});
});

View File

@ -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);

View File

@ -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<SendTransaction>({
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<SendTransaction>({
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);

View File

@ -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<CosmosConnection> {
): Promise<CosmWasmConnection> {
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<ChainData> {

View File

@ -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<CosmWasmConnection> {
const codec = new CosmWasmCodec(prefix, tokenInfo);
return {
establishConnection: async () => CosmWasmConnection.establish(url, prefix, tokenInfo),
codec: codec,
expectedChainId: expectedChainId,
};
}

View File

@ -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";

View File

@ -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;

View File

@ -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<CosmosConnection>;
export declare class CosmWasmConnection implements BlockchainConnection {
static establish(
url: string,
prefix: CosmosBech32Prefix,
tokenInfo: TokenInfos,
): Promise<CosmWasmConnection>;
private static initialize;
private readonly restClient;
private readonly chainData;

View File

@ -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<CosmosConnection>;
): ChainConnector<CosmWasmConnection>;

View File

@ -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";

View File

@ -0,0 +1 @@
../../.eslintignore

3
packages/faucet/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/
dist/
docs/

126
packages/faucet/README.md Normal file
View File

@ -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
```

View File

@ -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));

View File

@ -0,0 +1,12 @@
{
"suite": {
"displayNumber": true
},
"spec": {
"displayDuration": true
},
"summary": {
"displayPending": false,
"displayStacktrace": true
}
}

View File

@ -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();

View File

@ -0,0 +1 @@
Directory used to trigger lerna package updates for all packages

View File

@ -0,0 +1,56 @@
{
"name": "@cosmwasm/faucet",
"version": "0.0.1",
"description": "The faucet",
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
"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"
}
}

View File

@ -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<string>): Promise<void> {
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);
}

View File

@ -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`);
}

View File

@ -0,0 +1,4 @@
export { generate } from "./generate";
export { help } from "./help";
export { start } from "./start";
export { version } from "./version";

View File

@ -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);
}
});
});

View File

@ -0,0 +1,5 @@
export class HttpError extends Error {
constructor(public readonly status: number, text: string, public readonly expose: boolean = true) {
super(text);
}
}

View File

@ -0,0 +1 @@
export { start } from "./start";

View File

@ -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);
}
});
});

View File

@ -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,
};
}
}

View File

@ -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<string>): Promise<void> {
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);
}

View File

@ -0,0 +1,15 @@
import fs from "fs";
export async function version(): Promise<void> {
return new Promise<void>((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();
}
});
});
}

View File

@ -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",
});
});
});
});

View File

@ -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);
}

View File

@ -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);
});
});

View File

@ -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");
}
}

View File

@ -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;

View File

@ -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`);
}
}

View File

@ -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<Amount>): 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<Account>): 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} ...`);
}

View File

@ -0,0 +1,38 @@
import { generate, help, start, version } from "./actions";
export function main(args: ReadonlyArray<string>): 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");
}
}

View File

@ -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");
});
});
});

View File

@ -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");
}

View File

@ -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"]);
});
});
});

View File

@ -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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function identitiesOfFirstWallet(profile: UserProfile): ReadonlyArray<Identity> {
const wallet = profile.wallets.value[0];
return profile.getIdentities(wallet.id);
}
export async function accountsOfFirstChain(
profile: UserProfile,
signer: MultiChainSigner,
): Promise<ReadonlyArray<Account>> {
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<ReadonlyArray<TokenTicker>> {
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<void> {
const chainId = signer.chainIds()[0];
const connection = signer.connection(chainId);
const sendWithFee = await connection.withDefaultFee<SendTransaction>({
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<TokenTicker> {
return holderAccount.balance.map(coin => coin.tokenTicker);
}
export async function refillFirstChain(profile: UserProfile, signer: MultiChainSigner): Promise<void> {
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.");
}
}

View File

@ -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<void> {
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}`);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"declarationDir": "build/types",
"rootDir": "src"
},
"include": [
"src/**/*"
]
}

View File

@ -0,0 +1,3 @@
{
"extends": "../../tslint.json"
}

View File

@ -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,
};

View File

@ -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"

View File

@ -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"

403
yarn.lock
View File

@ -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==