Skip to content

Commit 2593601

Browse files
committed
feat: init commit, add orap, rek, utils
1 parent c15ade8 commit 2593601

File tree

26 files changed

+962
-0
lines changed

26 files changed

+962
-0
lines changed

src/orap/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# ORAP: Oracle Application Framework
2+
3+
ORAP is an out of box, express style framework.
4+
5+
## Usage
6+
```ts
7+
import { ListenOptions, Orap, StoreManager } from "../../orap";
8+
import { memoryStore, redisStore } from "../../utils";
9+
10+
const orap = new Orap()
11+
// const sm = new StoreManager(redisStore(...)) // use redis
12+
const sm = new StoreManager() // use memory
13+
14+
var eventSignalParam = {
15+
address: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
16+
abi: {"anonymous": false,"inputs": [{ "indexed": true,"name": "from","type": "address"},{"indexed": true,"name": "to","type": "address"},{"indexed": false,"name": "value","type": "uint256"}],"name": "Transfer","type": "event"},
17+
eventName: "Transfer",
18+
}
19+
20+
const handle = (...args:any)=>{console.log("handle", args)}
21+
22+
orap.event(eventSignalParam, handle)
23+
.crosscheck({intervalMsMin: 1000, batchBlocksCount: 1, blockIntervalMs: 12000})
24+
25+
orap.listen(
26+
{wsProvider:"wss://127.0.0.1", httpProvider:"http://127.0.0.1"},
27+
()=>{console.log("listening on provider.network")}
28+
)
29+
```
30+
31+
### listen options
32+
- required: wsProvider, for subscription
33+
- optional: httpProvider, for crosscheck only, since crosscheck is based on getLogs
34+
35+
## Task
36+
37+
### TaskBase
38+
- provide universal `toString`, `fromString`, `stringify`
39+
40+
### TaskStorable
41+
- provide store (redis) compatible features, i.e. load, save, remove, done
42+
- overwrite when extends:
43+
- `toKey()` (required): define the primary key that identifies each task, **doesn't** include `taskPrefix`
44+
- `taskPrefix` (recommend): set the prefix of all tasks, also is used when `load` task
45+
- `taskPrefixDone` (recommend): set the prefix of finished tasks, only used in `done`; no need to set if you don't use "task.done(sm)"
46+
47+
## Signal
48+
49+
all actions that arrive the oracle server and trigger actions are defined as signal, including:
50+
- [x] event
51+
- [ ] block
52+
- [ ] http request
53+
etc.
54+
55+
### EventSignal
56+
- define event listener as simple as: `orap.event({address:"0x", abi:"", eventName: "Transfer"}, handleSignal)`
57+
- provide crosschecker by `rek`, available config please checkout `AutoCrossCheckParam` in `rek`
58+
- currently one and only one crosschecker is set to each event signal
59+
- store: provide 2 options: use memory or redis, checkout `orap/store`

src/orap/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from "./signal"
2+
export * from "./store"
3+
export * from "./task"
4+
export * from "./orap"

src/orap/orap.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { ethers } from "ethers";
3+
import { EventSignal } from "./signal/event";
4+
5+
export interface ListenOptions {
6+
wsProvider: ethers.WebSocketProvider
7+
httpProvider?: ethers.JsonRpcProvider
8+
}
9+
10+
export class Orap {
11+
routes: {
12+
event: EventSignal[]
13+
}
14+
15+
constructor() {
16+
this.routes = {
17+
event: []
18+
}
19+
}
20+
21+
event(options: any, fn: any) {
22+
const es = new EventSignal(options, fn)
23+
this.routes.event.push(es)
24+
return es
25+
}
26+
27+
_listenChain(wsProvider: ethers.WebSocketProvider, httpProvider?: ethers.JsonRpcProvider) {
28+
this.routes.event.forEach(es => es.listen(wsProvider, httpProvider));
29+
}
30+
31+
listen(options: ListenOptions, onListen: any = ()=>{}) {
32+
this._listenChain(options.wsProvider, options.httpProvider);
33+
onListen();
34+
return this;
35+
}
36+
}

