Skip to content

Commit dda0c66

Browse files
authored
add fallbacks (#15)
1 parent 3740730 commit dda0c66

17 files changed

+364
-536
lines changed

README.md

+26-3
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,40 @@ This repository contains the logic for decoding a `raw_log_event` of a transacti
4747
);
4848
```
4949

50-
The method has 3 arguments:
50+
The method has 4 arguments:
5151

5252
1. **Event Id**: A case-sensitive string concatenation of the `protocol name` with the `event name` by a `:`.
53-
2. **Networks**: An array of all the networks the defined decoding function will run for
53+
2. **Networks**: An array of all the networks the defined decoding function will run for.
54+
3. **ABI**: The ABI of the contract on which the event exists.
55+
4. **Decoding Function**: The actual decoding function, it has 3 arguments passed to it:
56+
1. `log_event`: The raw log event that is being decoded.
57+
2. `tx`: The transaction object that generated this log.
58+
3. `chain_name`: Network to which the log belongs to.
59+
4. `covalent_client`: The covalent client created with your covalent API key.
60+
61+
3. `fallback`: Creates a fallback function for the specified event name. This function is not linked to any chain or contract. Its declaration is:
62+
63+
```ts
64+
GoldRushDecoder.fallback(
65+
"EventName",
66+
ABI as Abi,
67+
async (log_event, tx, chain_name, covalent_client): Promise<EventType> => {
68+
<!-- decoding logic -->
69+
}
70+
);
71+
```
72+
73+
The method has 3 arguments:
74+
75+
1. **Event Name**: A case-sensitive name of the event to be decoded.
76+
2. **ABI**: The ABI of the contract on which the event exists.
5477
3. **Decoding Function**: The actual decoding function, it has 3 arguments passed to it:
5578
1. `log_event`: The raw log event that is being decoded.
5679
2. `tx`: The transaction object that generated this log.
5780
3. `chain_name`: Network to which the log belongs to.
5881
4. `covalent_client`: The covalent client created with your covalent API key.
5982

60-
3. `decode`: The function that chooses which decoding function needs to be called for which log event. It collects all the decoded events for a transaction and returns them in an array of structured data. It is run when the API server receives a request.
83+
4. `decode`: The function that chooses which decoding function needs to be called for which log event. It collects all the decoded events for a transaction and returns them in an array of structured data. It is run when the API server receives a request.
6184

6285
### 1. Running the Development Server
6386

api/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ app.get("/api/v1/healthcheck", (_req: Request, res: Response) => {
2121
res.json({
2222
success: true,
2323
timestamp: now.toISOString(),
24-
// time: TimestampParser(now, "descriptive"),
2524
uptime: process.uptime(),
2625
});
2726
});

jest.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ module.exports = {
44
testEnvironment: "node",
55
coveragePathIgnorePatterns: ["./dist/*"],
66
maxWorkers: 5,
7-
testTimeout: 10000,
7+
testTimeout: 30000,
88
};

scripts/add-config.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const addressSchema = yup.string().trim().required("address is required");
4141
const isFactorySchema = yup.boolean().required("is_factory is required");
4242
const networkSchema = yup
4343
.mixed()
44-
.oneOf(Object.values(Chains))
44+
.oneOf(Object.values(Chains), "incorrect network name")
4545
.required("network is required");
4646
(async () => {
4747
const { protocol_name } = (await prompt({
@@ -148,8 +148,8 @@ const networkSchema = yup
148148
const eventName: string = "<EVENT NAME>";
149149
const abiContent: string = `[]`;
150150
const configsContent: string = `import{type Configs}from"../../decoder.types";\n\nconst configs:Configs=[{address:"${address}",is_factory:${is_factory},protocol_name:"${protocol_name}",network:"${network}"}];\n\nexport default configs;`;
151-
const decodersContent: string = `import{Decoder}from"../../decoder";import{type EventType}from"../../decoder.types";import{DECODED_ACTION,DECODED_EVENT_CATEGORY}from"../../decoder.constants";import{decodeEventLog,type Abi}from"viem";import ABI from "./abis/${protocol_name}.abi.json";\n\nDecoder.on("${protocol_name}:${eventName}",["${network}"],ABI as Abi,async(log,chain_name,covalent_client):Promise<EventType> =>{const{raw_log_data,raw_log_topics}=log;\n\nconst{args:decoded}=decodeEventLog({abi:ABI,topics:raw_log_topics as[],data:raw_log_data as \`0x\${string}\`,eventName:"${eventName}"})as{eventName:"${eventName}";args:{}};\n\nreturn{action:DECODED_ACTION.SWAPPED,category:DECODED_EVENT_CATEGORY.DEX,name:"${eventName}",protocol:{logo:log.sender_logo_url as string,name:log.sender_name as string}};});`;
152-
const testContent: string = `import request from"supertest";import app from"../../../..";import{type EventType}from"../../decoder.types";\n\ndescribe("${protocol_name}",()=>{test("${network}:${eventName}",async()=>{const res=await request(app).post("/api/v1/tx/decode").set({"x-covalent-api-key":process.env.TEST_COVALENT_API_KEY}).send({network:"${network}",tx_hash:"<ENTER TX HASH FOR TESTING>"});const{events}=res.body as{events:EventType[]};const event=events.find(({name})=>name==="${eventName}");if(!event){throw Error("Event not found")}const testAdded:boolean=false;expect(testAdded).toEqual(true)})});`;
151+
const decodersContent: string = `import{GoldRushDecoder}from"../../decoder";import{type EventType}from"../../decoder.types";import{DECODED_ACTION,DECODED_EVENT_CATEGORY}from"../../decoder.constants";import{decodeEventLog,type Abi}from"viem";import ABI from "./abis/${protocol_name}.abi.json";\n\nGoldRushDecoder.on("${protocol_name}:${eventName}",["${network}"],ABI as Abi,async(log_event,tx,chain_name,covalent_client):Promise<EventType> =>{const{raw_log_data,raw_log_topics}=log_event;\n\nconst{args:decoded}=decodeEventLog({abi:ABI,topics:raw_log_topics as[],data:raw_log_data as \`0x\${string}\`,eventName:"${eventName}"})as{eventName:"${eventName}";args:{}};\n\nreturn{action:DECODED_ACTION.SWAPPED,category:DECODED_EVENT_CATEGORY.DEX,name:"${eventName}",protocol:{logo:log_event.sender_logo_url as string,name:log_event.sender_name as string}};});`;
152+
const testContent: string = `import request from"supertest";import app from"../../../../api";import{type EventType}from"../../decoder.types";\n\ndescribe("${protocol_name}",()=>{test("${network}:${eventName}",async()=>{const res=await request(app).post("/api/v1/tx/decode").set({"x-covalent-api-key":process.env.TEST_COVALENT_API_KEY}).send({network:"${network}",tx_hash:"<ENTER TX HASH FOR TESTING>"});const{events}=res.body as{events:EventType[]};const event=events.find(({name})=>name==="${eventName}");if(!event){throw Error("Event not found")}const testAdded:boolean=false;expect(testAdded).toEqual(true)})});`;
153153
await writeInFile(
154154
protocolDir,
155155
`${protocol_name}.decoders.ts`,

services/decoder/decoder.ts

+70-37
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,24 @@ import {
1212
type DecodingFunctions,
1313
type EventType,
1414
type DecoderConfig,
15+
type Fallbacks,
1516
} from "./decoder.types";
1617
import { encodeEventTopics, type Abi } from "viem";
1718

1819
export class GoldRushDecoder {
1920
private static configs: DecoderConfig = {};
2021
private static decoders: Decoders = {};
22+
private static fallbacks: Fallbacks = {};
2123
private static decoding_functions: DecodingFunctions = [];
2224

2325
public static initDecoder = () => {
24-
const directoryPath: string = join(__dirname, "/protocols");
25-
const protocols = readdirSync(directoryPath);
26+
console.info("Initializing GoldrushDecoder Service...");
27+
28+
const protocolsDirectoryPath: string = join(__dirname, "/protocols");
29+
const protocols = readdirSync(protocolsDirectoryPath);
2630
let protocolsCount: number = 0;
2731
for (const protocol of protocols) {
28-
const protocolPath = join(directoryPath, protocol);
32+
const protocolPath = join(protocolsDirectoryPath, protocol);
2933
const files = readdirSync(protocolPath);
3034
let configFile: string | null = null,
3135
decodersFile: string | null = null;
@@ -56,6 +60,27 @@ export class GoldRushDecoder {
5660
}
5761
}
5862

63+
const fallbacksDirectoryPath: string = join(__dirname, "/fallbacks");
64+
const fallbacks = readdirSync(fallbacksDirectoryPath);
65+
let fallbacksCount: number = 0;
66+
for (const fallback of fallbacks) {
67+
const fallbackPath = join(fallbacksDirectoryPath, fallback);
68+
const files = readdirSync(fallbackPath);
69+
let fallbackFile: string | null = null;
70+
files.forEach((file) => {
71+
const fileExtension =
72+
process.env.NODE_ENV !== "test" ? "js" : "ts";
73+
if (file.endsWith(`.fallback.${fileExtension}`)) {
74+
fallbackFile = file;
75+
}
76+
});
77+
if (fallbackFile) {
78+
fallbacksCount++;
79+
require(join(fallbackPath, fallbackFile));
80+
}
81+
}
82+
83+
const decodersCount = Object.keys(this.decoding_functions).length;
5984
const configsCount = Object.values(this.configs).reduce(
6085
(networkCount, network) => {
6186
return (
@@ -68,11 +93,10 @@ export class GoldRushDecoder {
6893
0
6994
);
7095

71-
const decodersCount = Object.keys(this.decoding_functions).length;
72-
73-
console.info(
74-
`Created ${configsCount} configs and ${decodersCount} decoders for ${protocolsCount} protocols!`
75-
);
96+
console.info(`${protocolsCount.toLocaleString()} protocols found`);
97+
console.info(`${configsCount.toLocaleString()} configs generated`);
98+
console.info(`${decodersCount.toLocaleString()} decoders generated`);
99+
console.info(`${fallbacksCount.toLocaleString()} fallbacks generated`);
76100
};
77101

78102
public static on = (
@@ -82,7 +106,7 @@ export class GoldRushDecoder {
82106
decoding_function: DecodingFunction
83107
) => {
84108
const [protocol, event_name] = event_id.split(":");
85-
const [topic0] = encodeEventTopics({
109+
const [topic0_hash] = encodeEventTopics({
86110
abi: abi,
87111
eventName: event_name,
88112
});
@@ -101,45 +125,54 @@ export class GoldRushDecoder {
101125
Object.keys(this.configs[network][protocol]).forEach((address) => {
102126
this.decoders[network] ??= {};
103127
this.decoders[network][address] ??= {};
104-
this.decoders[network][address][topic0] =
128+
this.decoders[network][address][topic0_hash] =
105129
decoding_function_index;
106130
});
107131
});
108132
};
109133

134+
public static fallback = (
135+
event_name: string,
136+
abi: Abi,
137+
decoding_function: DecodingFunction
138+
) => {
139+
const [topic0_hash] = encodeEventTopics({
140+
abi: abi,
141+
eventName: event_name,
142+
});
143+
this.decoding_functions.push(decoding_function);
144+
const decoding_function_index: number =
145+
this.decoding_functions.length - 1;
146+
this.fallbacks[topic0_hash] = decoding_function_index;
147+
};
148+
110149
public static decode = async (
111150
network: Chain,
112151
tx: Transaction,
113152
covalent_api_key: string
114153
) => {
115-
try {
116-
const covalent_client = new CovalentClient(covalent_api_key);
117-
const events: EventType[] = [];
118-
const logs = tx.log_events.reverse();
119-
for (const log of logs) {
120-
const {
121-
raw_log_topics: [topic0_hash],
122-
sender_address: contract_address,
123-
// !ERROR: add factory_contract_address in the log_event(s)
124-
// factory_contract_address,
125-
} = log;
126-
const function_index =
127-
// !ERROR: add factory_contract_address in the log_event(s)
128-
// factory_contract_address ||
129-
this.decoders[network][contract_address]?.[topic0_hash];
130-
if (function_index !== undefined) {
131-
const event = await this.decoding_functions[function_index](
132-
log,
133-
tx,
134-
network,
135-
covalent_client
136-
);
137-
events.push(event);
138-
}
154+
const covalent_client = new CovalentClient(covalent_api_key);
155+
const events: EventType[] = [];
156+
const logs = (tx.log_events ?? []).reverse();
157+
for (const log of logs) {
158+
const {
159+
raw_log_topics: [topic0_hash],
160+
sender_address: contract_address,
161+
// !ERROR: add factory_contract_address in the log_event(s)
162+
// factory_contract_address,
163+
} = log;
164+
const decoding_index =
165+
// !ERROR: add factory_contract_address in the log_event(s)
166+
// factory_contract_address ||
167+
this.decoders[network][contract_address]?.[topic0_hash];
168+
const fallback_index = this.fallbacks[topic0_hash];
169+
if (decoding_index !== undefined || fallback_index !== undefined) {
170+
const event = await this.decoding_functions[
171+
decoding_index ?? fallback_index
172+
](log, tx, network, covalent_client);
173+
events.push(event);
139174
}
140-
return events;
141-
} catch (error) {
142-
console.error(error);
143175
}
176+
return events;
144177
};
145178
}

services/decoder/decoder.types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,10 @@ export type Decoders =
8686
}
8787
| Record<string, never>;
8888

89+
export type Fallbacks =
90+
| {
91+
[topic0_hash: string]: number;
92+
}
93+
| Record<string, never>;
94+
8995
export type DecodingFunctions = DecodingFunction[];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[
2+
{
3+
"anonymous": false,
4+
"inputs": [
5+
{
6+
"indexed": true,
7+
"internalType": "address",
8+
"name": "from",
9+
"type": "address"
10+
},
11+
{
12+
"indexed": true,
13+
"internalType": "address",
14+
"name": "to",
15+
"type": "address"
16+
},
17+
{
18+
"indexed": false,
19+
"internalType": "uint256",
20+
"name": "value",
21+
"type": "uint256"
22+
}
23+
],
24+
"name": "Transfer",
25+
"type": "event"
26+
}
27+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[
2+
{
3+
"anonymous": false,
4+
"inputs": [
5+
{
6+
"indexed": true,
7+
"internalType": "address",
8+
"name": "from",
9+
"type": "address"
10+
},
11+
{
12+
"indexed": true,
13+
"internalType": "address",
14+
"name": "to",
15+
"type": "address"
16+
},
17+
{
18+
"indexed": true,
19+
"internalType": "uint256",
20+
"name": "tokenId",
21+
"type": "uint256"
22+
}
23+
],
24+
"name": "Transfer",
25+
"type": "event"
26+
}
27+
]

0 commit comments

Comments
 (0)