Skip to content

Commit

Permalink
Deposit Screener Rulesets (#429)
Browse files Browse the repository at this point in the history
* wip poc

* thunk accepts any number of args

* let -> const

* compiles

* Cache API calls, TODO make a test for this

* use deposit trivially

* quickie

* some cleanup, add more examples

* poc complete

* interfaces not types

* simple dummy test
  • Loading branch information
panieldark authored Sep 12, 2023
1 parent 9098e2c commit 25556c7
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 9 deletions.
72 changes: 72 additions & 0 deletions actors/deposit-screener/scripts/testRulesets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { AssetTrait, StealthAddressTrait } from "@nocturne-xyz/core";
import { Rule, RuleSet } from "../src/screening/checks/RuleSet";
import { TrmData } from "../src/screening/checks/apiCalls";

(async () => {
const DELAY_50_ALWAYS = new Rule({
call: "TRM_SCREENING_ADDRESSES",
threshold: () => {
return true;
},
action: { type: "Delay", timeSeconds: 50 },
});
const REJECT_IF_RISK_OVER_POINT_5 = new Rule({
call: "TRM_SCREENING_ADDRESSES",
threshold: (data: TrmData) => {
return data.risk > 0.5;
},
action: { type: "Rejection", reason: "This should not fire" },
});
// const REJECT_ALWAYS = new Rule({
// call: "TRM_SCREENING_ADDRESSES",
// threshold: () => {
// return true;
// },
// action: {
// type: "Rejection",
// reason: "If included, this should make the deposit always reject",
// },
// });
const DELAY_100_IF_RISK_IS_POINT_FIVE = new Rule({
call: "TRM_SCREENING_ADDRESSES",
threshold: (data: TrmData) => {
return data.risk === 0.5;
},
action: { type: "Delay", timeSeconds: 100 },
});
const DELAY_10000000_NEVER = new Rule({
call: "TRM_SCREENING_ADDRESSES",
threshold: () => {
return false;
},
action: { type: "Delay", timeSeconds: 10000000 },
});
const DUMMY_RULESET = new RuleSet()
.add(DELAY_50_ALWAYS)
.add(REJECT_IF_RISK_OVER_POINT_5)
// .add(REJECT_ALWAYS)
.add(DELAY_100_IF_RISK_IS_POINT_FIVE)
.add(DELAY_10000000_NEVER);

const DUMMY_DEPOSIT_REQUEST = {
spender: "",
encodedAsset: AssetTrait.encode({ assetType: 0, assetAddr: "", id: 0n }),
value: 0n,
depositAddr: StealthAddressTrait.compress({
h1X: 0n,
h1Y: 0n,
h2X: 0n,
h2Y: 0n,
}),
nonce: 0n,
gasCompensation: 0n,
};

DUMMY_RULESET.check(DUMMY_DEPOSIT_REQUEST)
.then((result) => {
console.log(result);
})
.catch((err) => {
console.log(err);
});
})();
75 changes: 75 additions & 0 deletions actors/deposit-screener/src/screening/checks/RuleSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { DepositRequest } from "@nocturne-xyz/core";
import { API_CALLS, Data } from "./apiCalls";

export interface Rejection {
type: "Rejection";
reason: string;
}
export interface Delay {
type: "Delay";
timeSeconds: number;
}

export class Rule<T extends Data> {
public next: Rule<any> | null;
private threshold: (data: T) => boolean;
private call: keyof typeof API_CALLS;
private action: Rejection | Delay;
constructor({
call,
threshold,
action,
}: {
call: keyof typeof API_CALLS;
threshold: (data: T) => boolean;
action: Rejection | Delay;
}) {
this.next = null;
this.call = call;
this.threshold = threshold;
this.action = action;
}

async check(
deposit: DepositRequest,
cache: Record<string, Data>
): Promise<Rejection | Delay | undefined> {
if (!cache[this.call]) {
cache[this.call] = await API_CALLS[this.call](deposit);
}
const data = cache[this.call] as T;
return this.threshold(data) ? this.action : undefined;
}
}

export class RuleSet {
private head: Rule<any> | null = null;
private tail: Rule<any> | null = null;
private delay = 0;

add<T extends Data>(rule: Rule<T>): RuleSet {
if (!this.head) {
this.head = rule;
} else {
this.tail!.next = rule;
}
this.tail = rule;
return this;
}

async check(deposit: DepositRequest): Promise<Rejection | Delay> {
let currRule = this.head;
const cache: Record<string, Data> = {};
while (currRule !== null) {
const result = await currRule.check(deposit, cache);
if (result) {
if (result.type === "Rejection") {
return result;
}
this.delay += result.timeSeconds;
}
currRule = currRule.next;
}
return { type: "Delay", timeSeconds: this.delay };
}
}
23 changes: 23 additions & 0 deletions actors/deposit-screener/src/screening/checks/apiCalls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DepositRequest } from "@nocturne-xyz/core";

