Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored substrate-rpc-tester #3

Merged
merged 2 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/substrate-rpc-tester.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Substrate RPC Tester
on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
validates:
strategy:
matrix:
deno: ["1.41"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ matrix.deno }}

- name: Validate application
run: |
cd substrate-rpc-tester
deno task validate
- name: Build application
run: |
cd substrate-rpc-tester
deno task build
9 changes: 9 additions & 0 deletions substrate-rpc-tester/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Substrate RPC Tester

<div>
<a href="https://www.loom.com/share/4440ff6d68a8437d80890817d5823021">
<p>Substrate RPC Tester 视频讲解 🎥 - Watch Video</p>
</a>
<a href="https://www.loom.com/share/4440ff6d68a8437d80890817d5823021">
<img style="max-width:300px;" src="https://cdn.loom.com/sessions/thumbnails/4440ff6d68a8437d80890817d5823021-with-play.gif">
</a>
</div>

This tool connects to a series of Substrate RPC endpoints and sending a script of transactions to these endpoints.

To run the tester, you have to [install Deno](https://docs.deno.com/runtime/manual/getting_started/installation).
Expand Down
33 changes: 22 additions & 11 deletions substrate-rpc-tester/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import type { AppConfig } from "./types.ts";

const Config: AppConfig = {
endPoint: "ws://127.0.0.1:9944",
// "wss://testnet-rpc1.cess.cloud/ws/",
// "wss://rpc.polkadot.io"
keyring: {
// see: https://github.com/paritytech/ss58-registry/blob/main/ss58-registry.json
type: "sr25519",
ss58Format: 11330,
},
writeTxWait: "none",
endPoints: [
"ws://127.0.0.1:9944",
// "wss://testnet-rpc1.cess.cloud/ws/",
// "wss://rpc.polkadot.io"
],
connections: 5,
writeTxWait: "inblock",
connections: 2,
development: true,
signers: {
// Biden using secret key
Biden: "0x118a67f30b9d10b4efd11fd1c141909dd6f7c79f3586294905177b90bf6463fb",
// Chris using mnemonic and derived path
Chris: "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Chris",
},
txs: [
"api.query.timestamp.now",
{
Expand All @@ -28,15 +33,21 @@ const Config: AppConfig = {
{
// Bob transfers back to Alice
tx: "api.tx.balances.transfer",
params: ["Alice", 12345],
signer: "Bob",
params: ["Biden", 5000000000000],
signer: "Alice",
},
{
// Bob transfers back to Alice
tx: "api.tx.balances.transfer",
params: ["Chris", 2000000000000],
signer: "Biden",
},
{
// Alice adding Bob as proxy
tx: "api.tx.proxy.addProxy",
// (address, Staking type, BlockNumber)
params: ["Bob", "Staking", 16],
signer: "Alice",
params: ["Biden", "Staking", 16],
signer: "Chris",
},
],
};
Expand Down
141 changes: 6 additions & 135 deletions substrate-rpc-tester/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,143 +1,14 @@
import { ApiPromise, WsProvider } from "polkadot-js/api/mod.ts";
import { Keyring } from "polkadot-js/keyring/mod.ts";
import { Mutex, withTimeout } from "async-mutex";
import type { ISubmittableResult } from "polkadot-js/types/types/index.ts";

// Our own implementation
import config from "./config.ts";
import { UserNonces } from "./userNonces.ts";
import * as utils from "./utils.ts";
import type { TimingRecord, Tx } from "./types.ts";

const API_PREFIX = "api";
const keyring = new Keyring(config.keyring);

// For keeping track of user nonce when spitting out txs
// The mutex time out in 5 sec.
const mutex = withTimeout(new Mutex(), 5000, new Error("mutex time out"));
const userNonces = new UserNonces();

const timings: TimingRecord = {};

// deno-lint-ignore no-explicit-any
function getTxCall(api: ApiPromise, txStr: string): any {
const segs = txStr.split(".");
return segs.reduce(
(txCall, seg, idx) => idx === 0 && seg === API_PREFIX ? txCall : txCall[seg],
// deno-lint-ignore no-explicit-any
api as Record<string, any>,
);
}

async function sendTxsToApi(api: ApiPromise, txs: Array<Tx>) {
let txStr;
let lastResult;

for (const tx of txs) {
if (typeof tx === "string") {
txStr = tx;
const txCall = getTxCall(api, tx);
lastResult = await txCall.call(txCall);
} else if (!utils.isWriteOp(tx)) {
// tx is an Object but is a readOp
txStr = tx.tx;
const txCall = getTxCall(api, txStr);
const transformedParams = Array.isArray(tx.params)
? utils.transformParams(keyring, tx.params)
: [];

lastResult = await txCall.call(txCall, ...transformedParams);
} else {
// tx is a writeOp
txStr = tx.tx;
const txCall = getTxCall(api, txStr);
const transformedParams = Array.isArray(tx.params)
? utils.transformParams(keyring, tx.params)
: [];

if (!tx.signer || tx.signer.length === 0) {
throw new Error(`${txStr} writeOp has no signer specified.`);
}

const signer = utils.getSigner(keyring, tx.signer);

// lock the mutex
const release = await mutex.acquire();
const nonce = await userNonces.nextUserNonce(api, signer);

if (!config.writeTxWait || config.writeTxWait === "none") {
const txReceipt = await txCall
.call(txCall, ...transformedParams)
.signAndSend(signer, { nonce });

// release the mutex
release();
lastResult = `txReceipt: ${txReceipt}`;
} else {
lastResult = await new Promise((resolve, reject) => {
let unsub: () => void;
txCall
.call(txCall, ...transformedParams)
.signAndSend(signer, { nonce }, (res: ISubmittableResult) => {
if (config.writeTxWait === "inBlock" && res.isInBlock) {
unsub();
resolve(`inBlock: ${res.status.asInBlock}`);
}
if (config.writeTxWait === "finalized" && res.isFinalized) {
unsub();
resolve(`finalized: ${res.status.asFinalized}`);
}
if (res.isError) {
unsub();
reject(`error: ${res.dispatchError}`);
}
})
.then((us: () => void) => (unsub = us));

release();
});
}
}
lastResult = utils.transformResult(lastResult);
console.log(`${utils.txDisplay(tx)}\n L`, lastResult);
}

return lastResult;
}
import SubstrateRpcTester from "./substrateRpcTester.ts";

async function main() {
const { endPoints, connections, txs } = config;
const connArr: string[] = endPoints.reduce(
(memo, ep) => memo.concat([...Array(connections).keys()].map(() => ep)),
[] as string[],
);

const apiPromises: Promise<ApiPromise>[] = connArr.map((ep) =>
ApiPromise.create({ provider: new WsProvider(ep) })
);

timings["allConnStart"] = performance.now();

const results = await Promise.allSettled(apiPromises);

timings["allConnEnd"] = performance.now();

const apis = results.reduce(
(memo, res, idx) => {
if (res.status === "fulfilled") return memo.concat([res.value]);
console.log(`Connection rejected: ${connArr[idx]}`);
return memo;
},
[] as Array<ApiPromise>,
);

timings["allTxsStart"] = performance.now();

await Promise.all(apis.map((api) => sendTxsToApi(api, txs)));

timings["allTxsEnd"] = performance.now();
const substrateRpcTester = new SubstrateRpcTester(config);
await substrateRpcTester.initialize();
await substrateRpcTester.executeTxs();

utils.displayTimingReport(timings);
substrateRpcTester.displayTxResults();
substrateRpcTester.displayPerformance();
}

main()
Expand Down
Loading