@apibara/evm 2.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # `@apibara/evm`
2
+
3
+ This package provides a gRPC client to interact with an [Apibara streaming server](https://www.apibara.com/docs/streaming-protocol).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm add @apibara/evm
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ## License
14
+
15
+ Copyright 2024 GNC Labs Limited
16
+
17
+ Licensed under the Apache License, Version 2.0 (the "License");
18
+ you may not use this file except in compliance with the License.
19
+ You may obtain a copy of the License at
20
+
21
+ http://www.apache.org/licenses/LICENSE-2.0
22
+
23
+ Unless required by applicable law or agreed to in writing, software
24
+ distributed under the License is distributed on an "AS IS" BASIS,
25
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
+ See the License for the specific language governing permissions and
27
+ limitations under the License.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@apibara/evm",
3
+ "version": "2.0.0-beta.3",
4
+ "type": "module",
5
+ "source": "./src/index.ts",
6
+ "main": "./dist/index.mjs",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.cjs",
12
+ "default": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "publishConfig": {},
16
+ "scripts": {
17
+ "build": "pnpm build:proto && unbuild",
18
+ "build:proto": "buf generate proto",
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "vitest",
21
+ "test:ci": "vitest run",
22
+ "lint": "biome check .",
23
+ "lint:fix": "pnpm lint --write",
24
+ "format": "biome format . --write"
25
+ },
26
+ "devDependencies": {
27
+ "@bufbuild/buf": "^1.32.1",
28
+ "@types/long": "^5.0.0",
29
+ "@types/node": "^20.12.12",
30
+ "unbuild": "^2.0.0",
31
+ "vitest": "^1.6.0"
32
+ },
33
+ "dependencies": {
34
+ "@apibara/protocol": "2.0.0-beta.3",
35
+ "@effect/schema": "^0.67.15",
36
+ "effect": "^3.2.6",
37
+ "long": "^5.2.1",
38
+ "nice-grpc-common": "^2.0.2",
39
+ "protobufjs": "^7.1.2",
40
+ "viem": "^2.12.4"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "src",
45
+ "README.md"
46
+ ],
47
+ "types": "./dist/index.d.ts"
48
+ }
package/src/block.ts ADDED
@@ -0,0 +1,173 @@
1
+ import { Bytes, BytesFromUint8Array } from "@apibara/protocol";
2
+ import { Schema } from "@effect/schema";
3
+
4
+ import { Address, B256, U128, U256 } from "./common";
5
+ import * as proto from "./proto";
6
+
7
+ export const Bloom = Schema.transform(
8
+ Schema.Struct({
9
+ value: BytesFromUint8Array,
10
+ }),
11
+ Bytes,
12
+ {
13
+ strict: false,
14
+ encode(value) {
15
+ throw new Error("Not implemented");
16
+ },
17
+ decode({ value }) {
18
+ return value;
19
+ },
20
+ },
21
+ );
22
+
23
+ export const TransactionStatus = Schema.transform(
24
+ Schema.Enums(proto.data.TransactionStatus),
25
+ Schema.Literal("unknown", "succeeded", "reverted"),
26
+ {
27
+ decode(value) {
28
+ const enumMap = {
29
+ [proto.data.TransactionStatus.SUCCEEDED]: "succeeded",
30
+ [proto.data.TransactionStatus.REVERTED]: "reverted",
31
+ [proto.data.TransactionStatus.UNSPECIFIED]: "unknown",
32
+ [proto.data.TransactionStatus.UNRECOGNIZED]: "unknown",
33
+ } as const;
34
+
35
+ return enumMap[value] ?? "unknown";
36
+ },
37
+ encode(value) {
38
+ throw new Error("encode: not implemented");
39
+ },
40
+ },
41
+ );
42
+
43
+ export type TransactionStatus = typeof TransactionStatus.Type;
44
+
45
+ export const BlockHeader = Schema.Struct({
46
+ blockNumber: Schema.BigIntFromSelf,
47
+ blockHash: Schema.optional(B256),
48
+ parentBlockHash: Schema.optional(B256),
49
+ unclesHash: Schema.optional(B256),
50
+ miner: Schema.optional(Address),
51
+ stateRoot: Schema.optional(B256),
52
+ transactionsRoot: Schema.optional(B256),
53
+ receiptsRoot: Schema.optional(B256),
54
+ logsBloom: Schema.optional(Bloom),
55
+ difficulty: Schema.optional(U256),
56
+ gasLimit: Schema.optional(U128),
57
+ gasUsed: Schema.optional(U128),
58
+ timestamp: Schema.optional(Schema.DateFromSelf),
59
+ extraData: BytesFromUint8Array,
60
+ mixHash: Schema.optional(B256),
61
+ nonce: Schema.optional(Schema.BigIntFromSelf),
62
+ baseFeePerGas: Schema.optional(U128),
63
+ withdrawalsRoot: Schema.optional(B256),
64
+ totalDifficulty: Schema.optional(U256),
65
+ blobGasUsed: Schema.optional(U128),
66
+ excessBlobGas: Schema.optional(U128),
67
+ parentBeaconBlockRoot: Schema.optional(B256),
68
+ });
69
+
70
+ export type BlockHeader = typeof BlockHeader.Type;
71
+
72
+ export const Withdrawal = Schema.Struct({
73
+ filterIds: Schema.optional(Schema.Array(Schema.Number)),
74
+ withdrawalIndex: Schema.Number,
75
+ index: Schema.BigIntFromSelf,
76
+ validatorIndex: Schema.Number,
77
+ address: Schema.optional(Address),
78
+ amount: Schema.optional(Schema.BigIntFromSelf),
79
+ });
80
+
81
+ export const AccessListItem = Schema.Struct({
82
+ address: Schema.optional(Address),
83
+ storageKeys: Schema.Array(B256),
84
+ });
85
+
86
+ export const Signature = Schema.Struct({
87
+ r: Schema.optional(U256),
88
+ s: Schema.optional(U256),
89
+ v: Schema.optional(U256),
90
+ YParity: Schema.optional(Schema.Boolean),
91
+ });
92
+
93
+ export const Transaction = Schema.Struct({
94
+ filterIds: Schema.optional(Schema.Array(Schema.Number)),
95
+ transactionIndex: Schema.Number,
96
+ transactionHash: Schema.optional(B256),
97
+ nonce: Schema.optional(Schema.BigIntFromSelf),
98
+ from: Schema.optional(Address),
99
+ to: Schema.optional(Address),
100
+ value: Schema.optional(U256),
101
+ gasPrice: Schema.optional(U128),
102
+ gas: Schema.optional(U128),
103
+ maxFeePerGas: Schema.optional(U128),
104
+ maxPriorityFeePerGas: Schema.optional(U128),
105
+ input: BytesFromUint8Array,
106
+ signature: Schema.optional(Signature),
107
+ chainId: Schema.optional(Schema.BigIntFromSelf),
108
+ accessList: Schema.Array(AccessListItem),
109
+ transactionType: Schema.optional(Schema.BigIntFromSelf),
110
+ maxFeePerBlobGas: Schema.optional(U128),
111
+ blobVersionedHashes: Schema.Array(B256),
112
+ transactionStatus: Schema.optional(TransactionStatus),
113
+ });
114
+
115
+ export const TransactionReceipt = Schema.Struct({
116
+ filterIds: Schema.optional(Schema.Array(Schema.Number)),
117
+ transactionIndex: Schema.optional(Schema.Number),
118
+ transactionHash: Schema.optional(B256),
119
+ cumulativeGasUsed: Schema.optional(U128),
120
+ gasUsed: Schema.optional(U128),
121
+ effectiveGasPrice: Schema.optional(U128),
122
+ from: Schema.optional(Address),
123
+ to: Schema.optional(Address),
124
+ contractAddress: Schema.optional(Address),
125
+ logsBloom: Schema.optional(Bloom),
126
+ transactionType: Schema.optional(Schema.BigIntFromSelf),
127
+ blobGasUsed: Schema.optional(U128),
128
+ blobGasPrice: Schema.optional(U128),
129
+ transactionStatus: Schema.optional(TransactionStatus),
130
+ });
131
+
132
+ export const Log = Schema.Struct({
133
+ filterIds: Schema.optional(Schema.Array(Schema.Number)),
134
+ address: Schema.optional(Address),
135
+ topics: Schema.Array(B256),
136
+ data: BytesFromUint8Array,
137
+ logIndex: Schema.Number,
138
+ transactionIndex: Schema.Number,
139
+ transactionHash: Schema.optional(B256),
140
+ transactionStatus: Schema.optional(TransactionStatus),
141
+ });
142
+
143
+ export type Log = typeof Log.Type;
144
+
145
+ export const Block = Schema.Struct({
146
+ header: Schema.optional(BlockHeader),
147
+ withdrawals: Schema.Array(Withdrawal),
148
+ transactions: Schema.Array(Transaction),
149
+ receipts: Schema.Array(TransactionReceipt),
150
+ logs: Schema.Array(Log),
151
+ });
152
+
153
+ export type Block = typeof Block.Type;
154
+
155
+ export const BlockFromBytes = Schema.transform(
156
+ Schema.Uint8ArrayFromSelf,
157
+ Schema.NullOr(Block),
158
+ {
159
+ strict: false,
160
+ decode(value) {
161
+ if (value.length === 0) {
162
+ return null;
163
+ }
164
+ return proto.data.Block.decode(value);
165
+ },
166
+ encode(value) {
167
+ if (value === null) {
168
+ return new Uint8Array();
169
+ }
170
+ return proto.data.Block.encode(value).finish();
171
+ },
172
+ },
173
+ );
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { Schema } from "@effect/schema";
4
+ import { pad } from "viem";
5
+
6
+ import { Address, B256, U128, U256 } from "./common";
7
+
8
+ describe("Address", () => {
9
+ const encode = Schema.encodeSync(Address);
10
+ const decode = Schema.decodeSync(Address);
11
+
12
+ it("should convert to and from proto", () => {
13
+ const address = "0x27504265a9bc4330e3fe82061a60cd8b6369b4dc";
14
+
15
+ const message = encode(address);
16
+
17
+ expect(message.x0).toBeDefined();
18
+ expect(message.x1).toBeDefined();
19
+ expect(message.x2).toBeDefined();
20
+
21
+ const back = decode(message);
22
+ expect(back).toEqual(pad(address, { size: 20 }));
23
+ });
24
+ });
25
+
26
+ describe("B256", () => {
27
+ const encode = Schema.encodeSync(B256);
28
+ const decode = Schema.decodeSync(B256);
29
+
30
+ it("should convert to and from proto", () => {
31
+ const value =
32
+ "0x9df92d765b5aa041fd4bbe8d5878eb89290efa78e444c1a603eecfae2ea05fa4";
33
+ const message = encode(value);
34
+
35
+ expect(message.x0).toBeDefined();
36
+ expect(message.x1).toBeDefined();
37
+ expect(message.x2).toBeDefined();
38
+ expect(message.x3).toBeDefined();
39
+
40
+ const back = decode(message);
41
+ expect(back).toEqual(pad(value, { size: 32 }));
42
+ });
43
+ });
44
+
45
+ describe("U256", () => {
46
+ const encode = Schema.encodeSync(U256);
47
+ const decode = Schema.decodeSync(U256);
48
+
49
+ it("should convert to and from proto", () => {
50
+ const value = BigInt(
51
+ "0x9df92d765b5aa041fd4bbe8d5878eb89290efa78e444c1a603eecfae2ea05fa4",
52
+ );
53
+ const message = encode(value);
54
+
55
+ expect(message.x0).toBeDefined();
56
+ expect(message.x1).toBeDefined();
57
+ expect(message.x2).toBeDefined();
58
+ expect(message.x3).toBeDefined();
59
+
60
+ const back = decode(message);
61
+ expect(back).toEqual(value);
62
+ });
63
+ });
64
+
65
+ describe("U128", () => {
66
+ const encode = Schema.encodeSync(U128);
67
+ const decode = Schema.decodeSync(U128);
68
+
69
+ it("should convert to and from proto", () => {
70
+ const value = BigInt("0x090efa78e444c1a603eecfae2ea05fa4");
71
+ const message = encode(value);
72
+
73
+ expect(message.x0).toBeDefined();
74
+ expect(message.x1).toBeDefined();
75
+
76
+ const back = decode(message);
77
+ expect(back).toEqual(value);
78
+ });
79
+ });
package/src/common.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { Schema } from "@effect/schema";
2
+ import { hexToBytes, pad } from "viem";
3
+
4
+ const MAX_U64 = 0xffffffffffffffffn;
5
+
6
+ const _Address = Schema.TemplateLiteral(Schema.Literal("0x"), Schema.String);
7
+
8
+ /** Wire representation of `Address`. */
9
+ const AddressProto = Schema.Struct({
10
+ x0: Schema.BigIntFromSelf,
11
+ x1: Schema.BigIntFromSelf,
12
+ x2: Schema.Number,
13
+ });
14
+
15
+ /** An Ethereum address. */
16
+ export const Address = Schema.transform(AddressProto, _Address, {
17
+ decode(value) {
18
+ const x0 = value.x0.toString(16).padStart(16, "0");
19
+ const x1 = value.x1.toString(16).padStart(16, "0");
20
+ const x2 = value.x2.toString(16).padStart(8, "0");
21
+ return `0x${x0}${x1}${x2}` as `0x${string}`;
22
+ },
23
+ encode(value) {
24
+ const bytes = hexToBytes(pad(value, { size: 20, dir: "left" }));
25
+ const dv = new DataView(bytes.buffer);
26
+ const x0 = dv.getBigUint64(0);
27
+ const x1 = dv.getBigUint64(8);
28
+ const x2 = dv.getUint32(16);
29
+ return { x0, x1, x2 };
30
+ },
31
+ });
32
+
33
+ export type Address = typeof Address.Type;
34
+
35
+ const _B256 = Schema.TemplateLiteral(Schema.Literal("0x"), Schema.String);
36
+
37
+ /** Wire representation of `B256`. */
38
+ export const B256Proto = Schema.Struct({
39
+ x0: Schema.BigIntFromSelf,
40
+ x1: Schema.BigIntFromSelf,
41
+ x2: Schema.BigIntFromSelf,
42
+ x3: Schema.BigIntFromSelf,
43
+ });
44
+
45
+ /** Data with length 256 bits. */
46
+ export const B256 = Schema.transform(B256Proto, _B256, {
47
+ decode(value) {
48
+ const x0 = value.x0.toString(16).padStart(16, "0");
49
+ const x1 = value.x1.toString(16).padStart(16, "0");
50
+ const x2 = value.x2.toString(16).padStart(16, "0");
51
+ const x3 = value.x3.toString(16).padStart(16, "0");
52
+ return `0x${x0}${x1}${x2}${x3}` as `0x${string}`;
53
+ },
54
+ encode(value) {
55
+ const bytes = hexToBytes(pad(value, { size: 32, dir: "left" }));
56
+ const dv = new DataView(bytes.buffer);
57
+ const x0 = dv.getBigUint64(0);
58
+ const x1 = dv.getBigUint64(8);
59
+ const x2 = dv.getBigUint64(16);
60
+ const x3 = dv.getBigUint64(24);
61
+ return { x0, x1, x2, x3 };
62
+ },
63
+ });
64
+
65
+ export const b256ToProto = Schema.encodeSync(B256);
66
+ export const b256FromProto = Schema.decodeSync(B256);
67
+
68
+ /** Wire representation of `U256`. */
69
+ const U256Proto = Schema.Struct({
70
+ x0: Schema.BigIntFromSelf,
71
+ x1: Schema.BigIntFromSelf,
72
+ x2: Schema.BigIntFromSelf,
73
+ x3: Schema.BigIntFromSelf,
74
+ });
75
+
76
+ /** Data with length 256 bits. */
77
+ export const U256 = Schema.transform(U256Proto, Schema.BigIntFromSelf, {
78
+ decode(value) {
79
+ return (
80
+ (value.x0 << (8n * 24n)) +
81
+ (value.x1 << (8n * 16n)) +
82
+ (value.x2 << (8n * 8n)) +
83
+ value.x3
84
+ );
85
+ },
86
+ encode(value) {
87
+ const x0 = (value >> (8n * 24n)) & MAX_U64;
88
+ const x1 = (value >> (8n * 16n)) & MAX_U64;
89
+ const x2 = (value >> (8n * 8n)) & MAX_U64;
90
+ const x3 = value & MAX_U64;
91
+ return { x0, x1, x2, x3 };
92
+ },
93
+ });
94
+
95
+ export const u256ToProto = Schema.encodeSync(U256);
96
+ export const u256FromProto = Schema.decodeSync(U256);
97
+
98
+ /** Wire representation of `U128`. */
99
+ const U128Proto = Schema.Struct({
100
+ x0: Schema.BigIntFromSelf,
101
+ x1: Schema.BigIntFromSelf,
102
+ });
103
+
104
+ /** Data with length 128 bits. */
105
+ export const U128 = Schema.transform(U128Proto, Schema.BigIntFromSelf, {
106
+ decode(value) {
107
+ return (value.x0 << (8n * 8n)) + value.x1;
108
+ },
109
+ encode(value) {
110
+ const x0 = (value >> (8n * 8n)) & MAX_U64;
111
+ const x1 = value & MAX_U64;
112
+ return { x0, x1 };
113
+ },
114
+ });
115
+
116
+ export const u128ToProto = Schema.encodeSync(U128);
117
+ export const u128FromProto = Schema.decodeSync(U128);
@@ -0,0 +1,191 @@
1
+ import { encodeEventTopics, pad, parseAbi } from "viem";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { Schema } from "@effect/schema";
5
+ import {
6
+ Filter,
7
+ LogFilter,
8
+ filterFromProto,
9
+ filterToProto,
10
+ mergeFilter,
11
+ } from "./filter";
12
+
13
+ const abi = parseAbi([
14
+ "event Transfer(address indexed from, address indexed to, uint256 value)",
15
+ ]);
16
+
17
+ describe("Filter", () => {
18
+ it("all filters are optional", () => {
19
+ const filter = Filter.make({});
20
+
21
+ const proto = filterToProto(filter);
22
+ const back = filterFromProto(proto);
23
+ expect(back).toEqual(filter);
24
+ });
25
+
26
+ it("accepts logs filter", () => {
27
+ const filter = Filter.make({
28
+ logs: [
29
+ {
30
+ address: "0x123456789012",
31
+ strict: true,
32
+ topics: encodeEventTopics({
33
+ abi,
34
+ eventName: "Transfer",
35
+ args: { from: null, to: null },
36
+ }) as `0x${string}`[],
37
+ },
38
+ ],
39
+ });
40
+
41
+ expect(filter.logs).toHaveLength(1);
42
+
43
+ const proto = filterToProto(filter);
44
+ const back = filterFromProto(proto);
45
+
46
+ expect(back).toBeDefined();
47
+ expect(back.logs).toHaveLength(1);
48
+ });
49
+ });
50
+
51
+ describe("LogFilter", () => {
52
+ const encode = Schema.encodeSync(LogFilter);
53
+ const decode = Schema.decodeSync(LogFilter);
54
+
55
+ it("can be empty", () => {
56
+ const filter = LogFilter.make({});
57
+
58
+ const proto = encode(filter);
59
+ const back = decode(proto);
60
+ expect(back).toEqual(filter);
61
+ });
62
+
63
+ it("can have null topics", () => {
64
+ const filter = LogFilter.make({
65
+ topics: [null, pad("0x1"), null, pad("0x3")],
66
+ });
67
+
68
+ const proto = encode(filter);
69
+ const back = decode(proto);
70
+ expect(back).toEqual(filter);
71
+ });
72
+
73
+ it("can have all optional fields", () => {
74
+ const filter = LogFilter.make({
75
+ address: pad("0xa", { size: 20 }),
76
+ topics: [null, pad("0x1"), null, pad("0x3")],
77
+ includeTransaction: true,
78
+ includeReceipt: true,
79
+ strict: true,
80
+ });
81
+
82
+ const proto = encode(filter);
83
+ const back = decode(proto);
84
+ expect(back).toEqual(filter);
85
+ });
86
+ });
87
+
88
+ describe("mergeFilter", () => {
89
+ it("returns header.always if any has it", () => {
90
+ const fa = mergeFilter({}, { header: { always: true } });
91
+ expect(fa).toMatchInlineSnapshot(`
92
+ {
93
+ "header": {
94
+ "always": true,
95
+ },
96
+ "logs": [],
97
+ "transactions": [],
98
+ "withdrawals": [],
99
+ }
100
+ `);
101
+ const fb = mergeFilter({ header: { always: true } }, {});
102
+ expect(fb).toMatchInlineSnapshot(`
103
+ {
104
+ "header": {
105
+ "always": true,
106
+ },
107
+ "logs": [],
108
+ "transactions": [],
109
+ "withdrawals": [],
110
+ }
111
+ `);
112
+ });
113
+
114
+ it("returns an empty header by default", () => {
115
+ const f = mergeFilter({}, {});
116
+ expect(f).toMatchInlineSnapshot(`
117
+ {
118
+ "header": undefined,
119
+ "logs": [],
120
+ "transactions": [],
121
+ "withdrawals": [],
122
+ }
123
+ `);
124
+ });
125
+
126
+ it("concatenates logs", () => {
127
+ const f = mergeFilter(
128
+ { logs: [{ address: "0xAAAAAAAAAAAAAAAAAAAAAA" }] },
129
+ { logs: [{ address: "0xBBBBBBBBBBBBBBBBBBBBBB" }] },
130
+ );
131
+ expect(f).toMatchInlineSnapshot(`
132
+ {
133
+ "header": undefined,
134
+ "logs": [
135
+ {
136
+ "address": "0xAAAAAAAAAAAAAAAAAAAAAA",
137
+ },
138
+ {
139
+ "address": "0xBBBBBBBBBBBBBBBBBBBBBB",
140
+ },
141
+ ],
142
+ "transactions": [],
143
+ "withdrawals": [],
144
+ }
145
+ `);
146
+ });
147
+
148
+ it("concatenates transactions", () => {
149
+ const f = mergeFilter(
150
+ { transactions: [{ from: "0xAAAAAAAAAAAAAAAAAAAAAA" }] },
151
+ { transactions: [{ from: "0xBBBBBBBBBBBBBBBBBBBBBB" }] },
152
+ );
153
+ expect(f).toMatchInlineSnapshot(`
154
+ {
155
+ "header": undefined,
156
+ "logs": [],
157
+ "transactions": [
158
+ {
159
+ "from": "0xAAAAAAAAAAAAAAAAAAAAAA",
160
+ },
161
+ {
162
+ "from": "0xBBBBBBBBBBBBBBBBBBBBBB",
163
+ },
164
+ ],
165
+ "withdrawals": [],
166
+ }
167
+ `);
168
+ });
169
+
170
+ it("concatenates withdrawals", () => {
171
+ const f = mergeFilter(
172
+ { withdrawals: [{ validatorIndex: 1 }] },
173
+ { withdrawals: [{ validatorIndex: 100 }] },
174
+ );
175
+ expect(f).toMatchInlineSnapshot(`
176
+ {
177
+ "header": undefined,
178
+ "logs": [],
179
+ "transactions": [],
180
+ "withdrawals": [
181
+ {
182
+ "validatorIndex": 1,
183
+ },
184
+ {
185
+ "validatorIndex": 100,
186
+ },
187
+ ],
188
+ }
189
+ `);
190
+ });
191
+ });