Add packages/faucet
This commit is contained in:
@@ -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
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.eslintignore
|
||||
@@ -0,0 +1,3 @@
|
||||
build/
|
||||
dist/
|
||||
docs/
|
||||
Executable
+6
@@ -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));
|
||||
@@ -0,0 +1 @@
|
||||
Directory used to trigger lerna package updates for all packages
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"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": {
|
||||
"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",
|
||||
"move-types": "shx rm -rf ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts",
|
||||
"format-types": "prettier --write --loglevel warn \"./types/**/*.d.ts\"",
|
||||
"build": "shx rm -rf ./build && tsc && yarn move-types && yarn format-types",
|
||||
"build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build",
|
||||
"test-node": "mocha \"./build/**/*.spec.js\"",
|
||||
"test": "yarn build-or-skip && yarn test-node"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iov/bcp": "^2.0.0-alpha.7",
|
||||
"@iov/bns": "^2.0.0-alpha.7",
|
||||
"@iov/crypto": "^2.0.0-alpha.7",
|
||||
"@iov/encoding": "^2.0.0-alpha.7",
|
||||
"@iov/ethereum": "^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/chai": "^4.2.7",
|
||||
"@types/koa": "^2.11.0",
|
||||
"@types/koa-bodyparser": "^4.3.0",
|
||||
"@types/koa__cors": "^3.0.1",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"chai": "^4.2.0",
|
||||
"mocha": "^7.0.1"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export function help(): void {
|
||||
const out = `
|
||||
Usage: iov-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.
|
||||
`.trim();
|
||||
|
||||
process.stdout.write(`${out}\n`);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { generate } from "./generate";
|
||||
export { help } from "./help";
|
||||
export { start } from "./start";
|
||||
export { version } from "./version";
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect } from "chai";
|
||||
|
||||
import { HttpError } from "./httperror";
|
||||
|
||||
describe("HttpError", () => {
|
||||
it("can be constructed", () => {
|
||||
{
|
||||
const error = new HttpError(400, "Invalid name field");
|
||||
expect(error.message).to.eql("Invalid name field");
|
||||
expect(error.status).to.eql(400);
|
||||
expect(error.expose).to.eql(true);
|
||||
}
|
||||
{
|
||||
const error = new HttpError(500, "Out of memory", false);
|
||||
expect(error.message).to.eql("Out of memory");
|
||||
expect(error.status).to.eql(500);
|
||||
expect(error.expose).to.eql(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export class HttpError extends Error {
|
||||
constructor(public readonly status: number, text: string, public readonly expose: boolean = true) {
|
||||
super(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { start } from "./start";
|
||||
@@ -0,0 +1,48 @@
|
||||
import { expect } from "chai";
|
||||
|
||||
import { RequestParser } from "./requestparser";
|
||||
|
||||
describe("RequestParser", () => {
|
||||
it("can process valid credit request", () => {
|
||||
const body = { address: "abc", ticker: "CASH" };
|
||||
expect(RequestParser.parseCreditBody(body)).to.eql({ address: "abc", ticker: "CASH" });
|
||||
});
|
||||
|
||||
it("throws for invalid credit requests", () => {
|
||||
// address unset
|
||||
{
|
||||
const body = { ticker: "CASH" };
|
||||
expect(() => RequestParser.parseCreditBody(body)).to.throw(/Property 'address' must be a string/i);
|
||||
}
|
||||
|
||||
// address wrong type
|
||||
{
|
||||
const body = { address: true, ticker: "CASH" };
|
||||
expect(() => RequestParser.parseCreditBody(body)).to.throw(/Property 'address' must be a string/i);
|
||||
}
|
||||
|
||||
// address empty
|
||||
{
|
||||
const body = { address: "", ticker: "CASH" };
|
||||
expect(() => RequestParser.parseCreditBody(body)).to.throw(/Property 'address' must not be empty/i);
|
||||
}
|
||||
|
||||
// ticker unset
|
||||
{
|
||||
const body = { address: "abc" };
|
||||
expect(() => RequestParser.parseCreditBody(body)).to.throw(/Property 'ticker' must be a string/i);
|
||||
}
|
||||
|
||||
// ticker wrong type
|
||||
{
|
||||
const body = { address: "abc", ticker: true };
|
||||
expect(() => RequestParser.parseCreditBody(body)).to.throw(/Property 'ticker' must be a string/i);
|
||||
}
|
||||
|
||||
// ticker empty
|
||||
{
|
||||
const body = { address: "abc", ticker: "" };
|
||||
expect(() => RequestParser.parseCreditBody(body)).to.throw(/Property 'ticker' must not be empty/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable require-atomic-updates */
|
||||
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, gasLimit, gasPrice, 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, codec);
|
||||
setInterval(async () => refillFirstChain(profile, signer, codec), 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,
|
||||
gasPrice: gasPrice(codec),
|
||||
gasLimit: gasLimit(codec),
|
||||
};
|
||||
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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { TokenTicker } from "@iov/bcp";
|
||||
import { expect } from "chai";
|
||||
|
||||
import {
|
||||
creditAmount,
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
refillAmount,
|
||||
refillThreshold,
|
||||
setFractionalDigits,
|
||||
} from "./cashflow";
|
||||
import { Codec } from "./codec";
|
||||
|
||||
describe("Cashflow", () => {
|
||||
before(() => {
|
||||
setFractionalDigits(3);
|
||||
});
|
||||
|
||||
describe("creditAmount", () => {
|
||||
it("returns '10' + '000' by default", () => {
|
||||
expect(creditAmount("TOKENZ" as TokenTicker)).to.eql({
|
||||
quantity: "10000",
|
||||
fractionalDigits: 3,
|
||||
tokenTicker: "TOKENZ",
|
||||
});
|
||||
expect(creditAmount("TRASH" as TokenTicker)).to.eql({
|
||||
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)).to.eql({
|
||||
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)).to.eql({
|
||||
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)).to.eql({
|
||||
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)).to.eql({
|
||||
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)).to.eql({
|
||||
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)).to.eql({
|
||||
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)).to.eql({
|
||||
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)).to.eql({
|
||||
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)).to.eql({
|
||||
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)).to.eql({
|
||||
quantity: "110000",
|
||||
fractionalDigits: 3,
|
||||
tokenTicker: "WTF",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("gasPrice", () => {
|
||||
it("returns undefined for non-Ethereum codecs", () => {
|
||||
expect(gasPrice(Codec.Lisk)).to.be.undefined;
|
||||
expect(gasPrice(Codec.Bns)).to.be.undefined;
|
||||
});
|
||||
|
||||
it("returns amount for Ethereum codec", () => {
|
||||
expect(gasPrice(Codec.Ethereum)).to.be.eql({
|
||||
quantity: "20000000000",
|
||||
fractionalDigits: 18,
|
||||
tokenTicker: "ETH",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("gasLimit", () => {
|
||||
it("returns undefined for non-Ethereum codecs", () => {
|
||||
expect(gasLimit(Codec.Lisk)).to.be.undefined;
|
||||
expect(gasLimit(Codec.Bns)).to.be.undefined;
|
||||
});
|
||||
|
||||
it("returns amount for Ethereum codec", () => {
|
||||
expect(gasLimit(Codec.Ethereum)).to.be.eql({
|
||||
quantity: "2100000",
|
||||
fractionalDigits: 18,
|
||||
tokenTicker: "ETH",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import BN = require("bn.js");
|
||||
|
||||
import { Account, Amount, TokenTicker } from "@iov/bcp";
|
||||
import { Int53 } from "@iov/encoding";
|
||||
|
||||
import { Codec } from "./codec";
|
||||
import * as constants from "./constants";
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
export function gasPrice(codec: Codec): Amount | undefined {
|
||||
switch (codec) {
|
||||
case Codec.Bns:
|
||||
case Codec.Lisk:
|
||||
return undefined;
|
||||
case Codec.Ethereum:
|
||||
return {
|
||||
quantity: constants.ethereum.gasPrice,
|
||||
fractionalDigits: 18,
|
||||
tokenTicker: "ETH" as TokenTicker,
|
||||
};
|
||||
default:
|
||||
throw new Error("No codec imlementation for this codec found");
|
||||
}
|
||||
}
|
||||
|
||||
export function gasLimit(codec: Codec): Amount | undefined {
|
||||
switch (codec) {
|
||||
case Codec.Bns:
|
||||
case Codec.Lisk:
|
||||
return undefined;
|
||||
case Codec.Ethereum:
|
||||
return {
|
||||
quantity: constants.ethereum.gasLimit,
|
||||
fractionalDigits: 18,
|
||||
tokenTicker: "ETH" as TokenTicker,
|
||||
};
|
||||
default:
|
||||
throw new Error("Codec not supported");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { expect } from "chai";
|
||||
|
||||
import { Codec, codecFromString } from "./codec";
|
||||
|
||||
describe("Codec", () => {
|
||||
it("can convert string to codec", () => {
|
||||
expect(codecFromString("bns")).to.equal(Codec.Bns);
|
||||
expect(codecFromString("lisk")).to.equal(Codec.Lisk);
|
||||
expect(codecFromString("ethereum")).to.equal(Codec.Ethereum);
|
||||
|
||||
expect(() => codecFromString("")).to.throw(/not supported/i);
|
||||
expect(() => codecFromString("abc")).to.throw(/not supported/i);
|
||||
expect(() => codecFromString("LISK")).to.throw(/not supported/i);
|
||||
expect(() => codecFromString("ETHEREUM")).to.throw(/not supported/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ChainConnector, TxCodec } from "@iov/bcp";
|
||||
import { bnsCodec, createBnsConnector } from "@iov/bns";
|
||||
import { Slip10RawIndex } from "@iov/crypto";
|
||||
import { createEthereumConnector, ethereumCodec } from "@iov/ethereum";
|
||||
import { HdPaths } from "@iov/keycontrol";
|
||||
import { createLiskConnector, liskCodec } from "@iov/lisk";
|
||||
|
||||
export const enum Codec {
|
||||
Bns,
|
||||
Lisk,
|
||||
Ethereum,
|
||||
}
|
||||
|
||||
export function codecFromString(input: string): Codec {
|
||||
switch (input) {
|
||||
case "bns":
|
||||
return Codec.Bns;
|
||||
case "lisk":
|
||||
return Codec.Lisk;
|
||||
case "ethereum":
|
||||
return Codec.Ethereum;
|
||||
default:
|
||||
throw new Error(`Codec '${input}' not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
export function codecImplementation(codec: Codec): TxCodec {
|
||||
switch (codec) {
|
||||
case Codec.Bns:
|
||||
return bnsCodec;
|
||||
case Codec.Lisk:
|
||||
return liskCodec;
|
||||
case Codec.Ethereum:
|
||||
return ethereumCodec;
|
||||
default:
|
||||
throw new Error("No codec implementation for this codec found");
|
||||
}
|
||||
}
|
||||
|
||||
export function createPathBuilderForCodec(codec: Codec): (derivation: number) => readonly Slip10RawIndex[] {
|
||||
const pathBuilder = (accountIndex: number): readonly Slip10RawIndex[] => {
|
||||
switch (codec) {
|
||||
case Codec.Bns:
|
||||
return HdPaths.iov(accountIndex);
|
||||
case Codec.Lisk:
|
||||
return HdPaths.bip44Like(134, accountIndex);
|
||||
case Codec.Ethereum:
|
||||
return HdPaths.ethereum(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.Bns:
|
||||
return createBnsConnector(url);
|
||||
case Codec.Lisk:
|
||||
return createLiskConnector(url);
|
||||
case Codec.Ethereum:
|
||||
return createEthereumConnector(url, {});
|
||||
default:
|
||||
throw new Error("No connector for this codec found");
|
||||
}
|
||||
}
|
||||
|
||||
export function codecDefaultFractionalDigits(codec: Codec): number {
|
||||
switch (codec) {
|
||||
case Codec.Bns:
|
||||
return 9; // fixed for all weave tokens
|
||||
case Codec.Lisk:
|
||||
return 8;
|
||||
case Codec.Ethereum:
|
||||
return 18;
|
||||
default:
|
||||
throw new Error("Unknown codec");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
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;
|
||||
export const ethereum = {
|
||||
gasPrice: "20000000000",
|
||||
gasLimit: "2100000",
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Ed25519HdWallet, Secp256k1HdWallet, Wallet } from "@iov/keycontrol";
|
||||
|
||||
import { Codec } from "./codec";
|
||||
|
||||
export function createWalletForCodec(input: Codec, mnemonic: string): Wallet {
|
||||
switch (input) {
|
||||
case Codec.Bns:
|
||||
case Codec.Lisk:
|
||||
return Ed25519HdWallet.fromMnemonic(mnemonic);
|
||||
case Codec.Ethereum:
|
||||
return Secp256k1HdWallet.fromMnemonic(mnemonic);
|
||||
default:
|
||||
throw new Error(`Codec '${input}' not supported`);
|
||||
}
|
||||
}
|
||||
@@ -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} ...`);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Slip10RawIndex } from "@iov/crypto";
|
||||
import { expect } from "chai";
|
||||
|
||||
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([])).to.eql("m");
|
||||
});
|
||||
|
||||
it("works for normal components", () => {
|
||||
const one = Slip10RawIndex.normal(1);
|
||||
expect(debugPath([one])).to.eql("m/1");
|
||||
expect(debugPath([one, one])).to.eql("m/1/1");
|
||||
expect(debugPath([one, one, one])).to.eql("m/1/1/1");
|
||||
|
||||
const min = Slip10RawIndex.normal(0);
|
||||
expect(debugPath([min])).to.eql("m/0");
|
||||
|
||||
const max = Slip10RawIndex.normal(2 ** 31 - 1);
|
||||
expect(debugPath([max])).to.eql("m/2147483647");
|
||||
});
|
||||
|
||||
it("works for hardened components", () => {
|
||||
const one = Slip10RawIndex.hardened(1);
|
||||
expect(debugPath([one])).to.eql("m/1'");
|
||||
expect(debugPath([one, one])).to.eql("m/1'/1'");
|
||||
expect(debugPath([one, one, one])).to.eql("m/1'/1'/1'");
|
||||
|
||||
const min = Slip10RawIndex.hardened(0);
|
||||
expect(debugPath([min])).to.eql("m/0'");
|
||||
|
||||
const max = Slip10RawIndex.hardened(2 ** 31 - 1);
|
||||
expect(debugPath([max])).to.eql("m/2147483647'");
|
||||
});
|
||||
|
||||
it("works for mixed components", () => {
|
||||
const one = Slip10RawIndex.normal(1);
|
||||
const two = Slip10RawIndex.hardened(2);
|
||||
expect(debugPath([one, two, two, one])).to.eql("m/1/2'/2'/1");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Address, Algorithm, PubkeyBundle, PubkeyBytes, TokenTicker } from "@iov/bcp";
|
||||
import { expect } from "chai";
|
||||
|
||||
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).to.be.empty;
|
||||
});
|
||||
|
||||
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).to.eql(["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).to.eql(["CASH", "TRASH"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
Account,
|
||||
Identity,
|
||||
isBlockInfoFailed,
|
||||
isBlockInfoPending,
|
||||
SendTransaction,
|
||||
TokenTicker,
|
||||
WithCreator,
|
||||
} from "@iov/bcp";
|
||||
import { UserProfile } from "@iov/keycontrol";
|
||||
import { MultiChainSigner } from "@iov/multichain";
|
||||
|
||||
import { gasLimit, gasPrice, needsRefill, refillAmount } from "./cashflow";
|
||||
import { Codec } from "./codec";
|
||||
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];
|
||||
|
||||
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 & WithCreator>({
|
||||
kind: "bcp/send",
|
||||
creator: {
|
||||
chainId: chainId,
|
||||
pubkey: job.sender.pubkey,
|
||||
},
|
||||
sender: signer.identityToAddress(job.sender),
|
||||
recipient: job.recipient,
|
||||
memo: "We ❤️ developers – iov.one",
|
||||
amount: job.amount,
|
||||
});
|
||||
|
||||
const post = await signer.signAndPost(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,
|
||||
codec: Codec,
|
||||
): 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);
|
||||
|
||||
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),
|
||||
gasPrice: gasPrice(codec),
|
||||
gasLimit: gasLimit(codec),
|
||||
});
|
||||
}
|
||||
}
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Address, Amount, Identity, TokenTicker } from "@iov/bcp";
|
||||
|
||||
export interface SendJob {
|
||||
readonly sender: Identity;
|
||||
readonly recipient: Address;
|
||||
readonly tokenTicker: TokenTicker;
|
||||
readonly amount: Amount;
|
||||
readonly gasPrice?: Amount;
|
||||
readonly gasLimit?: Amount;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"declarationDir": "build/types",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tslint.json"
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user