Skip to content

Commit 0211adf

Browse files
committed
Adding a new JsonRpcProvider subclass supporting erigon's ots_ (Otterscan) methods
If your node supports the ots_ namespace, you'll have access to 11 new RPC methods: ots_getApiLevel, ots_hasCode, ots_getInternalOperations, ots_getTransactionError, ots_traceTransaction, ots_getBlockDetails, ots_getBlockTransactions, ots_searchTransactionsBefore/After, ots_getTransactionBySenderAndNonce, ots_getContractCreator - Convenience methods: getTransactionRevertReason() (decodes revert data), ensureOts() (making sure you're running an ots_ compatible node), iterateAddressHistory() (async iterator for pagination) - Full TypeScript support: Proper interfaces for all return types (OtsInternalOp, OtsBlockDetails, etc.) - 16 new unit tests covering all functionality
1 parent 9fd9d41 commit 0211adf

File tree

4 files changed

+609
-2
lines changed

4 files changed

+609
-2
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import assert from "assert";
2+
3+
import {
4+
FetchRequest,
5+
OtterscanProvider,
6+
JsonRpcProvider,
7+
type OtsInternalOp,
8+
type OtsBlockDetails,
9+
type OtsBlockTxPage,
10+
type OtsSearchPage,
11+
type OtsContractCreator,
12+
} from "../index.js";
13+
14+
describe("Test Otterscan Provider", function() {
15+
// Mock OTS responses for testing
16+
function createMockOtsProvider() {
17+
const req = new FetchRequest("http://localhost:8545/");
18+
19+
req.getUrlFunc = async (_req, signal) => {
20+
const bodyStr = typeof _req.body === "string" ? _req.body : new TextDecoder().decode(_req.body || new Uint8Array());
21+
const request = JSON.parse(bodyStr || "{}");
22+
23+
let result: any;
24+
25+
switch (request.method) {
26+
case "ots_getApiLevel":
27+
result = 8;
28+
break;
29+
case "ots_hasCode":
30+
// Mock: return true for non-zero addresses
31+
result = request.params[0] !== "0x0000000000000000000000000000000000000000";
32+
break;
33+
case "ots_getInternalOperations":
34+
result = [{
35+
type: 0,
36+
from: "0x1234567890123456789012345678901234567890",
37+
to: "0x0987654321098765432109876543210987654321",
38+
value: "0x1000000000000000000"
39+
}];
40+
break;
41+
case "ots_getTransactionError":
42+
result = "0x";
43+
break;
44+
case "ots_traceTransaction":
45+
result = { calls: [] };
46+
break;
47+
case "ots_getBlockDetails":
48+
result = {
49+
block: {
50+
hash: "0x123abc",
51+
number: "0x1000"
52+
},
53+
transactionCount: 5,
54+
totalFees: "0x5000000000000000"
55+
};
56+
break;
57+
case "ots_getBlockTransactions":
58+
result = {
59+
transactions: [{
60+
hash: "0x456def",
61+
from: "0x1111111111111111111111111111111111111111",
62+
to: "0x2222222222222222222222222222222222222222",
63+
value: "0x1000000000000000000"
64+
}],
65+
receipts: [{
66+
status: "0x1",
67+
gasUsed: "0x5208"
68+
}]
69+
};
70+
break;
71+
case "ots_searchTransactionsBefore":
72+
case "ots_searchTransactionsAfter":
73+
result = {
74+
txs: [{
75+
hash: "0x789ghi",
76+
blockNumber: "0x1000"
77+
}],
78+
receipts: [{
79+
status: "0x1"
80+
}],
81+
firstPage: true,
82+
lastPage: false
83+
};
84+
break;
85+
case "ots_getTransactionBySenderAndNonce":
86+
result = "0xabcdef123456789";
87+
break;
88+
case "ots_getContractCreator":
89+
result = {
90+
hash: "0x987654321",
91+
creator: "0x1111111111111111111111111111111111111111"
92+
};
93+
break;
94+
case "eth_chainId":
95+
result = "0x1";
96+
break;
97+
case "eth_blockNumber":
98+
result = "0x1000";
99+
break;
100+
default:
101+
throw new Error(`Unsupported method: ${request.method}`);
102+
}
103+
104+
const response = {
105+
id: request.id,
106+
jsonrpc: "2.0",
107+
result
108+
};
109+
110+
return {
111+
statusCode: 200,
112+
statusMessage: "OK",
113+
headers: { "content-type": "application/json" },
114+
body: new TextEncoder().encode(JSON.stringify(response))
115+
};
116+
};
117+
118+
return new OtterscanProvider(req, 1, { staticNetwork: true });
119+
}
120+
121+
it("should extend JsonRpcProvider", function() {
122+
const provider = createMockOtsProvider();
123+
assert(provider instanceof OtterscanProvider, "should be OtterscanProvider instance");
124+
assert(provider instanceof JsonRpcProvider, "should extend JsonRpcProvider");
125+
});
126+
127+
it("should get OTS API level", async function() {
128+
const provider = createMockOtsProvider();
129+
const apiLevel = await provider.otsApiLevel();
130+
assert.strictEqual(apiLevel, 8, "should return API level 8");
131+
});
132+
133+
it("should check if address has code", async function() {
134+
const provider = createMockOtsProvider();
135+
136+
const hasCodeTrue = await provider.hasCode("0x1234567890123456789012345678901234567890");
137+
assert.strictEqual(hasCodeTrue, true, "should return true for non-zero address");
138+
139+
const hasCodeFalse = await provider.hasCode("0x0000000000000000000000000000000000000000");
140+
assert.strictEqual(hasCodeFalse, false, "should return false for zero address");
141+
});
142+
143+
it("should get internal operations", async function() {
144+
const provider = createMockOtsProvider();
145+
const internalOps = await provider.getInternalOperations("0x123");
146+
147+
assert(Array.isArray(internalOps), "should return array");
148+
assert.strictEqual(internalOps.length, 1, "should have one operation");
149+
150+
const op = internalOps[0];
151+
assert.strictEqual(op.type, 0, "should have type 0");
152+
assert.strictEqual(op.from, "0x1234567890123456789012345678901234567890", "should have correct from");
153+
assert.strictEqual(op.to, "0x0987654321098765432109876543210987654321", "should have correct to");
154+
assert.strictEqual(op.value, "0x1000000000000000000", "should have correct value");
155+
});
156+
157+
it("should get transaction error data", async function() {
158+
const provider = createMockOtsProvider();
159+
const errorData = await provider.getTransactionErrorData("0x123");
160+
assert.strictEqual(errorData, "0x", "should return empty error data");
161+
});
162+
163+
it("should get transaction revert reason", async function() {
164+
const provider = createMockOtsProvider();
165+
const revertReason = await provider.getTransactionRevertReason("0x123");
166+
assert.strictEqual(revertReason, null, "should return null for no error");
167+
});
168+
169+
it("should trace transaction", async function() {
170+
const provider = createMockOtsProvider();
171+
const trace = await provider.traceTransaction("0x123");
172+
assert(typeof trace === "object", "should return trace object");
173+
assert(Array.isArray(trace.calls), "should have calls array");
174+
});
175+
176+
it("should get block details", async function() {
177+
const provider = createMockOtsProvider();
178+
const blockDetails = await provider.getBlockDetails(4096);
179+
180+
assert(typeof blockDetails === "object", "should return object");
181+
assert.strictEqual(blockDetails.transactionCount, 5, "should have transaction count");
182+
assert.strictEqual(blockDetails.totalFees, "0x5000000000000000", "should have total fees");
183+
assert(blockDetails.block, "should have block data");
184+
});
185+
186+
it("should get block transactions", async function() {
187+
const provider = createMockOtsProvider();
188+
const blockTxs = await provider.getBlockTransactions(4096, 0, 10);
189+
190+
assert(Array.isArray(blockTxs.transactions), "should have transactions array");
191+
assert(Array.isArray(blockTxs.receipts), "should have receipts array");
192+
assert.strictEqual(blockTxs.transactions.length, 1, "should have one transaction");
193+
assert.strictEqual(blockTxs.receipts.length, 1, "should have one receipt");
194+
});
195+
196+
it("should search transactions before", async function() {
197+
const provider = createMockOtsProvider();
198+
const searchResults = await provider.searchTransactionsBefore("0x123", 4096, 10);
199+
200+
assert(Array.isArray(searchResults.txs), "should have txs array");
201+
assert(Array.isArray(searchResults.receipts), "should have receipts array");
202+
assert.strictEqual(searchResults.firstPage, true, "should be first page");
203+
assert.strictEqual(searchResults.lastPage, false, "should not be last page");
204+
});
205+
206+
it("should search transactions after", async function() {
207+
const provider = createMockOtsProvider();
208+
const searchResults = await provider.searchTransactionsAfter("0x123", 4096, 10);
209+
210+
assert(Array.isArray(searchResults.txs), "should have txs array");
211+
assert(Array.isArray(searchResults.receipts), "should have receipts array");
212+
});
213+
214+
it("should get transaction by sender and nonce", async function() {
215+
const provider = createMockOtsProvider();
216+
const txHash = await provider.getTransactionBySenderAndNonce("0x123", 0);
217+
assert.strictEqual(txHash, "0xabcdef123456789", "should return transaction hash");
218+
});
219+
220+
it("should get contract creator", async function() {
221+
const provider = createMockOtsProvider();
222+
const creator = await provider.getContractCreator("0x123");
223+
224+
assert(typeof creator === "object", "should return object");
225+
assert.strictEqual(creator?.hash, "0x987654321", "should have creation hash");
226+
assert.strictEqual(creator?.creator, "0x1111111111111111111111111111111111111111", "should have creator address");
227+
});
228+
229+
it("should ensure OTS capability", async function() {
230+
const provider = createMockOtsProvider();
231+
232+
// Should not throw
233+
await provider.ensureOts(8);
234+
235+
// Should throw for higher requirement
236+
try {
237+
await provider.ensureOts(10);
238+
assert.fail("should have thrown for unsupported API level");
239+
} catch (error: any) {
240+
assert(error.message.includes("ots_getApiLevel"), "should mention API level");
241+
assert.strictEqual(error.code, "OTS_UNAVAILABLE", "should have correct error code");
242+
}
243+
});
244+
245+
it("should have async iterator for address history", function() {
246+
const provider = createMockOtsProvider();
247+
const iterator = provider.iterateAddressHistory("0x123", "before", 4096);
248+
249+
assert(typeof iterator[Symbol.asyncIterator] === "function", "should be async iterable");
250+
});
251+
252+
it("should properly type return values", async function() {
253+
const provider = createMockOtsProvider();
254+
255+
// Test TypeScript typing works correctly
256+
const apiLevel: number = await provider.otsApiLevel();
257+
const hasCode: boolean = await provider.hasCode("0x123");
258+
const internalOps: OtsInternalOp[] = await provider.getInternalOperations("0x123");
259+
const blockDetails: OtsBlockDetails = await provider.getBlockDetails(4096);
260+
const blockTxs: OtsBlockTxPage = await provider.getBlockTransactions(4096, 0, 10);
261+
const searchResults: OtsSearchPage = await provider.searchTransactionsBefore("0x123", 4096, 10);
262+
const creator: OtsContractCreator | null = await provider.getContractCreator("0x123");
263+
264+
// Basic type assertions
265+
assert.strictEqual(typeof apiLevel, "number");
266+
assert.strictEqual(typeof hasCode, "boolean");
267+
assert(Array.isArray(internalOps));
268+
assert(typeof blockDetails === "object");
269+
assert(typeof blockTxs === "object" && Array.isArray(blockTxs.transactions));
270+
assert(typeof searchResults === "object" && Array.isArray(searchResults.txs));
271+
assert(creator === null || typeof creator === "object");
272+
});
273+
});

