Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ruleset Unit Tests and tweaks #445

Merged
merged 16 commits into from
Sep 13, 2023
5 changes: 5 additions & 0 deletions .changeset/khaki-queens-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nocturne-xyz/deposit-screener": minor
---

Add unit tests, update internals
2 changes: 1 addition & 1 deletion actors/deposit-screener/.env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ SUBGRAPH_URL=http://localhost:8000/subgraphs/name/nocturne
RPC_URL=http://127.0.0.1:8545

TX_SIGNER_KEY=0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
ATTESTATION_SIGNER_KEY=0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
ATTESTATION_SIGNER_KEY=0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
2 changes: 1 addition & 1 deletion actors/deposit-screener/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"prettier:check": "prettier --check ./src",
"prettier:write": "prettier --write ./src",
"screener-cli": "npx ts-node src/cli/index.ts",
"test:unit": ""
"test:unit": "mocha --timeout 20000 -r ts-node/register test/** --exclude test/snapshots/** -r dotenv/config"
},
"dependencies": {
"@nocturne-xyz/config": "workspace:^",
Expand Down
54 changes: 0 additions & 54 deletions actors/deposit-screener/scripts/testMisttrackCallAndRuleset.ts

This file was deleted.

102 changes: 0 additions & 102 deletions actors/deposit-screener/scripts/testRulesets.ts

This file was deleted.

101 changes: 61 additions & 40 deletions actors/deposit-screener/src/screening/checks/RuleSet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ScreeningDepositRequest } from "..";
import { API_CALLS, ApiCallKeys, ApiMap, CallReturnData } from "./apiCalls";
import {
API_CALL_MAP,
ApiCallNames,
ApiCallToReturnType,
CallReturnData,
} from "./apiCalls";
export interface Rejection {
type: "Rejection";
reason: string;
Expand Down Expand Up @@ -34,41 +39,44 @@ const ACTION_NOT_TRIGGERED = {
type: "ActionNotTriggered",
} as const;

export interface RuleParams<K extends keyof ApiMap> {
export interface RuleParams<C extends ApiCallNames> {
name: string;
call: K;
threshold: (data: ApiMap[K]) => boolean;
call: C;
threshold: (data: ApiCallToReturnType[C]) => boolean;
action: Rejection | DelayAction;
}

export type PartialRuleParams<K extends keyof ApiMap> = Omit<
RuleParams<K>,
export type PartialRuleParams<C extends ApiCallNames> = Omit<
RuleParams<C>,
"action"
>;

export interface CombinedRulesParams<T extends ReadonlyArray<keyof ApiMap>> {
partials: { readonly [K in keyof T]: PartialRuleParams<T[K]> };
export interface CombinedRulesParams<T extends ReadonlyArray<ApiCallNames>> {
// Essentially a readonly object type, i.e, [PartialRuleParams, PartialRuleParams, ...]
partialRules: { readonly [C in keyof T]: PartialRuleParams<T[C]> };
action: Rejection | DelayAction;
applyIf: "Any" | "All";
}

export type CachedApiCallData = Partial<Record<ApiCallNames, CallReturnData>>;

export interface RuleLike {
next: RuleLike | null;
name: string;
check: (
deposit: ScreeningDepositRequest,
cache: Record<ApiCallKeys, ApiMap[ApiCallKeys]>
cache: CachedApiCallData
) => Promise<Rejection | DelayAction | typeof ACTION_NOT_TRIGGERED>;
}

export class Rule<K extends keyof ApiMap> implements RuleLike {
export class Rule<C extends ApiCallNames> implements RuleLike {
public next: RuleLike | null = null;
public readonly name: RuleParams<K>["name"];
private threshold: RuleParams<K>["threshold"];
private call: RuleParams<K>["call"];
private action: RuleParams<K>["action"];
public readonly name: RuleParams<C>["name"];
private threshold: RuleParams<C>["threshold"];
private call: RuleParams<C>["call"];
private action: RuleParams<C>["action"];

constructor({ name, call, threshold, action }: RuleParams<K>) {
constructor({ name, call, threshold, action }: RuleParams<C>) {
this.name = name;
this.call = call;
this.threshold = threshold;
Expand All @@ -77,54 +85,63 @@ export class Rule<K extends keyof ApiMap> implements RuleLike {

async check(
deposit: ScreeningDepositRequest,
cache: Record<ApiCallKeys, ApiMap[ApiCallKeys]>
cache: CachedApiCallData
): Promise<Rejection | DelayAction | typeof ACTION_NOT_TRIGGERED> {
if (!cache[this.call]) {
cache[this.call] = await API_CALLS[this.call](deposit);
cache[this.call] = await API_CALL_MAP[this.call](deposit);
}
const data = cache[this.call] as ApiMap[K];
const data = cache[this.call] as ApiCallToReturnType[C];

return this.threshold(data) ? this.action : ACTION_NOT_TRIGGERED;
}
}

export class CompositeRule<T extends ReadonlyArray<keyof ApiMap>>
export class CompositeRule<T extends ReadonlyArray<ApiCallNames>>
implements RuleLike
{
public next: RuleLike | null = null;
public readonly name: string;
private partials: CombinedRulesParams<T>["partials"];
private partialRules: CombinedRulesParams<T>["partialRules"];
private action: CombinedRulesParams<T>["action"];
private predicateFn: "some" | "every"; // corresponds to Array.prototype.some and Array.prototype.every

constructor(params: CombinedRulesParams<T>) {
this.name = `Composite(${params.partials.map((r) => r.name).join(", ")})`;
this.partials = params.partials;
this.name = `Composite(${params.partialRules
.map((r) => r.name)
.join(", ")}):${params.applyIf}`;
this.partialRules = params.partialRules;
this.action = params.action;
this.predicateFn = params.applyIf === "Any" ? "some" : "every";
}

async check(
deposit: ScreeningDepositRequest,
cache: Record<ApiCallKeys, CallReturnData>
cache: CachedApiCallData
): Promise<Rejection | DelayAction | typeof ACTION_NOT_TRIGGERED> {
const shouldApply = this.partials[this.predicateFn](async (partial) => {
if (!cache[partial.call]) {
cache[partial.call] = await API_CALLS[partial.call](deposit);
}
const data = cache[partial.call] as ApiMap[typeof partial.call];
return partial.threshold(data);
});
return shouldApply ? this.action : ACTION_NOT_TRIGGERED;
const results = await Promise.all(
this.partialRules.map(async (partial) => {
if (!cache[partial.call]) {
cache[partial.call] = await API_CALL_MAP[partial.call](deposit);
}
const data = cache[
partial.call
] as ApiCallToReturnType[typeof partial.call];

return partial.threshold(data);
})
);
const shouldApplyRule = results[this.predicateFn]((_) => _);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need ((_) => _)? Can we not just say results[this.predicateFn]()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do; the predicate fns require a fn to predicate off of, and unfortunately in TypeScript there isn't cleaner syntactic sugar

return shouldApplyRule ? this.action : ACTION_NOT_TRIGGERED;
}
}

export class RuleSet {
private head: RuleLike | null = null;
private tail: RuleLike | null = null;
private delaySeconds;
private readonly baseDelaySeconds;

constructor({ baseDelaySeconds = 0 }: { baseDelaySeconds?: number } = {}) {
this.delaySeconds = baseDelaySeconds;
this.baseDelaySeconds = baseDelaySeconds;
}

private _add(ruleLike: RuleLike) {
Expand All @@ -136,27 +153,31 @@ export class RuleSet {
this.tail = ruleLike;
}

add<K extends keyof ApiMap>(ruleParams: RuleParams<K>): RuleSet {
add<C extends ApiCallNames>(ruleParams: RuleParams<C>): RuleSet {
const rule = new Rule(ruleParams);
this._add(rule);
return this;
}

combineAndAdd<T extends ReadonlyArray<keyof ApiMap>>(
combineAndAdd<T extends ReadonlyArray<ApiCallNames>>(
compositeParams: CombinedRulesParams<T>
): RuleSet {
const composite = new CompositeRule(compositeParams);
this._add(composite);
return this;
}

async check(deposit: ScreeningDepositRequest): Promise<Rejection | Delay> {
async check(
deposit: ScreeningDepositRequest,
cache: CachedApiCallData = {}
): Promise<Rejection | Delay> {
let delaySeconds = this.baseDelaySeconds;
let currRule = this.head;
const cache: Record<string, CallReturnData> = {};
const rulesLogList: {
ruleName: string;
result: Awaited<ReturnType<RuleLike["check"]>>;
}[] = [];

while (currRule !== null) {
const result = await currRule.check(deposit, cache);
rulesLogList.push({
Expand All @@ -167,14 +188,14 @@ export class RuleSet {
console.log(`Screener execution for deposit:`, deposit, rulesLogList);
return result;
} else if (result.type === "Delay") {
this.delaySeconds = APPLY_DELAY_OPERATION[result.operation](
this.delaySeconds,
delaySeconds = APPLY_DELAY_OPERATION[result.operation](
delaySeconds,
result.operation === "Add" ? result.valueSeconds : result.factor
);
}
currRule = currRule.next;
}
console.log(`Screener execution for deposit:`, deposit, rulesLogList);
return { type: "Delay", timeSeconds: this.delaySeconds };
return { type: "Delay", timeSeconds: delaySeconds };
}
}
Loading
Loading