Merge pull request #23 from confio/no-multichain

No multichain
This commit is contained in:
Ethan Frey 2020-01-30 12:47:25 +01:00 committed by GitHub
commit 065b1883ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 92 additions and 224 deletions

View File

@ -27,6 +27,7 @@
"import/no-cycle": "warn",
"simple-import-sort/sort": "warn",
"@typescript-eslint/explicit-function-return-type": ["warn", { "allowExpressions": true }],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],

View File

@ -21,7 +21,7 @@ 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/cosmwasm-faucet start cosmwasm "http://localhost:1317"
./bin/cosmwasm-faucet start "http://localhost:1317"
```
## Usage
@ -36,12 +36,10 @@ 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
1 Chain ID
start Starts the faucet
1 Codec
2 Node base URL, e.g. wss://bov.friendnet-fast.iov.one
1 Node base URL, e.g. http://localhost:1317
Environment variables
@ -99,7 +97,7 @@ DOCKER_HOST_IP=$(docker run --read-only --rm alpine ip route | awk 'NR==1 {print
-e FAUCET_CONCURRENCY \
-p 8000:8000 \
cosmwasm/faucet:manual \
start cosmwasm "http://$DOCKER_HOST_IP:1317"
start "http://$DOCKER_HOST_IP:1317"
```
### Using the faucet

View File

@ -21,7 +21,7 @@
"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/cosmwasm-faucet start cosmwasm \"http://localhost:1317\"",
"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/cosmwasm-faucet start \"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 .",
@ -36,8 +36,7 @@
"@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",
"@iov/keycontrol": "^2.0.0-alpha.7",
"@koa/cors": "^3.0.0",
"axios": "^0.19.0",
"fast-deep-equal": "^3.1.1",

View File

