Skip to content
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
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ jobs:
- uses: actions/checkout@v6

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v6
with:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ jobs:
- uses: actions/checkout@v6

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v6
with:
Expand Down
5 changes: 1 addition & 4 deletions packages/adapters-langchain/src/verify.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { buildVerificationLinks } from "@openscan/utils";
import type { VerifyLinkParams } from "@openscan/utils";

export function injectVerificationLinks(
data: unknown,
params: VerifyLinkParams,
): unknown {
export function injectVerificationLinks(data: unknown, params: VerifyLinkParams): unknown {
if (data && typeof data === "object") {
const links = buildVerificationLinks(params);
if (links.length > 0) {
Expand Down
28 changes: 17 additions & 11 deletions packages/algorithms/src/tx-history/TransactionHistoryAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ interface AddressState {
balance: bigint;
}

// biome-ignore lint/suspicious/noExplicitAny: RPC client from dynamic peer dep import
type RpcClient = {
execute: <T>(method: string, params: any[]) => Promise<{ success: boolean; data?: T | null }>;
execute: <T>(method: string, params: unknown[]) => Promise<{ success: boolean; data?: T | null }>;
close: () => Promise<void>;
};

Expand Down Expand Up @@ -94,13 +93,20 @@ export class TransactionHistoryAlgorithm implements Algorithm<TxHistoryParams, T
this.balanceCache.clear();
}

private getClient(): RpcClient {
if (!this.client) {
throw new Error("Client not initialized. Call initClient() or use execute().");
}
return this.client;
}

// -- State queries --

private async getNonce(address: string, block: number): Promise<number> {
const key = `${address}:${block}`;
const cached = this.nonceCache.get(key);
if (cached !== undefined) return cached;
const client = this.client!;
const client = this.getClient();
const result = await client.execute<string>("eth_getTransactionCount", [address, toHex(block)]);
this.rpcCalls++;
const nonce = Number(hexToNumber(result.data || "0x0"));
Expand All @@ -112,7 +118,7 @@ export class TransactionHistoryAlgorithm implements Algorithm<TxHistoryParams, T
const key = `${address}:${block}`;
const cached = this.balanceCache.get(key);
if (cached !== undefined) return cached;
const client = this.client!;
const client = this.getClient();
const result = await client.execute<string>("eth_getBalance", [address, toHex(block)]);
this.rpcCalls++;
const balance = BigInt(result.data || "0x0");
Expand Down Expand Up @@ -193,7 +199,7 @@ export class TransactionHistoryAlgorithm implements Algorithm<TxHistoryParams, T
signal?: AbortSignal,
): Promise<{ fromBlock: number; toBlock: number } | null> {
if (!latestBlock) {
const result = await this.client!.execute<string>("eth_blockNumber", []);
const result = await this.getClient().execute<string>("eth_blockNumber", []);
this.rpcCalls++;
if (!result.data) return null;
latestBlock = Number(hexToNumber(result.data));
Expand Down Expand Up @@ -232,7 +238,7 @@ export class TransactionHistoryAlgorithm implements Algorithm<TxHistoryParams, T
async getTransactionRange(
address: string,
): Promise<{ startBlock: number; endBlock: number; totalSent: number } | null> {
const client = this.client!;
const client = this.getClient();
const blockResult = await client.execute<string>("eth_blockNumber", []);
this.rpcCalls++;
if (!blockResult.data) return null;
Expand Down Expand Up @@ -373,7 +379,7 @@ export class TransactionHistoryAlgorithm implements Algorithm<TxHistoryParams, T
// -- Block transaction fetching --

private async fetchBlockReceipts(blockNum: number): Promise<Map<string, EthTransactionReceipt>> {
const client = this.client!;
const client = this.getClient();
const receipts = new Map<string, EthTransactionReceipt>();
try {
const result = await client.execute<EthTransactionReceipt[]>("eth_getBlockReceipts", [
Expand All @@ -397,7 +403,7 @@ export class TransactionHistoryAlgorithm implements Algorithm<TxHistoryParams, T
private async fetchIndividualReceipts(
hashes: string[],
): Promise<Map<string, EthTransactionReceipt>> {
const client = this.client!;
const client = this.getClient();
const receipts = new Map<string, EthTransactionReceipt>();
const tasks = hashes.map((hash) => async () => {
const result = await client.execute<EthTransactionReceipt>("eth_getTransactionReceipt", [
Expand All @@ -416,7 +422,7 @@ export class TransactionHistoryAlgorithm implements Algorithm<TxHistoryParams, T
signal?: AbortSignal,
): Promise<TxHistoryEntry[]> {
if (signal?.aborted) return [];
const client = this.client!;
const client = this.getClient();
const blockResult = await client.execute<EthBlock>("eth_getBlockByNumber", [
toHex(blockNum),
true,
Expand Down Expand Up @@ -559,9 +565,9 @@ export class TransactionHistoryAlgorithm implements Algorithm<TxHistoryParams, T
const normalizedAddress = address.toLowerCase();

// Resolve latest block
const blockResult = await this.client!.execute<string>("eth_blockNumber", []);
const blockResult = await this.client?.execute<string>("eth_blockNumber", []);
this.rpcCalls++;
if (!blockResult.data) {
if (!blockResult?.data) {
return {
blocks: [],
entries: [],
Expand Down
1 change: 0 additions & 1 deletion packages/algorithms/tests/gas-price.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,6 @@ describe("GasPriceHistoryAlgorithm", () => {

it("falls back to 0x0 when baseFeePerGas is absent (pre-EIP-1559)", async () => {
const header = blockHeader(99);
// biome-ignore lint/performance/noDelete: test needs to remove property
delete (header as Record<string, unknown>).baseFeePerGas;
const headers = new Map([[99, header]]);
setupMockRpc(100, headers as Map<number, ReturnType<typeof blockHeader>>);
Expand Down
34 changes: 17 additions & 17 deletions packages/algorithms/tests/tx-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ function defaultParams(overrides?: Record<string, unknown>) {
* Nonce goes 0→1 at txBlock. Balance stays constant at 1 ETH.
*/
function setupSingleSentTx(currentBlock: number, txBlock: number) {
const txHash = "0x" + "ab".repeat(32);
const txHash = `0x${"ab".repeat(32)}`;
const tx = buildTx({ hash: txHash, blockNumber: txBlock, from: TEST_ADDRESS, to: OTHER_ADDRESS });
const receipt = buildReceipt(txHash);

Expand Down Expand Up @@ -232,7 +232,7 @@ function setupSingleSentTx(currentBlock: number, txBlock: number) {
* Nonce stays 0. Balance goes 0→1 ETH at rxBlock.
*/
function setupSingleReceivedTx(currentBlock: number, rxBlock: number) {
const txHash = "0x" + "cd".repeat(32);
const txHash = `0x${"cd".repeat(32)}`;
const tx = buildTx({ hash: txHash, blockNumber: rxBlock, from: OTHER_ADDRESS, to: TEST_ADDRESS });
const receipt = buildReceipt(txHash);

Expand Down Expand Up @@ -496,7 +496,7 @@ describe("TransactionHistoryAlgorithm", () => {
});

it("classifies tx as 'internal' when address in tx.input", async () => {
const txHash = "0x" + "ee".repeat(32);
const txHash = `0x${"ee".repeat(32)}`;
const strippedAddress = TEST_ADDRESS.replace("0x", "");
const tx = buildTx({
hash: txHash,
Expand Down Expand Up @@ -534,7 +534,7 @@ describe("TransactionHistoryAlgorithm", () => {
});

it("classifies tx as 'internal' when address in receipt log topics", async () => {
const txHash = "0x" + "ff".repeat(32);
const txHash = `0x${"ff".repeat(32)}`;
const strippedAddress = TEST_ADDRESS.replace("0x", "");
const paddedAddress = `0x000000000000000000000000${strippedAddress}`;
const tx = buildTx({
Expand Down Expand Up @@ -574,7 +574,7 @@ describe("TransactionHistoryAlgorithm", () => {
});

it("classifies tx as 'internal' when address in receipt log data", async () => {
const txHash = "0x" + "dd".repeat(32);
const txHash = `0x${"dd".repeat(32)}`;
const strippedAddress = TEST_ADDRESS.replace("0x", "");
const tx = buildTx({
hash: txHash,
Expand Down Expand Up @@ -613,7 +613,7 @@ describe("TransactionHistoryAlgorithm", () => {
});

it("extracts methodId from input data >= 10 chars", async () => {
const txHash = "0x" + "ab".repeat(32);
const txHash = `0x${"ab".repeat(32)}`;
const tx = buildTx({
hash: txHash,
blockNumber: 950,
Expand Down Expand Up @@ -668,7 +668,7 @@ describe("TransactionHistoryAlgorithm", () => {
});

it("maps non-0x1 status to 'failure'", async () => {
const txHash = "0x" + "ab".repeat(32);
const txHash = `0x${"ab".repeat(32)}`;
const tx = buildTx({ hash: txHash, blockNumber: 950, from: TEST_ADDRESS, to: OTHER_ADDRESS });
const receipt = buildReceipt(txHash, { status: "0x0" });

Expand Down Expand Up @@ -697,7 +697,7 @@ describe("TransactionHistoryAlgorithm", () => {
});

it("handles null 'to' field (contract creation)", async () => {
const txHash = "0x" + "ab".repeat(32);
const txHash = `0x${"ab".repeat(32)}`;
const tx = buildTx({ hash: txHash, blockNumber: 950, from: TEST_ADDRESS, to: null });
const receipt = buildReceipt(txHash);

Expand Down Expand Up @@ -746,7 +746,7 @@ describe("TransactionHistoryAlgorithm", () => {

describe("receipt fetching", () => {
it("falls back to individual receipts when eth_getBlockReceipts fails", async () => {
const txHash = "0x" + "ab".repeat(32);
const txHash = `0x${"ab".repeat(32)}`;
const tx = buildTx({ hash: txHash, blockNumber: 950, from: TEST_ADDRESS, to: OTHER_ADDRESS });
const receipt = buildReceipt(txHash);

Expand Down Expand Up @@ -786,8 +786,8 @@ describe("TransactionHistoryAlgorithm", () => {

describe("multiple transactions", () => {
it("finds transactions across multiple blocks", async () => {
const txHash1 = "0x" + "a1".repeat(32);
const txHash2 = "0x" + "a2".repeat(32);
const txHash1 = `0x${"a1".repeat(32)}`;
const txHash2 = `0x${"a2".repeat(32)}`;
const tx1 = buildTx({
hash: txHash1,
blockNumber: 900,
Expand Down Expand Up @@ -833,8 +833,8 @@ describe("TransactionHistoryAlgorithm", () => {
});

it("sorts entries by blockNumber descending", async () => {
const txHash1 = "0x" + "a1".repeat(32);
const txHash2 = "0x" + "a2".repeat(32);
const txHash1 = `0x${"a1".repeat(32)}`;
const txHash2 = `0x${"a2".repeat(32)}`;
const tx1 = buildTx({
hash: txHash1,
blockNumber: 900,
Expand Down Expand Up @@ -880,7 +880,7 @@ describe("TransactionHistoryAlgorithm", () => {

describe("searchAddressActivity via initClient", () => {
it("returns stats with correct counts", async () => {
const txHash = "0x" + "ab".repeat(32);
const txHash = `0x${"ab".repeat(32)}`;
const tx = buildTx({ hash: txHash, blockNumber: 950, from: TEST_ADDRESS, to: OTHER_ADDRESS });
const receipt = buildReceipt(txHash);

Expand Down Expand Up @@ -961,9 +961,9 @@ describe("TransactionHistoryAlgorithm", () => {

it("respects limit option", async () => {
// Set up 3 transactions at different blocks
const txHash1 = "0x" + "a1".repeat(32);
const txHash2 = "0x" + "a2".repeat(32);
const txHash3 = "0x" + "a3".repeat(32);
const txHash1 = `0x${"a1".repeat(32)}`;
const txHash2 = `0x${"a2".repeat(32)}`;
const txHash3 = `0x${"a3".repeat(32)}`;
const tx1 = buildTx({
hash: txHash1,
blockNumber: 800,
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ const main = defineCommand({
const result = await registry.execute(commandName, commandArgs, ctx);

if (result.data && typeof result.data === "object") {
const address = (commandArgs.address ??
(result.data as Record<string, unknown>).address) as string | undefined;
const address = (commandArgs.address ?? (result.data as Record<string, unknown>).address) as
| string
| undefined;
const links = buildVerificationLinks({ chainId: ctx.chainId, address });
if (links.length > 0) {
(result.data as Record<string, unknown>).verificationLinks = links;
Expand Down
31 changes: 14 additions & 17 deletions packages/utils/tests/signatures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import { parseSignature, formatSignature } from "../src/signatures/index.js";
const knownR = "0xd693b532a80fed6392b428604171fb32fdbf953728a3a7ecc7d4062b1652c042";
const knownS = "0x24e9c602ac800b983b035700a14b23f78a253ab762deab5dc27e3555a750b354";
const knownV = 27; // 0x1b
const knownSigHex =
"0x" +
"d693b532a80fed6392b428604171fb32fdbf953728a3a7ecc7d4062b1652c042" +
"24e9c602ac800b983b035700a14b23f78a253ab762deab5dc27e3555a750b354" +
"1b";
const knownSigHex = `0x${"d693b532a80fed6392b428604171fb32fdbf953728a3a7ecc7d4062b1652c042"}${"24e9c602ac800b983b035700a14b23f78a253ab762deab5dc27e3555a750b354"}1b`;
"24e9c602ac800b983b035700a14b23f78a253ab762deab5dc27e3555a750b354" + "1b";

describe("parseSignature", () => {
it("parses a valid signature with 0x prefix", () => {
Expand All @@ -28,19 +25,19 @@ describe("parseSignature", () => {
});

it("parses v=28 (0x1c)", () => {
const sig = "0x" + "a".repeat(64) + "b".repeat(64) + "1c";
const sig = `0x${"a".repeat(64)}${"b".repeat(64)}1c`;
const result = parseSignature(sig);
assert.equal(result.v, 28);
});

it("parses v=0 (0x00)", () => {
const sig = "0x" + "a".repeat(64) + "b".repeat(64) + "00";
const sig = `0x${"a".repeat(64)}${"b".repeat(64)}00`;
const result = parseSignature(sig);
assert.equal(result.v, 0);
});

it("parses v=1 (0x01)", () => {
const sig = "0x" + "a".repeat(64) + "b".repeat(64) + "01";
const sig = `0x${"a".repeat(64)}${"b".repeat(64)}01`;
const result = parseSignature(sig);
assert.equal(result.v, 1);
});
Expand All @@ -50,7 +47,7 @@ describe("parseSignature", () => {
});

it("throws for too long signature", () => {
assert.throws(() => parseSignature("0x" + "ab".repeat(66)), /Invalid signature length/);
assert.throws(() => parseSignature(`0x${"ab".repeat(66)}`), /Invalid signature length/);
});

it("throws for empty input", () => {
Expand All @@ -74,7 +71,7 @@ describe("formatSignature", () => {
});

it("pads short r value", () => {
const result = formatSignature("0xff", "0x" + "bb".repeat(32), 0);
const result = formatSignature("0xff", `0x${"bb".repeat(32)}`, 0);
// r should be padded to 64 chars
assert.equal(result.length, 2 + 130); // 0x + 64 + 64 + 2
assert.ok(
Expand All @@ -83,28 +80,28 @@ describe("formatSignature", () => {
});

it("pads short s value", () => {
const result = formatSignature("0x" + "aa".repeat(32), "0xff", 0);
const result = formatSignature(`0x${"aa".repeat(32)}`, "0xff", 0);
const s = result.slice(2 + 64, 2 + 128);
assert.equal(s, "00000000000000000000000000000000000000000000000000000000000000ff");
});

it("formats v=0", () => {
const result = formatSignature("0x" + "aa".repeat(32), "0x" + "bb".repeat(32), 0);
const result = formatSignature(`0x${"aa".repeat(32)}`, `0x${"bb".repeat(32)}`, 0);
assert.ok(result.endsWith("00"));
});

it("formats v=1", () => {
const result = formatSignature("0x" + "aa".repeat(32), "0x" + "bb".repeat(32), 1);
const result = formatSignature(`0x${"aa".repeat(32)}`, `0x${"bb".repeat(32)}`, 1);
assert.ok(result.endsWith("01"));
});

it("formats v=27 (0x1b)", () => {
const result = formatSignature("0x" + "aa".repeat(32), "0x" + "bb".repeat(32), 27);
const result = formatSignature(`0x${"aa".repeat(32)}`, `0x${"bb".repeat(32)}`, 27);
assert.ok(result.endsWith("1b"));
});

it("formats v=28 (0x1c)", () => {
const result = formatSignature("0x" + "aa".repeat(32), "0x" + "bb".repeat(32), 28);
const result = formatSignature(`0x${"aa".repeat(32)}`, `0x${"bb".repeat(32)}`, 28);
assert.ok(result.endsWith("1c"));
});
});
Expand All @@ -117,14 +114,14 @@ describe("parseSignature / formatSignature round-trip", () => {
});

it("round-trips a v=0 signature", () => {
const sig = "0x" + "ab".repeat(32) + "cd".repeat(32) + "00";
const sig = `0x${"ab".repeat(32)}${"cd".repeat(32)}00`;
const parsed = parseSignature(sig);
const formatted = formatSignature(parsed.r, parsed.s, parsed.v);
assert.equal(formatted, sig);
});

it("round-trips a v=28 signature", () => {
const sig = "0x" + "12".repeat(32) + "34".repeat(32) + "1c";
const sig = `0x${"12".repeat(32)}${"34".repeat(32)}1c`;
const parsed = parseSignature(sig);
const formatted = formatSignature(parsed.r, parsed.s, parsed.v);
assert.equal(formatted, sig);
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/tests/tx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe("decodeTxInput", () => {
});

it("returns methodId only for empty ABI array", () => {
const data = "0xa9059cbb" + "00".repeat(64);
const data = `0xa9059cbb${"00".repeat(64)}`;
const result = decodeTxInput(data, []);
assert.equal(result.methodId, "0xa9059cbb");
assert.equal(result.params.length, 0);
Expand All @@ -94,7 +94,7 @@ describe("decodeTxInput", () => {
selector: "0xa9059cbb",
};

const data = "0xa9059cbb" + "00".repeat(64);
const data = `0xa9059cbb${"00".repeat(64)}`;
const result = decodeTxInput(data, [eventAbi]);
assert.equal(result.methodName, undefined);
});
Expand Down
Loading