Skip to content

Commit 8faff13

Browse files
committed
rewrite api.ts
I took a closer look at api.ts and decided to "go to town". Here are some loose motivations 1. I didn't like having all the types for the format of the api response. They felt like implementation details and so I stuffed those details away in the functions. 2. I get why we used Single in the public function names, but I don't like it. It's too long. query and queryLive are clear and concise. If we get around to adding a function for batch requests we can name it queryBatch. In this case, the function that is less used and more complicated can have a longer name while the common, simple functions can have a shorter name. 3. At the risk of chopping down Chesterton's Fence, I removed a lot of the exported types. eg export type QuerySingleRawFunction = typeof querySingleRaw I don't think they are pulling their weight in terms of value / cost to maintain in the public, exported interface. 4. Instead of having 2 methods: one for formatted return values and another for 'raw' return values, I decided to have only one kind of function and if you want that function to return typed data then you must provide a formatRow function. Otherwise, we use the column names in the HTTP response to create a generic object to return.
1 parent f6e4b09 commit 8faff13

File tree

5 files changed

+127
-168
lines changed

5 files changed

+127
-168
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A Typescript wrapper for Index Supply's API.
1010
Make a one-time query to get historical data:
1111

1212
```typescript
13-
const { blockNumber, result } = await querySingle({
13+
const { blockNumber, result } = await query({
1414
apiKey: "face",
1515
chainId: 8453,
1616
query: 'select "from", "to", value from transfer',
@@ -30,7 +30,7 @@ console.log(`Block ${blockNumber}:`, result)
3030
Subscribe to new data as it arrives:
3131

3232
```typescript
33-
const liveQuery = querySingleLive({
33+
const liveQuery = queryLive({
3434
apiKey: "face",
3535
chainId: 8453,
3636
blockNumber: 1234567, // Optional: start from specific block

examples/live.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1-
import { querySingleLive } from '../src/index'
1+
import { queryLive } from "../src/index";
22

3-
type Address = `0x${string}`
3+
type Hex = `0x${string}`;
44

5-
const liveQuery = querySingleLive({
6-
chainId: 8453,
7-
eventSignatures: ['Transfer(address indexed from, address indexed to, uint256 v)'],
8-
query: 'select "from", "to", v from transfer limit 20',
9-
formatRow: ([from, to, value]) => ({
10-
from: from as Address,
11-
to: to as Address,
12-
value: BigInt(value),
13-
})
14-
})
5+
type Transfer = {
6+
tx: Hex;
7+
block: bigint;
8+
};
159

16-
for await (const { blockNumber, result } of liveQuery) {
17-
console.log(`New data at block ${blockNumber}:`, result)
10+
const query = queryLive({
11+
chainId: 8453n,
12+
eventSignatures: [
13+
"Transfer(address indexed from, address indexed to, uint256 v)",
14+
],
15+
query: "select tx_hash, block_num from transfer limit 1",
16+
formatRow: ([tx_hash, block_num]) => {
17+
return {
18+
tx: tx_hash as Hex,
19+
block: BigInt(block_num),
20+
};
21+
},
22+
});
23+
24+
for await (const { blockNumber, result } of query) {
25+
console.log(result);
1826
}

examples/single.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
import { querySingle } from '../src/index'
1+
import { query } from "../src/index";
22

3-
type Address = `0x${string}`
3+
type Address = `0x${string}`;
44

5-
const result = await querySingle({
6-
chainId: 8453,
7-
eventSignatures: ['Transfer(address indexed from, address indexed to, uint256 value)'],
8-
query: 'select "from", "to", "value" from transfer limit 5',
9-
formatRow: ([from, to, value]) => ({
10-
from: from as Address,
11-
to: to as Address,
12-
value: BigInt(value),
13-
})
14-
})
5+
const { blockNumber, result } = await query({
6+
chainId: 8453n,
7+
eventSignatures: [
8+
"Transfer(address indexed from, address indexed to, uint256 value)",
9+
],
10+
query:
11+
'select block_num, log_idx, "from", "to", "value" from transfer limit 5',
12+
});
1513

16-
console.log(result)
14+
result.forEach((r) => console.log(r.block_num));

src/api.ts

Lines changed: 91 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,156 +1,122 @@
11
import fetch from "cross-fetch";
2-
import EventSource from 'eventsource-platform-specific';
3-
4-
const apiUrl = 'https://api.indexsupply.net'
5-
6-
type APISchemaType = string[]
7-
type APIQueryRow = string[]
8-
type APIResultType = [] | [APISchemaType, ...APIQueryRow[]]
9-
type APIDataFormat = {
10-
block_height: number
11-
result: APIResultType[]
12-
}
13-
14-
export type SupportedChainId = number
15-
16-
export type QuerySingleRawOptions = {
17-
apiKey?: string,
18-
chainId: SupportedChainId
19-
query: string
20-
eventSignatures: ReadonlyArray<string>
21-
}
22-
23-
export type QuerySingleData<FormattedRow> = {
24-
blockNumber: number
25-
result: FormattedRow[]
26-
}
27-
28-
export type QuerySingleRawFunction = typeof querySingleRaw
29-
30-
export async function querySingleRaw(options: QuerySingleRawOptions): Promise<QuerySingleData<string[]>> {
2+
import EventSource from "eventsource-platform-specific";
3+
4+
export type Response<T> = {
5+
blockNumber: bigint;
6+
result: T[];
7+
};
8+
9+
type JsonValue = ReturnType<typeof JSON.parse>;
10+
type DefaultType = { [key: string]: JsonValue };
11+
type Formatter<T> = (row: JsonValue[]) => T;
12+
13+
export type Request<T> = {
14+
apiUrl?: string;
15+
apiKey?: string;
16+
chainId: bigint;
17+
query: string;
18+
eventSignatures?: ReadonlyArray<string>;
19+
formatRow?: T extends DefaultType ? undefined | Formatter<T> : Formatter<T>;
20+
};
21+
22+
function url<T>(
23+
path: string,
24+
request: Request<T> & { blockNumber?: bigint },
25+
): string {
3126
const params = new URLSearchParams();
32-
params.append("chain", options.chainId.toString());
33-
params.append("query", options.query);
34-
params.append("event_signatures", options.eventSignatures.join(','));
35-
if (options.apiKey) {
36-
params.append("api-key", options.apiKey.toString());
27+
params.append("chain", request.chainId.toString());
28+
params.append("query", request.query);
29+
if (request.eventSignatures) {
30+
params.append("event_signatures", request.eventSignatures.join(","));
3731
}
38-
39-
const response = await fetch(`${apiUrl}/query?${params.toString()}`)
40-
if (response.status !== 200) {
41-
throw new Error(`Invalid API response: Status ${response.status}`)
32+
if (request.apiKey) {
33+
params.append("api-key", request.apiKey.toString());
34+
}
35+
if (request.blockNumber) {
36+
params.append("block_height", request.blockNumber.toString());
4237
}
43-
const data = await response.json() as APIDataFormat;
38+
let apiUrl = "https://api.indexsupply.net";
39+
if (request.apiUrl) {
40+
apiUrl = request.apiUrl;
41+
}
42+
return `${apiUrl}/${path}?${params.toString()}`;
43+
}
4444

45+
const defaultFormatRow = (names: string[]): Formatter<DefaultType> => {
46+
return (row: JsonValue[]) => {
47+
if (row.length !== names.length) {
48+
throw new Error(
49+
`Row length (${row.length}) does not match column names length (${names.length})`,
50+
);
51+
}
52+
return names.reduce((acc, name, index) => {
53+
acc[name] = row[index];
54+
return acc;
55+
}, {} as DefaultType);
56+
};
57+
};
58+
59+
export async function query<T = DefaultType>(
60+
request: Request<T>,
61+
): Promise<Response<T>> {
62+
const resp = await fetch(url("query", request));
63+
if (resp.status !== 200) {
64+
throw new Error(`Invalid API response: Status ${resp.status}`);
65+
}
66+
const data = await resp.json();
4567
if (data.result.length === 0) {
46-
return { blockNumber: data.block_height, result: [] }
68+
return { blockNumber: data.block_height, result: [] };
4769
}
48-
4970
if (data.result.length !== 1) {
50-
throw new Error(`Expected 1 result, got ${data.result.length}`)
71+
throw new Error(`Expected 1 result, got ${data.result.length}`);
5172
}
52-
53-
const result = data.result[0]
54-
55-
if (result.length === 0) {
56-
return { blockNumber: data.block_height, result: [] }
73+
const rows = data.result[0];
74+
if (rows.length === 0) {
75+
return { blockNumber: data.blockHeight, result: [] };
5776
}
58-
77+
const columnNames = rows.shift();
78+
const formatRow = request.formatRow || defaultFormatRow(columnNames);
5979
return {
6080
blockNumber: data.block_height,
61-
result: result.slice(1),
62-
}
63-
}
64-
65-
export type QuerySingleLiveRawFunction = typeof querySingleLiveRaw
66-
67-
export type QuerySingleOptions<FormattedRow> = QuerySingleRawOptions & {
68-
formatRow: (row: string[]) => FormattedRow
81+
result: rows.map(formatRow),
82+
};
6983
}
7084

71-
export async function querySingle<FormattedRow>(
72-
{ formatRow, ...options }: QuerySingleOptions<FormattedRow>
73-
): Promise<QuerySingleData<FormattedRow>> {
74-
const { blockNumber, result } = await querySingleRaw(options)
75-
76-
return {
77-
blockNumber,
78-
result: result.map(formatRow)
79-
}
80-
}
81-
82-
export type QuerySingleLiveRawOptions = {
83-
apiKey?: string
84-
chainId: SupportedChainId
85-
query: string
86-
eventSignatures: ReadonlyArray<string>
87-
blockNumber?: number
88-
}
89-
90-
export async function* querySingleLiveRaw(options: QuerySingleLiveRawOptions): AsyncGenerator<QuerySingleData<string[]>> {
91-
const params = new URLSearchParams();
92-
params.append("chain", options.chainId.toString());
93-
params.append("query", options.query);
94-
params.append("event_signatures", options.eventSignatures.join(','));
95-
if (options.apiKey) {
96-
params.append("api-key", options.apiKey.toString());
97-
}
98-
if (options.blockNumber) {
99-
params.append('block_height', options.blockNumber.toString())
100-
}
101-
const url = new URL(`${apiUrl}/query-live?${params}`)
102-
103-
const eventSource = new EventSource(url.toString())
104-
85+
export async function* queryLive<T = DefaultType>(
86+
request: Request<T> & { blockNumber?: bigint },
87+
): AsyncGenerator<Response<T>> {
88+
const eventSource = new EventSource(url("query-live", request));
10589
try {
10690
while (true) {
10791
const event = await new Promise<MessageEvent>((resolve, reject) => {
10892
eventSource.onmessage = (event) => {
109-
resolve(event)
110-
}
111-
93+
resolve(event);
94+
};
11295
eventSource.onerror = (error) => {
113-
reject(error)
114-
}
115-
})
116-
117-
const data = JSON.parse(event.data) as APIDataFormat
96+
reject(error);
97+
};
98+
});
99+
const data = JSON.parse(event.data);
118100
if (data.result.length === 0) {
119-
yield { blockNumber: data.block_height, result: [] }
120-
continue
101+
yield { blockNumber: data.block_height, result: [] };
102+
continue;
121103
}
122-
123104
if (data.result.length !== 1) {
124-
throw new Error(`Expected 1 result, got ${data.result.length}`)
105+
throw new Error(`Expected 1 result, got ${data.result.length}`);
125106
}
126-
127-
const result = data.result[0]
107+
let result = data.result[0];
128108
if (result.length === 0) {
129-
yield { blockNumber: data.block_height, result: [] }
130-
continue
109+
yield { blockNumber: data.block_height, result: [] };
110+
continue;
131111
}
132-
112+
const columnNames = result.shift();
113+
const formatRow = request.formatRow || defaultFormatRow(columnNames);
133114
yield {
134115
blockNumber: data.block_height,
135-
result: result.slice(1),
136-
}
116+
result: result.map(formatRow),
117+
};
137118
}
138119
} finally {
139-
eventSource.close()
140-
}
141-
}
142-
143-
export type QuerySingleLiveOptions<FormattedRow> = QuerySingleLiveRawOptions & {
144-
formatRow: (row: string[]) => FormattedRow
145-
}
146-
147-
export async function* querySingleLive<FormattedRow>(
148-
{ formatRow, ...options }: QuerySingleLiveOptions<FormattedRow>
149-
): AsyncGenerator<QuerySingleData<FormattedRow>> {
150-
for await (const { blockNumber, result } of querySingleLiveRaw(options)) {
151-
yield {
152-
blockNumber,
153-
result: result.map(formatRow),
154-
}
120+
eventSource.close();
155121
}
156122
}

src/index.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1 @@
1-
export {
2-
type QuerySingleData,
3-
type QuerySingleLiveOptions,
4-
type QuerySingleLiveRawFunction,
5-
type QuerySingleLiveRawOptions,
6-
type QuerySingleOptions,
7-
type QuerySingleRawFunction,
8-
type QuerySingleRawOptions,
9-
type SupportedChainId,
10-
querySingle,
11-
querySingleLive,
12-
querySingleLiveRaw,
13-
querySingleRaw,
14-
} from './api'
1+
export { type Response, type Request, query, queryLive } from "./api";

0 commit comments

Comments
 (0)