@ -2,22 +2,21 @@ import { ChainId } from "@iov/bcp";
import { Bip39, Random } from "@iov/crypto";
import { UserProfile } from "@iov/keycontrol";
import { codecFromString } from "../codec";
import * as constants from "../constants";
import { setSecretAndCreateIdentities } from "../profile";
export async function generate(args: ReadonlyArray<string>): Promise<void> {
if (args.length < 2) {
if (args.length < 1) {
throw Error(
`Not enough arguments for action 'generate'. See '${constants.binaryName} help' or README for arguments.`,
);
}
const codecName = codecFromString(args[0]);
const chainId = args[1] as ChainId;
const chainId = args[0] as ChainId;
const mnemonic = Bip39.encode(Random.getBytes(16)).toString();
console.info(`FAUCET_MNEMONIC="${mnemonic}"`);
const profile = new UserProfile();
await setSecretAndCreateIdentities(profile, mnemonic, chainId, codecName);
await setSecretAndCreateIdentities(profile, mnemonic, chainId);
}

View File

@ -11,12 +11,10 @@ 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
1 Chain ID
start Starts the faucet
1 Codec
2 Node base URL, e.g. wss://bov.friendnet-fast.iov.one
1 Node base URL, e.g. http://localhost:1317
Environment variables

View File

@ -1,26 +1,20 @@
// 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 { codecDefaultFractionalDigits, codecImplementation, establishConnection } from "../../codec";
import * as constants from "../../constants";
import { logAccountsState, logSendJob } from "../../debugging";
import {
accountsOfFirstChain,
availableTokensFromHolder,
identitiesOfFirstWallet,
refillFirstChain,
sendOnFirstChain,
tokenTickersOfFirstChain,
loadAccounts,
loadTokenTickers,
refill,
send,
} from "../../multichainhelpers";
import { setSecretAndCreateIdentities } from "../../profile";
import { SendJob } from "../../types";
@ -35,13 +29,13 @@ function getCount(): number {
}
export async function start(args: ReadonlyArray<string>): Promise<void> {
if (args.length < 2) {
if (args.length < 1) {
throw Error(
`Not enough arguments for action 'start'. See '${constants.binaryName} help' or README for arguments.`,
);
}
const codec = codecFromString(args[0]);
const blockchainBaseUrl: string = args[1];
const blockchainBaseUrl = args[0];
const port = constants.port;
@ -49,34 +43,33 @@ export async function start(args: ReadonlyArray<string>): Promise<void> {
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 connection = await establishConnection(blockchainBaseUrl);
const connectedChainId = connection.chainId();
console.info(`Connected to network: ${connectedChainId}`);
setFractionalDigits(codecDefaultFractionalDigits(codec));
await setSecretAndCreateIdentities(profile, constants.mnemonic, connectedChainId, codec);
setFractionalDigits(codecDefaultFractionalDigits());
await setSecretAndCreateIdentities(profile, constants.mnemonic, connectedChainId);
const chainTokens = await tokenTickersOfFirstChain(signer);
const chainTokens = await loadTokenTickers(connection);
console.info("Chain tokens:", chainTokens);
const accounts = await accountsOfFirstChain(profile, signer);
const accounts = await loadAccounts(profile, connection);
logAccountsState(accounts);
let availableTokens = availableTokensFromHolder(accounts[0]);
console.info("Available tokens:", availableTokens);
setInterval(async () => {
const updatedAccounts = await accountsOfFirstChain(profile, signer);
const updatedAccounts = await loadAccounts(profile, connection);
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
await refill(profile, connection);
setInterval(async () => refill(profile, connection), 60_000); // ever 60 seconds
console.info("Creating webserver ...");
const api = new Koa();
@ -95,7 +88,7 @@ export async function start(args: ReadonlyArray<string>): Promise<void> {
"See https://github.com/iov-one/iov-faucet for all further information.\n";
break;
case "/status": {
const updatedAccounts = await accountsOfFirstChain(profile, signer);
const updatedAccounts = await loadAccounts(profile, connection);
context.response.body = {
status: "ok",
nodeUrl: blockchainBaseUrl,
@ -120,7 +113,7 @@ export async function start(args: ReadonlyArray<string>): Promise<void> {
const requestBody = (context.request as any).body;
const { address, ticker } = RequestParser.parseCreditBody(requestBody);
if (!codecImplementation(codec).isValidAddress(address)) {
if (!codecImplementation().isValidAddress(address)) {
throw new HttpError(400, "Address is not in the expected format for this chain.");
}
@ -138,8 +131,8 @@ export async function start(args: ReadonlyArray<string>): Promise<void> {
amount: creditAmount(ticker),
tokenTicker: ticker,
};
logSendJob(signer, job);
await sendOnFirstChain(profile, signer, job);
logSendJob(job);
await send(profile, connection, job);
} catch (e) {
console.error(e);
throw new HttpError(500, "Sending tokens failed");

View File

@ -1,14 +1 @@
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);
});
});
describe("codec", () => {});

View File

@ -1,44 +1,7 @@
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";
import { CosmWasmCodec, CosmWasmConnection, TokenInfo } from "@cosmwasm/bcp";
import { TokenTicker, TxCodec } from "@iov/bcp";
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 prefix = "cosmos";
const tokens: readonly TokenInfo[] = [
{
fractionalDigits: 6,
@ -53,24 +16,15 @@ export function createChainConnector(codec: Codec, url: string): ChainConnector
denom: "stake",
},
];
return createCosmWasmConnector(url, "cosmos", tokens);
}
default:
throw new Error("No connector for this codec found");
}
export async function establishConnection(url: string): Promise<CosmWasmConnection> {
return CosmWasmConnection.establish(url, prefix, tokens);
}
export function codecImplementation(codec: Codec): TxCodec {
return createChainConnector(codec, "unused dummy url").codec;
export function codecImplementation(): TxCodec {
return new CosmWasmCodec(prefix, tokens);
}
export function codecDefaultFractionalDigits(codec: Codec): number {
switch (codec) {
case Codec.Lisk:
return 8;
case Codec.CosmWasm:
export function codecDefaultFractionalDigits(): number {
return 6;
default:
throw new Error("Unknown codec");
}
}

View File

@ -1,14 +0,0 @@
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

@ -1,7 +1,7 @@
import { Account, Amount } from "@iov/bcp";
import { Decimal } from "@iov/encoding";
import { MultiChainSigner } from "@iov/multichain";
import { codecImplementation } from "./codec";
import { SendJob } from "./types";
/** A string representation of a coin in a human-readable format that can change at any time */
@ -30,8 +30,8 @@ export function logAccountsState(accounts: ReadonlyArray<Account>): void {
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);
export function logSendJob(job: SendJob): void {
const from = codecImplementation().identityToAddress(job.sender);
const to = job.recipient;
const amount = debugAmount(job.amount);
console.info(`Sending ${amount} from ${from} to ${to} ...`);

View File

@ -1,5 +1,6 @@
import {
Account,
BlockchainConnection,
Identity,
isBlockInfoFailed,
isBlockInfoPending,
@ -7,9 +8,9 @@ import {
TokenTicker,
} from "@iov/bcp";
import { UserProfile } from "@iov/keycontrol";
import { MultiChainSigner } from "@iov/multichain";
import { needsRefill, refillAmount } from "./cashflow";
import { codecImplementation } from "./codec";
import { debugAccount, logAccountsState, logSendJob } from "./debugging";
import { SendJob } from "./types";
@ -22,17 +23,17 @@ export function identitiesOfFirstWallet(profile: UserProfile): ReadonlyArray<Ide
return profile.getIdentities(wallet.id);
}
export async function accountsOfFirstChain(
export async function loadAccounts(
profile: UserProfile,
signer: MultiChainSigner,
connection: BlockchainConnection,
): Promise<ReadonlyArray<Account>> {
const addresses = identitiesOfFirstWallet(profile).map(identity => signer.identityToAddress(identity));
const chainId = signer.chainIds()[0];
const codec = codecImplementation();
const addresses = identitiesOfFirstWallet(profile).map(identity => codec.identityToAddress(identity));
// tslint:disable-next-line: readonly-array
const out: Account[] = [];
for (const address of addresses) {
const response = await signer.connection(chainId).getAccount({ address: address });
const response = await connection.getAccount({ address: address });
if (response) {
out.push({
address: response.address,
@ -49,35 +50,36 @@ export async function accountsOfFirstChain(
return out;
}
export async function tokenTickersOfFirstChain(
signer: MultiChainSigner,
export async function loadTokenTickers(
connection: BlockchainConnection,
): Promise<ReadonlyArray<TokenTicker>> {
const chainId = signer.chainIds()[0];
return (await signer.connection(chainId).getAllTokens()).map(token => token.tokenTicker);
return (await connection.getAllTokens()).map(token => token.tokenTicker);
}
/**
* Creates and posts a send transaction. Then waits until the transaction is in a block.
*/
export async function sendOnFirstChain(
export async function send(
profile: UserProfile,
signer: MultiChainSigner,
connection: BlockchainConnection,
job: SendJob,
): Promise<void> {
const chainId = signer.chainIds()[0];
const connection = signer.connection(chainId);
const codec = codecImplementation();
const sendWithFee = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: chainId,
sender: signer.identityToAddress(job.sender),
chainId: connection.chainId(),
sender: codec.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 nonce = await connection.getNonce({ pubkey: job.sender.pubkey });
const signed = await profile.signTransaction(job.sender, sendWithFee, codec, nonce);
const post = await connection.postTx(codec.bytesToPost(signed));
const blockInfo = await post.blockInfo.waitFor(info => !isBlockInfoPending(info));
if (isBlockInfoFailed(blockInfo)) {
throw new Error(`Sending tokens failed. Code: ${blockInfo.code}, message: ${blockInfo.message}`);
@ -88,15 +90,13 @@ export function availableTokensFromHolder(holderAccount: Account): ReadonlyArray
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(", ")}`);
export async function refill(profile: UserProfile, connection: BlockchainConnection): Promise<void> {
console.info(`Connected to network: ${connection.chainId()}`);
console.info(`Tokens on network: ${(await loadTokenTickers(connection)).join(", ")}`);
const holderIdentity = identitiesOfFirstWallet(profile)[0];
const accounts = await accountsOfFirstChain(profile, signer);
const accounts = await loadAccounts(profile, connection);
logAccountsState(accounts);
const holderAccount = accounts[0];
const distributorAccounts = accounts.slice(1);
@ -124,13 +124,13 @@ export async function refillFirstChain(profile: UserProfile, signer: MultiChainS
}
if (jobs.length > 0) {
for (const job of jobs) {
logSendJob(signer, job);
await sendOnFirstChain(profile, signer, job);
logSendJob(job);
await send(profile, connection, job);
await sleep(50);
}
console.info("Done refilling accounts.");
logAccountsState(await accountsOfFirstChain(profile, signer));
logAccountsState(await loadAccounts(profile, connection));
} else {
console.info("Nothing to be done. Anyways, thanks for checking.");
}

View File

@ -1,34 +1,30 @@
import { ChainId } from "@iov/bcp";
import { UserProfile } from "@iov/keycontrol";
import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol";
import { Codec, codecImplementation, createPathBuilderForCodec } from "./codec";
import { codecImplementation } 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);
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(mnemonic));
// first account is the token holder
const numberOfIdentities = 1 + constants.concurrency;
for (let i = 0; i < numberOfIdentities; i++) {
// create
const path = pathBuilder(i);
const path = HdPaths.cosmos(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);
const address = codecImplementation().identityToAddress(identity);
console.info(`Created ${role} (${debugPath(path)}): ${address}`);
}
}

View File

@ -130,15 +130,6 @@
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"
@ -159,35 +150,6 @@
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"
@ -1124,11 +1086,6 @@
"@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"