export interface TrmData {
risk: number; // TODO, use vals from response
}
export interface MisttrackData {
misttrackRisk: number;
}

export type Data = TrmData | MisttrackData;

export const API_CALLS = {
// {{TRM_URL}}/public/v2/screening/addresses
TRM_SCREENING_ADDRESSES: async (deposit: DepositRequest) => {
console.log(deposit);
return await Promise.resolve({ risk: 0.5 });
},
// {{MISTTRACK_BASE_URL}}/risk_score
MISTTRACK_ADDRESS_RISK_SCORE: async (deposit: DepositRequest) => {
console.log(deposit);
return await Promise.resolve({ misttrackRisk: 0.5 });
},
} as const;
14 changes: 14 additions & 0 deletions actors/deposit-screener/src/screening/checks/poc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { DepositRequest } from "@nocturne-xyz/core";
import { RULESET_V1 } from "./v1/RuleSetV1";

/**
* USAGE
*/
const DUMMY_DEPOSIT_REQUEST = {} as DepositRequest;
RULESET_V1.check(DUMMY_DEPOSIT_REQUEST)
.then((result) => {
console.log(result);
})
.catch((err) => {
console.log(err);
});
23 changes: 23 additions & 0 deletions actors/deposit-screener/src/screening/checks/v1/RuleSetV1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Rule, RuleSet } from "../RuleSet";
import { MisttrackData, TrmData } from "../apiCalls";

const TRM_RULE_1 = new Rule({
call: "TRM_SCREENING_ADDRESSES",
threshold: (data: TrmData) => data.risk > 0.5,
action: { type: "Rejection", reason: "Risk is too high" },
});
const TRM_RULE_2 = new Rule({
call: "TRM_SCREENING_ADDRESSES",
threshold: (data: TrmData) => data.risk > 0.25,
action: { type: "Delay", timeSeconds: 1000 },
});
const MISTTRACK_RULE_1 = new Rule({
call: "MISTTRACK_ADDRESS_RISK_SCORE",
threshold: (data: MisttrackData) => data.misttrackRisk > 0.5,
action: { type: "Rejection", reason: "misttrackRisk is too high" },
});

export const RULESET_V1 = new RuleSet()
.add(TRM_RULE_1)
.add(TRM_RULE_2)
.add(MISTTRACK_RULE_1);
22 changes: 13 additions & 9 deletions packages/core/src/utils/functional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,21 @@ export function maxArray(arr: number[] | bigint[]): number | bigint {
return arr.reduce((curr, x) => max(curr, x));
}

export type Thunk<T> = () => Promise<T>;

export function thunk<T>(fn: () => Promise<T>): Thunk<T> {
let item: T | undefined;

return async () => {
if (!item) {
item = await fn();
export type Thunk<T, P extends any[] = any[]> = (...args: P) => Promise<T>;

export function thunk<T, P extends any[] = any[]>(
fn: (...args: P) => Promise<T>
): Thunk<T, P> {
const cache = new Map<string, T>(); // cached based on computed result from varying args

return async (...args: P) => {
const cacheKey = JSON.stringify(args);
if (!cache.has(cacheKey)) {
const result = await fn(...args);
cache.set(cacheKey, result);
}

return item;
return cache.get(cacheKey)!;
};
}

Expand Down

0 comments on commit 25556c7

Please sign in to comment.