Skip to content

Commit

Permalink
Refactored substrate-rpc-tester (#3)
Browse files Browse the repository at this point in the history
* Added github workflow (#4)

* Adding loom video demo

* Adding the video demo

* Refactored the code and report can be printed individually

* add github workflow for validations

* push

* Refactored the code (#5)

* updated

* Display result and performance report
  • Loading branch information
jimmychu0807 authored Mar 5, 2024
1 parent b061e8a commit 0f37ced
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 184 deletions.
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

0 comments on commit 0f37ced

Please sign in to comment.