src/orap/signal/event.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/* eslint-disable no-unused-vars */
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
import { ethers, EventFragment, Log } from "ethers";
4+
import { Signal } from "./type";
5+
import { AutoCrossChecker } from "../../rek/event/crosschecker/autochecker";
6+
import { AutoCrossCheckParam } from "../../rek/event/crosschecker/interface";
7+
import { ONE_MINUTE_MS } from "../../rek/constants";
8+
9+
export interface EventSignalRegisterParams {
10+
address: string,
11+
abi: any,
12+
eventName: string,
13+
// esig?: string,
14+
}
15+
16+
export type EventSignalCallback = ethers.Listener
17+
18+
export class EventSignal implements Signal {
19+
20+
provider?: ethers.JsonRpcProvider | ethers.WebSocketProvider;
21+
contract: ethers.Contract;
22+
esig: string
23+
eventFragment: EventFragment
24+
25+
subscribeCallback: EventSignalCallback
26+
crosscheckCallback: EventSignalCallback
27+
28+
crosschecker?: AutoCrossChecker;
29+
crosscheckerOptions?: AutoCrossCheckParam;
30+
31+
constructor(
32+
public params: EventSignalRegisterParams,
33+
public callback: EventSignalCallback
34+
) {
35+
this.contract = new ethers.Contract(
36+
params.address,
37+
params.abi,
38+
)
39+
40+
// Get the event fragment by name
41+
const iface = this.contract.interface;
42+
const _ef = iface.getEvent(params.eventName);
43+
if (!_ef) throw new Error("")
44+
this.eventFragment = _ef;
45+
46+
this.esig = this.eventFragment.topicHash
47+
48+
// to align with crosschecker onMissing, parse the last arg from ContractEventPayload to EventLog
49+
this.subscribeCallback = async (...args: Array<any>) => {
50+
const _contractEventPayload = args.pop()
51+
await this.callback(...args, _contractEventPayload.log);
52+
}
53+
// to align with subscribe listener, parse event params and add EventLog to the last
54+
this.crosscheckCallback = async (log: Log) => {
55+
const parsedLog = this.contract.interface.decodeEventLog(this.eventFragment, log.data, log.topics);
56+
await this.callback(...parsedLog, log)
57+
}
58+
}
59+
60+
// TODO: how to integrate crosschecker
61+
// TODO: should be wsProvider only?
62+
listen(provider: ethers.WebSocketProvider, crosscheckProvider?: ethers.JsonRpcProvider) {
63+
this.provider = provider
64+
65+
// start event listener
66+
const listener = this.contract.connect(provider)
67+
listener?.on(
68+
this.params.eventName,
69+
// TODO: calling this seems to be async, should we make it to sequential?
70+
this.subscribeCallback,
71+
)
72+
73+
// start cross-checker if ever set
74+
if (this.crosscheckerOptions) {
75+
if (!crosscheckProvider)
76+
throw new Error("crosschecker set, please provide crosscheckProvider to listen function")
77+
this.startCrossChecker(crosscheckProvider)
78+
}
79+
80+
return this;
81+
}
82+
83+
async startCrossChecker(provider: ethers.JsonRpcProvider) {
84+
if (!this.crosscheckerOptions)
85+
throw new Error("no crosscheck set, can't start crosschecker");
86+
this.crosschecker = new AutoCrossChecker(provider, this.crosscheckerOptions)
87+
await this.crosschecker.start(this.crosscheckerOptions);
88+
}
89+
90+
// TODO: hide address & topics & onMissingLog from interface AutoCrossCheckParam
91+
crosscheck(options?: Omit<AutoCrossCheckParam, 'address' | 'topics' | 'onMissingLog'>) {
92+
const {
93+
intervalMsMin = ONE_MINUTE_MS * 60,
94+
ignoreLogs = [],
95+
} = options ?? {}
96+
// save crosschecker param
97+
this.crosscheckerOptions = {
98+
...options,
99+
address: this.params.address,
100+
topics: [this.esig],
101+
onMissingLog: this.crosscheckCallback,
102+
intervalMsMin,
103+
ignoreLogs,
104+
}
105+
return this;
106+
}
107+
}

src/orap/signal/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./event"
2+
export * from "./type"

src/orap/signal/type.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable no-unused-vars */
3+
4+
export interface Signal {
5+
listen(options: any): void;
6+
}

src/orap/store/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./storemanager"

src/orap/store/storemanager.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable no-unused-vars */
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
import { retryOnRet, Store, SimpleStoreManager } from "../../utils";
4+
5+
export class StoreManager extends SimpleStoreManager {
6+
queryDelay: number
7+
8+
constructor(
9+
store?: Store, options?: {queryDelay?: number}
10+
) {
11+
super(store)
12+
this.queryDelay = options?.queryDelay ?? 3000
13+
}
14+
15+
async keys(pattern?: string, isWait = false): Promise<string[]> {
16+
const _keys = () => {
17+
// console.log('[cache] _keys wait non-null')
18+
return super.keys(pattern)
19+
}
20+
if (isWait) {
21+
return retryOnRet(_keys, async (rst) => {
22+
return rst && (await rst).length > 0
23+
}, { delay: this.queryDelay });
24+
} else {
25+
return _keys()
26+
}
27+
}
28+
}

src/orap/task/base.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { deepMerge, Constructor } from '@murongg/utils';
3+
4+
export abstract class TaskBase {
5+
abstract toKey(): string
6+
7+
toString() {
8+
const obj: Record<string, any> = {};
9+
deepMerge(obj, this);
10+
return this.stringify(obj);
11+
}
12+
13+
private stringify(obj: Record<string, any>) {
14+
const replace = (key: string, value: any) => {
15+
if (typeof value === 'bigint') {
16+
return value.toString();
17+
} else {
18+
return value
19+
}
20+
}
21+
return JSON.stringify(obj, replace);
22+
}
23+
24+
fromString(jsonString: string) {
25+
const obj = JSON.parse(jsonString);
26+
Object.assign(this, obj);
27+
return this
28+
}
29+
30+
static fromString<T extends TaskBase>(this: Constructor<T>, jsonString: string): T {
31+
const instance = new this();
32+
instance.fromString(jsonString)
33+
return instance;
34+
}
35+
}

src/orap/task/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./base"
2+
export * from "./storable"

0 commit comments

Comments
 (0)