Skip to content

Commit

Permalink
test(wallet): creates a unique wallet for each spec for each run
Browse files Browse the repository at this point in the history
refs #760
  • Loading branch information
ygrishajev committed Mar 10, 2025
1 parent 14ccc49 commit ca23838
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 8 deletions.
5 changes: 3 additions & 2 deletions apps/api/env/.env.functional.test
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
CHAIN_INDEXER_POSTGRES_DB_URI=postgres://postgres:password@localhost:5432/console-akash-sandbox
POSTGRES_DB_URI=postgres://postgres:password@localhost:5432/console-users
MASTER_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony"
UAKT_TOP_UP_MASTER_WALLET_MNEMONIC="since bread kind field rookie stairs elephant tent horror rice gain tongue collect goose rural garment cover client biology toe ability boat afford mind"
USDC_TOP_UP_MASTER_WALLET_MNEMONIC="leaf brush weapon puppy depart hockey walnut hospital orphan require unfair hunt ribbon toe cereal eagle hour door awesome dress mouse when phone return"
NETWORK=sandbox
Expand All @@ -23,4 +22,6 @@ STRIPE_CHECKOUT_REDIRECT_URL=http://localhost:3000
AUTH0_M2M_DOMAIN=AUTH0_M2M_DOMAIN
AUTH0_M2M_SECRET=AUTH0_SECRET
AUTH0_M2M_CLIENT_ID=AUTH0_CLIENT_ID
DEPLOYMENT_ENV=test
DEPLOYMENT_ENV=test
FAUCET_URL=https://faucet.sandbox-01.aksh.pw/faucet
API_NODE_ENDPOINT=https://api.sandbox-01.aksh.pw
4 changes: 3 additions & 1 deletion apps/api/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ module.exports = {
...common,
testMatch: ["<rootDir>/test/functional/**/*.spec.ts"],
setupFilesAfterEnv: ["./test/setup-functional-tests.ts"],
setupFiles: ["./test/setup-functional-env.ts"]
setupFiles: ["./test/setup-functional-env.ts"],
globalSetup: "./test/setup-global-functional.ts",
testEnvironment: "./test/custom-jest-environment.ts"
}
]
};
12 changes: 10 additions & 2 deletions apps/api/src/billing/lib/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ export class Wallet implements OfflineDirectSigner {

private readonly instanceAsPromised: Promise<DirectSecp256k1HdWallet>;

constructor(mnemonic: string, index?: number) {
this.instanceAsPromised = DirectSecp256k1HdWallet.fromMnemonic(mnemonic, this.getInstanceOptions(index));
constructor(mnemonic?: string, index?: number) {
if (typeof mnemonic === "undefined") {
this.instanceAsPromised = DirectSecp256k1HdWallet.generate(24, this.getInstanceOptions(index));
} else {
this.instanceAsPromised = DirectSecp256k1HdWallet.fromMnemonic(mnemonic, this.getInstanceOptions(index));
}
}

private getInstanceOptions(index?: number): Partial<DirectSecp256k1HdWalletOptions> {
Expand All @@ -37,4 +41,8 @@ export class Wallet implements OfflineDirectSigner {
const accounts = await this.getAccounts();
return accounts[0].address;
}

async getMnemonic() {
return (await this.instanceAsPromised).mnemonic;
}
}
19 changes: 19 additions & 0 deletions apps/api/test/custom-jest-environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { EnvironmentContext, JestEnvironmentConfig } from "@jest/environment";
import NodeEnvironment from "jest-environment-node";

import { TestWalletService } from "./services/test-wallet.service";

export default class CustomJestEnvironment extends NodeEnvironment {
private readonly path: string;

constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {
super(config, context);
this.path = context.testPath;
}

async setup() {
await super.setup();
const mnemonic = TestWalletService.instance.getMnemonic(this.path);
this.global.process.env.MASTER_WALLET_MNEMONIC = mnemonic;
}
}
149 changes: 149 additions & 0 deletions apps/api/test/services/test-wallet.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { BalanceHttpService } from "@akashnetwork/http-sdk";
import { coins, EncodeObject } from "@cosmjs/proto-signing";
import { calculateFee, GasPrice, SigningStargateClient } from "@cosmjs/stargate";
import dotenv from "dotenv";
import dotenvExpand from "dotenv-expand";
import * as fs from "fs";
import keyBy from "lodash/keyBy";
import { setTimeout as delay } from "timers/promises";

import { Wallet } from "../../src/billing/lib/wallet/wallet";

const { parsed: config } = dotenvExpand.expand(dotenv.config({ path: "env/.env.functional.test" }));

type TestWalletServiceOptions = {
testsDir: string;
};

type WalletConfig = {
path: string;
mnemonic: string;
message: EncodeObject;
};

const MIN_AMOUNTS: Record<string, number> = {
"create-deployment.spec.ts": 5100000
};

export class TestWalletService {
static get instance() {
return global.testWalletService;
}

static async init(options: TestWalletServiceOptions) {
if (!global.testWalletService) {
global.testWalletService = new TestWalletService(options);
await global.testWalletService.init();
}
return global.testWalletService;
}

private readonly balanceHttpService = new BalanceHttpService({
baseURL: config.API_NODE_ENDPOINT
});

private wallets: Record<string, WalletConfig>;

constructor(private readonly options: TestWalletServiceOptions) {}

getMnemonic(path: string) {
const fileName = this.getFileName(path);
return this.wallets[fileName].mnemonic;
}

async init() {
const { wallet: faucetWallet, amount: faucetAmount } = await this.prepareFaucetWallet();
this.wallets = await this.prepareWallets(faucetWallet, faucetAmount);
}

private async prepareWallets(faucetWallet: Wallet, totalDistibutionAmount: number) {
const specPaths = fs.readdirSync(this.options.testsDir).filter(spec => spec.endsWith(".spec.ts"));
const faucetAddress = await faucetWallet.getFirstAddress();
const totalMinAmount = Object.values(MIN_AMOUNTS).reduce((acc, curr) => acc + curr, 0);
const amount = (totalDistibutionAmount - totalMinAmount - totalDistibutionAmount * 0.01) / specPaths.length;

const configs = await Promise.all(
specPaths.map(async path => {
const wallet = new Wallet();
const address = await wallet.getFirstAddress();
const fileName = this.getFileName(path);

return {
path,
mnemonic: await wallet.getMnemonic(),
address,
message: {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: {
fromAddress: faucetAddress,
toAddress: address,
amount: coins(MIN_AMOUNTS[fileName] || amount, "uakt")
}
}
};
})
);
const messages = Object.values(configs).map(config => config.message) as readonly EncodeObject[];

const client = await SigningStargateClient.connectWithSigner(config.RPC_NODE_ENDPOINT, faucetWallet);
const gasEstimation = await client.simulate(faucetAddress, messages, undefined);
const estimatedGas = Math.round(gasEstimation * 1.5);

const fee = calculateFee(estimatedGas, GasPrice.fromString("0.0025uakt"));

await client.signAndBroadcast(faucetAddress, messages, fee);

this.log("Created and filled wallets");
await Promise.all(
configs.map(async config => {
const balance = await this.balanceHttpService.getBalance(config.address, "uakt");
this.log(`Spec: ${config.path} - Address: ${config.address} - Balance: ${balance?.amount} uAKT`);
})
);

return keyBy(configs, "path");
}

private async prepareFaucetWallet() {
const faucetWallet = new Wallet();
const faucetAddress = await faucetWallet.getFirstAddress();

const initialBalance = await this.balanceHttpService.getBalance(faucetAddress, "uakt");
const initialAmount = initialBalance?.amount;
let updatedAmount = initialAmount;

await this.topUpFaucetWallet(faucetAddress);

while (initialAmount === updatedAmount) {
const updatedBalance = await this.balanceHttpService.getBalance(faucetAddress, "uakt");
updatedAmount = updatedBalance?.amount;
await delay(1000);
}

return {
wallet: faucetWallet,
amount: updatedAmount
};
}

private async topUpFaucetWallet(address: string) {
const times = 1;
for (let i = 0; i < times; i++) {
await fetch(config.FAUCET_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `address=${encodeURIComponent(address)}`
});
}
}

private getFileName(path: string) {
return path.split("/").pop();
}

private log(message: string) {
console.log(message);
}
}
2 changes: 0 additions & 2 deletions apps/api/test/setup-functional-env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import "reflect-metadata";

import dotenv from "dotenv";
import dotenvExpand from "dotenv-expand";

Expand Down
9 changes: 9 additions & 0 deletions apps/api/test/setup-global-functional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import path from "path";

import { TestWalletService } from "./services/test-wallet.service";

export default async () => {
await TestWalletService.init({
testsDir: path.join(__dirname, "functional")
});
};
2 changes: 1 addition & 1 deletion apps/api/test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"@test/*": ["*"]
}
},
"include": ["../src/**/*", "**/*", "../../packages/logging/src/types/pino-fluentd.d.ts"],
"include": ["../src/**/*", "**/*"],
"exclude": ["../node_modules", "../dist"]
}
6 changes: 6 additions & 0 deletions apps/api/test/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TestWalletService } from "../services/test-wallet.service";

declare global {
// eslint-disable-next-line no-var
var testWalletService: TestWalletService;
}

0 comments on commit ca23838

Please sign in to comment.