193 lines
6.6 KiB
TypeScript
193 lines
6.6 KiB
TypeScript
import {
|
|
assertIsBroadcastTxSuccess as assertIsBroadcastTxSuccessLaunchpad,
|
|
CosmosClient,
|
|
SigningCosmosClient,
|
|
} from "@cosmjs/launchpad";
|
|
import {
|
|
assertIsBroadcastTxSuccess as assertIsBroadcastTxSuccessStargate,
|
|
SigningStargateClient,
|
|
StargateClient,
|
|
} from "@cosmjs/stargate";
|
|
import { sleep } from "@cosmjs/utils";
|
|
|
|
import * as constants from "./constants";
|
|
import { debugAccount, logAccountsState, logSendJob } from "./debugging";
|
|
import { createClients, createWallets } from "./profile";
|
|
import { TokenConfiguration, TokenManager } from "./tokenmanager";
|
|
import { MinimalAccount, SendJob } from "./types";
|
|
|
|
function isDefined<X>(value: X | undefined): value is X {
|
|
return value !== undefined;
|
|
}
|
|
|
|
export class Faucet {
|
|
public static async make(
|
|
apiUrl: string,
|
|
addressPrefix: string,
|
|
config: TokenConfiguration,
|
|
mnemonic: string,
|
|
numberOfDistributors: number,
|
|
stargate = true,
|
|
logging = false,
|
|
): Promise<Faucet> {
|
|
const wallets = await createWallets(mnemonic, addressPrefix, numberOfDistributors, stargate, logging);
|
|
const clients = await createClients(apiUrl, wallets);
|
|
const readonlyClient = stargate ? await StargateClient.connect(apiUrl) : new CosmosClient(apiUrl);
|
|
return new Faucet(addressPrefix, config, clients, readonlyClient, logging);
|
|
}
|
|
|
|
public readonly addressPrefix: string;
|
|
public readonly holderAddress: string;
|
|
public readonly distributorAddresses: readonly string[];
|
|
|
|
private readonly tokenConfig: TokenConfiguration;
|
|
private readonly tokenManager: TokenManager;
|
|
private readonly readOnlyClient: CosmosClient | StargateClient;
|
|
private readonly clients: { [senderAddress: string]: SigningCosmosClient | SigningStargateClient };
|
|
private readonly logging: boolean;
|
|
private creditCount = 0;
|
|
|
|
private constructor(
|
|
addressPrefix: string,
|
|
config: TokenConfiguration,
|
|
clients: ReadonlyArray<readonly [string, SigningCosmosClient | SigningStargateClient]>,
|
|
readonlyClient: CosmosClient | StargateClient,
|
|
logging = false,
|
|
) {
|
|
this.addressPrefix = addressPrefix;
|
|
this.tokenConfig = config;
|
|
this.tokenManager = new TokenManager(config);
|
|
|
|
this.readOnlyClient = readonlyClient;
|
|
[this.holderAddress, ...this.distributorAddresses] = clients.map(([address]) => address);
|
|
this.clients = clients.reduce(
|
|
(acc, [senderAddress, client]) => ({ ...acc, [senderAddress]: client }),
|
|
{},
|
|
);
|
|
this.logging = logging;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of denoms of tokens owned by the the holder and configured in the faucet
|
|
*/
|
|
public async availableTokens(): Promise<string[]> {
|
|
const { balance } = await this.loadAccount(this.holderAddress);
|
|
return balance
|
|
.filter((b) => b.amount !== "0")
|
|
.map((b) => this.tokenConfig.bankTokens.find((token) => token == b.denom))
|
|
.filter(isDefined);
|
|
}
|
|
|
|
/**
|
|
* Creates and broadcasts a send transaction. Then waits until the transaction is in a block.
|
|
* Throws an error if the transaction failed.
|
|
*/
|
|
public async send(job: SendJob): Promise<void> {
|
|
const client = this.clients[job.sender];
|
|
if (client instanceof SigningCosmosClient) {
|
|
const result = await client.sendTokens(job.recipient, [job.amount], constants.memo);
|
|
return assertIsBroadcastTxSuccessLaunchpad(result);
|
|
}
|
|
const result = await client.sendTokens(job.sender, job.recipient, [job.amount], constants.memo);
|
|
assertIsBroadcastTxSuccessStargate(result);
|
|
}
|
|
|
|
/** Use one of the distributor accounts to send tokens to user */
|
|
public async credit(recipient: string, denom: string): Promise<void> {
|
|
if (this.distributorAddresses.length === 0) throw new Error("No distributor account available");
|
|
const sender = this.distributorAddresses[this.getCreditCount() % this.distributorAddresses.length];
|
|
const job: SendJob = {
|
|
sender: sender,
|
|
recipient: recipient,
|
|
amount: this.tokenManager.creditAmount(denom),
|
|
};
|
|
if (this.logging) logSendJob(job);
|
|
await this.send(job);
|
|
}
|
|
|
|
/** Returns a list to token denoms which are configured */
|
|
public configuredTokens(): string[] {
|
|
return Array.from(this.tokenConfig.bankTokens);
|
|
}
|
|
|
|
public async loadAccount(address: string): Promise<MinimalAccount> {
|
|
const balance =
|
|
this.readOnlyClient instanceof CosmosClient
|
|
? (await this.readOnlyClient.getAccount(address))?.balance ?? []
|
|
: await this.readOnlyClient.getAllBalancesUnverified(address);
|
|
|
|
return {
|
|
address: address,
|
|
balance: balance,
|
|
};
|
|
}
|
|
|
|
public async loadAccounts(): Promise<readonly MinimalAccount[]> {
|
|
const addresses = [this.holderAddress, ...this.distributorAddresses];
|
|
return Promise.all(addresses.map(this.loadAccount.bind(this)));
|
|
}
|
|
|
|
public async refill(): Promise<void> {
|
|
if (this.logging) {
|
|
console.info(`Connected to network: ${await this.readOnlyClient.getChainId()}`);
|
|
console.info(`Tokens on network: ${this.configuredTokens().join(", ")}`);
|
|
}
|
|
|
|
const accounts = await this.loadAccounts();
|
|
if (this.logging) logAccountsState(accounts);
|
|
const [_, ...distributorAccounts] = accounts;
|
|
|
|
const availableTokenDenoms = await this.availableTokens();
|
|
if (this.logging) console.info("Available tokens:", availableTokenDenoms);
|
|
|
|
const jobs: SendJob[] = [];
|
|
for (const denom of availableTokenDenoms) {
|
|
const refillDistibutors = distributorAccounts.filter((account) =>
|
|
this.tokenManager.needsRefill(account, denom),
|
|
);
|
|
|
|
if (this.logging) {
|
|
console.info(`Refilling ${denom} of:`);
|
|
console.info(
|
|
refillDistibutors.length
|
|
? refillDistibutors.map((r) => ` ${debugAccount(r)}`).join("\n")
|
|
: " none",
|
|
);
|
|
}
|
|
for (const refillDistibutor of refillDistibutors) {
|
|
jobs.push({
|
|
sender: this.holderAddress,
|
|
recipient: refillDistibutor.address,
|
|
amount: this.tokenManager.refillAmount(denom),
|
|
});
|
|
}
|
|
}
|
|
if (jobs.length > 0) {
|
|
for (const job of jobs) {
|
|
if (this.logging) logSendJob(job);
|
|
// don't crash faucet when one send fails
|
|
try {
|
|
await this.send(job);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
await sleep(75);
|
|
}
|
|
|
|
if (this.logging) {
|
|
console.info("Done refilling accounts.");
|
|
logAccountsState(await this.loadAccounts());
|
|
}
|
|
} else {
|
|
if (this.logging) {
|
|
console.info("Nothing to be done. Anyways, thanks for checking.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/** returns an integer >= 0 that increments and is unique for this instance */
|
|
private getCreditCount(): number {
|
|
return this.creditCount++;
|
|
}
|
|
}
|