src.ts/ethers.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export {
6666
AbstractProvider,
6767

6868
FallbackProvider,
69-
JsonRpcApiProvider, JsonRpcProvider, JsonRpcSigner,
69+
JsonRpcApiProvider, JsonRpcProvider, JsonRpcSigner, OtterscanProvider,
7070

7171
BrowserProvider,
7272

@@ -175,7 +175,9 @@ export type {
175175
ContractRunner, DebugEventBrowserProvider, Eip1193Provider,
176176
Eip6963ProviderInfo, EventFilter, Filter, FilterByBlockHash,
177177
GasCostParameters, JsonRpcApiProviderOptions, JsonRpcError,
178-
JsonRpcPayload, JsonRpcResult, JsonRpcTransactionRequest, LogParams,
178+
JsonRpcPayload, JsonRpcResult, JsonRpcTransactionRequest,
179+
Hex, OtsInternalOp, OtsBlockDetails, OtsBlockTxPage, OtsSearchPage, OtsContractCreator,
180+
LogParams,
179181
MinedBlock, MinedTransactionResponse, Networkish, OrphanFilter,
180182
PerformActionFilter, PerformActionRequest, PerformActionTransaction,
181183
PreparedTransactionRequest, ProviderEvent, Subscriber, Subscription,

src.ts/providers/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export {
5757
export { FallbackProvider } from "./provider-fallback.js";
5858
export { JsonRpcApiProvider, JsonRpcProvider, JsonRpcSigner } from "./provider-jsonrpc.js"
5959

60+
export { OtterscanProvider } from "./provider-otterscan.js";
61+
6062
export { BrowserProvider } from "./provider-browser.js";
6163

6264
export { AlchemyProvider } from "./provider-alchemy.js";
@@ -127,6 +129,15 @@ export type {
127129
JsonRpcTransactionRequest,
128130
} from "./provider-jsonrpc.js";
129131

132+
export type {
133+
Hex,
134+
OtsInternalOp,
135+
OtsBlockDetails,
136+
OtsBlockTxPage,
137+
OtsSearchPage,
138+
OtsContractCreator
139+
} from "./provider-otterscan.js";
140+
130141
export type {
131142
WebSocketCreator, WebSocketLike
132143
} from "./provider-websocket.js";

0 commit comments

Comments
 (0)