mofu-sdk 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/index.d.ts +100 -0
- package/dist/index.js +172 -0
- package/dist/types.d.ts +177 -0
- package/dist/types.js +9 -0
- package/package.json +44 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mofu contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mofu-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Robinhood Chain token and wallet checks for agents, bots and scripts, from [Mofu](https://mofu.works).
|
|
4
|
+
|
|
5
|
+
- **Token Scanner:** what a contract allows (mint, pause, blacklist, fees, upgrades, owner), a simulated buy and sell with taxes, how big one sale can be before the price drops 10%, copycats, and whether the code matches known honeypots.
|
|
6
|
+
- **Wallet Health:** every active approval, re-checked on chain, with risky spenders flagged.
|
|
7
|
+
- **Webhooks:** a signed POST when a watched token's owner, supply, code, pause state, liquidity or verdict changes.
|
|
8
|
+
- **A buy rule** you can tune, so an agent refuses what Mofu flags.
|
|
9
|
+
|
|
10
|
+
No dependencies. Works wherever `fetch` and WebCrypto exist: Node.js 18+, browsers, Cloudflare Workers, Deno, Bun. The API is free: 50 calls a day per IP, shared with Mofu's MCP server and `mofu-cli`.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install mofu-sdk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Check a token
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Mofu } from "mofu-sdk";
|
|
20
|
+
|
|
21
|
+
const mofu = new Mofu();
|
|
22
|
+
const { report, url } = await mofu.scanToken("0xf76593c1c321c2a2f77775025e9677999ce7ea8c");
|
|
23
|
+
|
|
24
|
+
report.verdict; // "clean" | "caution" | "risky"
|
|
25
|
+
report.sellTest?.status; // "passed" | "failed"
|
|
26
|
+
report.sellTest?.depth?.ethAt10Percent; // ETH one sale can take before the price drops 10%
|
|
27
|
+
report.codeMatch; // same code as tokens whose sell failed, if any
|
|
28
|
+
url; // the shareable report on mofu.works
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Gate a buy
|
|
32
|
+
|
|
33
|
+
`checkBeforeBuy` scans and applies a rule. If Mofu can't be reached, the answer is no.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const d = await mofu.checkBeforeBuy(token, { maxSellTaxPercent: 5, minSellDepthEth: 0.5 });
|
|
37
|
+
if (!d.allowed) return console.log("skip:", d.reasons);
|
|
38
|
+
await yourBuy(token);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Option | Default | Blocks when |
|
|
42
|
+
| ------------------- | ------------ | -------------------------------------------------------------- |
|
|
43
|
+
| `blockVerdicts` | `["risky"]` | the verdict is in the list (any single risk check also blocks) |
|
|
44
|
+
| `requireSellTest` | `true` | no sell could be simulated |
|
|
45
|
+
| `maxBuyTaxPercent` | `10` | the simulated buy tax is higher |
|
|
46
|
+
| `maxSellTaxPercent` | `10` | the simulated sell tax is higher |
|
|
47
|
+
| `blockLookalikes` | `false` | another token uses the same name or ticker |
|
|
48
|
+
| `minSellDepthEth` | `null` (off) | a sale this size would move the price more than 10% |
|
|
49
|
+
|
|
50
|
+
`decide(report, policy)` is the same rule as a pure function, for reports you already have.
|
|
51
|
+
|
|
52
|
+
## Check a wallet
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const { report } = await mofu.checkWallet("0x…");
|
|
56
|
+
report.approvals.filter((a) => a.risk === "risk");
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Webhooks
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const { webhook, secret } = await mofu.webhooks.create({
|
|
63
|
+
url: "https://your.domain/mofu",
|
|
64
|
+
tokens: ["0x…"],
|
|
65
|
+
events: ["owner_changed", "liquidity_dropped", "verdict_changed"], // leave out for all six
|
|
66
|
+
});
|
|
67
|
+
// Mofu pinged your URL first; keep `secret`, it isn't shown again.
|
|
68
|
+
|
|
69
|
+
await mofu.webhooks.get(webhook.id, secret); // status, and when each token was last read
|
|
70
|
+
await mofu.webhooks.test(webhook.id, secret);
|
|
71
|
+
await mofu.webhooks.remove(webhook.id, secret);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
In your handler, verify the signature against the raw body:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { parseWebhook } from "mofu-sdk";
|
|
78
|
+
|
|
79
|
+
const event = await parseWebhook(request.headers.get("Mofu-Signature"), rawBody, secret);
|
|
80
|
+
if (event.type === "owner_changed") pause(event.token!.address);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`parseWebhook` throws when the signature is wrong or older than five minutes; `verifyWebhook` returns a boolean instead.
|
|
84
|
+
|
|
85
|
+
## Errors
|
|
86
|
+
|
|
87
|
+
Failures throw `MofuError` with `status` and `code`: `daily_limit` (429), `bad_address` (400), `chain_unreachable` (503), `network` (Mofu couldn't be reached).
|
|
88
|
+
|
|
89
|
+
## Not advice
|
|
90
|
+
|
|
91
|
+
Mofu reads what a contract's code allows and simulates trades; it can't see the future. A clean report doesn't make a token a good buy.
|
|
92
|
+
|
|
93
|
+
Docs: <https://mofu.works/docs/#api> · Source: <https://github.com/dev-mofu/mofu/tree/main/packages/sdk> · MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mofu-sdk: Mofu's Robinhood Chain checks for agents, bots and scripts.
|
|
3
|
+
* No dependencies; works wherever fetch and WebCrypto exist (Node 18+, browsers, Workers, Deno, Bun).
|
|
4
|
+
*/
|
|
5
|
+
import type { Checked, TokenReport, Verdict, WalletReport, Webhook, WebhookEvent, WebhookEventType } from "./types.js";
|
|
6
|
+
export * from "./types.js";
|
|
7
|
+
export interface MofuOptions {
|
|
8
|
+
/** Defaults to https://mofu.works. */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
/** Defaults to the global fetch. */
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
/** Sent as User-Agent where the runtime allows it. */
|
|
13
|
+
userAgent?: string;
|
|
14
|
+
/** Per request, in milliseconds. Defaults to 90 000 (a first scan can take half a minute). */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/** An answer from Mofu that isn't a report: a bad address, the daily limit, the chain unreachable. */
|
|
18
|
+
export declare class MofuError extends Error {
|
|
19
|
+
/** HTTP status, or 0 when Mofu couldn't be reached. */
|
|
20
|
+
readonly status: number;
|
|
21
|
+
/** Mofu's error code, e.g. "daily_limit", "bad_address", "network". */
|
|
22
|
+
readonly code: string;
|
|
23
|
+
name: string;
|
|
24
|
+
constructor(message: string,
|
|
25
|
+
/** HTTP status, or 0 when Mofu couldn't be reached. */
|
|
26
|
+
status: number,
|
|
27
|
+
/** Mofu's error code, e.g. "daily_limit", "bad_address", "network". */
|
|
28
|
+
code: string);
|
|
29
|
+
}
|
|
30
|
+
export declare class Mofu {
|
|
31
|
+
private readonly base;
|
|
32
|
+
private readonly fetchFn;
|
|
33
|
+
private readonly headers;
|
|
34
|
+
private readonly timeoutMs;
|
|
35
|
+
constructor(options?: MofuOptions);
|
|
36
|
+
private call;
|
|
37
|
+
private static address;
|
|
38
|
+
/** The Token Scanner report for a token contract on Robinhood Chain. */
|
|
39
|
+
scanToken(address: string): Promise<Checked<TokenReport>>;
|
|
40
|
+
/** Wallet Health: every active approval (risky ones flagged) and the tokens a wallet holds. */
|
|
41
|
+
checkWallet(address: string): Promise<Checked<WalletReport>>;
|
|
42
|
+
/** Scans a token and applies a buy rule. Says no, with a reason, when Mofu can't be reached. */
|
|
43
|
+
checkBeforeBuy(address: string, policy?: Partial<Policy>): Promise<Decision>;
|
|
44
|
+
readonly webhooks: {
|
|
45
|
+
/**
|
|
46
|
+
* Watches up to 10 tokens. Mofu pings the URL first and saves the webhook only if it answers
|
|
47
|
+
* 2xx. The secret comes back once: keep it.
|
|
48
|
+
*/
|
|
49
|
+
create: (input: {
|
|
50
|
+
url: string;
|
|
51
|
+
tokens: string[];
|
|
52
|
+
events?: WebhookEventType[];
|
|
53
|
+
}) => Promise<{
|
|
54
|
+
webhook: Webhook;
|
|
55
|
+
secret: string;
|
|
56
|
+
}>;
|
|
57
|
+
get: (id: string, secret: string) => Promise<Webhook>;
|
|
58
|
+
test: (id: string, secret: string) => Promise<{
|
|
59
|
+
delivered: boolean;
|
|
60
|
+
result: string;
|
|
61
|
+
}>;
|
|
62
|
+
remove: (id: string, secret: string) => Promise<void>;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export interface Policy {
|
|
66
|
+
/** Verdicts that block a buy. Default: ["risky"]. */
|
|
67
|
+
blockVerdicts: Verdict[];
|
|
68
|
+
/** Block when no sell could be simulated. Default: true. */
|
|
69
|
+
requireSellTest: boolean;
|
|
70
|
+
/** Default: 10. */
|
|
71
|
+
maxBuyTaxPercent: number;
|
|
72
|
+
/** Default: 10. */
|
|
73
|
+
maxSellTaxPercent: number;
|
|
74
|
+
/** Block when another token uses the same name or ticker. Default: false. */
|
|
75
|
+
blockLookalikes: boolean;
|
|
76
|
+
/** Block when one sale of this much ETH would move the price more than 10%. Default: off. */
|
|
77
|
+
minSellDepthEth: number | null;
|
|
78
|
+
}
|
|
79
|
+
export declare const DEFAULT_POLICY: Policy;
|
|
80
|
+
export interface Decision {
|
|
81
|
+
allowed: boolean;
|
|
82
|
+
/** Why it was blocked; empty when allowed. */
|
|
83
|
+
reasons: string[];
|
|
84
|
+
report: TokenReport | null;
|
|
85
|
+
}
|
|
86
|
+
/** Applies a buy rule to a report. Pure: test it, tune it, log it. */
|
|
87
|
+
export declare function decide(r: TokenReport, policy?: Partial<Policy>): Decision;
|
|
88
|
+
/**
|
|
89
|
+
* Checks a Mofu-Signature header (t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">) against
|
|
90
|
+
* the raw request body. Rejects signatures older than `toleranceSeconds` (default 300).
|
|
91
|
+
*/
|
|
92
|
+
export declare function verifyWebhook(signatureHeader: string | null | undefined, rawBody: string, secret: string, options?: {
|
|
93
|
+
toleranceSeconds?: number;
|
|
94
|
+
now?: number;
|
|
95
|
+
}): Promise<boolean>;
|
|
96
|
+
/** Verifies and parses a webhook delivery. Throws MofuError("bad_signature") when it doesn't check out. */
|
|
97
|
+
export declare function parseWebhook(signatureHeader: string | null | undefined, rawBody: string, secret: string, options?: {
|
|
98
|
+
toleranceSeconds?: number;
|
|
99
|
+
now?: number;
|
|
100
|
+
}): Promise<WebhookEvent>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
/** An answer from Mofu that isn't a report: a bad address, the daily limit, the chain unreachable. */
|
|
3
|
+
export class MofuError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
name = "MofuError";
|
|
7
|
+
constructor(message,
|
|
8
|
+
/** HTTP status, or 0 when Mofu couldn't be reached. */
|
|
9
|
+
status,
|
|
10
|
+
/** Mofu's error code, e.g. "daily_limit", "bad_address", "network". */
|
|
11
|
+
code) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
18
|
+
export class Mofu {
|
|
19
|
+
base;
|
|
20
|
+
fetchFn;
|
|
21
|
+
headers;
|
|
22
|
+
timeoutMs;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.base = (options.baseUrl ?? "https://mofu.works").replace(/\/+$/, "");
|
|
25
|
+
this.fetchFn = options.fetch ?? ((input, init) => fetch(input, init));
|
|
26
|
+
this.headers = {
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
"User-Agent": options.userAgent ?? "mofu-sdk/0.1",
|
|
29
|
+
};
|
|
30
|
+
this.timeoutMs = options.timeoutMs ?? 90_000;
|
|
31
|
+
}
|
|
32
|
+
async call(path, init = {}) {
|
|
33
|
+
let res;
|
|
34
|
+
try {
|
|
35
|
+
res = await this.fetchFn(`${this.base}${path}`, {
|
|
36
|
+
...init,
|
|
37
|
+
headers: {
|
|
38
|
+
...this.headers,
|
|
39
|
+
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
40
|
+
...init.headers,
|
|
41
|
+
},
|
|
42
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
throw new MofuError(`Couldn't reach Mofu: ${err.message}`, 0, "network");
|
|
47
|
+
}
|
|
48
|
+
const body = (await res.json().catch(() => null));
|
|
49
|
+
if (!res.ok || !body) {
|
|
50
|
+
throw new MofuError(body?.error?.message ?? `Mofu answered HTTP ${res.status}`, res.status, body?.error?.code ?? "http_error");
|
|
51
|
+
}
|
|
52
|
+
return body;
|
|
53
|
+
}
|
|
54
|
+
static address(a, what) {
|
|
55
|
+
if (!ADDRESS.test(a))
|
|
56
|
+
throw new MofuError(`${what} must be 0x followed by 40 hex characters`, 400, "bad_address");
|
|
57
|
+
return a.toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
/** The Token Scanner report for a token contract on Robinhood Chain. */
|
|
60
|
+
scanToken(address) {
|
|
61
|
+
return this.call(`/api/v1/token/${Mofu.address(address, "address")}`);
|
|
62
|
+
}
|
|
63
|
+
/** Wallet Health: every active approval (risky ones flagged) and the tokens a wallet holds. */
|
|
64
|
+
checkWallet(address) {
|
|
65
|
+
return this.call(`/api/v1/wallet/${Mofu.address(address, "address")}`);
|
|
66
|
+
}
|
|
67
|
+
/** Scans a token and applies a buy rule. Says no, with a reason, when Mofu can't be reached. */
|
|
68
|
+
async checkBeforeBuy(address, policy = {}) {
|
|
69
|
+
try {
|
|
70
|
+
return decide((await this.scanToken(address)).report, policy);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return {
|
|
74
|
+
allowed: false,
|
|
75
|
+
reasons: [`no answer from Mofu: ${err.message}`],
|
|
76
|
+
report: null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
webhooks = {
|
|
81
|
+
/**
|
|
82
|
+
* Watches up to 10 tokens. Mofu pings the URL first and saves the webhook only if it answers
|
|
83
|
+
* 2xx. The secret comes back once: keep it.
|
|
84
|
+
*/
|
|
85
|
+
create: (input) => this.call("/api/v1/webhooks", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
body: JSON.stringify(input),
|
|
88
|
+
}),
|
|
89
|
+
get: async (id, secret) => (await this.call(`/api/v1/webhooks/${id}`, { headers: auth(secret) }))
|
|
90
|
+
.webhook,
|
|
91
|
+
test: (id, secret) => this.call(`/api/v1/webhooks/${id}/test`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: auth(secret),
|
|
94
|
+
}),
|
|
95
|
+
remove: async (id, secret) => {
|
|
96
|
+
await this.call(`/api/v1/webhooks/${id}`, { method: "DELETE", headers: auth(secret) });
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const auth = (secret) => ({ Authorization: `Bearer ${secret}` });
|
|
101
|
+
export const DEFAULT_POLICY = {
|
|
102
|
+
blockVerdicts: ["risky"],
|
|
103
|
+
requireSellTest: true,
|
|
104
|
+
maxBuyTaxPercent: 10,
|
|
105
|
+
maxSellTaxPercent: 10,
|
|
106
|
+
blockLookalikes: false,
|
|
107
|
+
minSellDepthEth: null,
|
|
108
|
+
};
|
|
109
|
+
/** Applies a buy rule to a report. Pure: test it, tune it, log it. */
|
|
110
|
+
export function decide(r, policy = {}) {
|
|
111
|
+
const p = { ...DEFAULT_POLICY, ...policy };
|
|
112
|
+
const reasons = [];
|
|
113
|
+
if (!r.isContract)
|
|
114
|
+
reasons.push("not a token contract");
|
|
115
|
+
if (p.blockVerdicts.includes(r.verdict))
|
|
116
|
+
reasons.push(`Mofu's verdict is ${r.verdict}`);
|
|
117
|
+
for (const c of r.checks)
|
|
118
|
+
if (c.status === "risk")
|
|
119
|
+
reasons.push(`${c.label} ${c.detail}`);
|
|
120
|
+
const t = r.sellTest;
|
|
121
|
+
if (t?.status === "failed")
|
|
122
|
+
reasons.push("a simulated sell failed (possible honeypot)");
|
|
123
|
+
else if (!t || t.status !== "passed") {
|
|
124
|
+
if (p.requireSellTest)
|
|
125
|
+
reasons.push("Mofu couldn't test selling it");
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if ((t.buyTaxPercent ?? 0) > p.maxBuyTaxPercent)
|
|
129
|
+
reasons.push(`buy tax ${t.buyTaxPercent}% is over ${p.maxBuyTaxPercent}%`);
|
|
130
|
+
if ((t.sellTaxPercent ?? 0) > p.maxSellTaxPercent)
|
|
131
|
+
reasons.push(`sell tax ${t.sellTaxPercent}% is over ${p.maxSellTaxPercent}%`);
|
|
132
|
+
if (p.minSellDepthEth !== null) {
|
|
133
|
+
const d = t.depth;
|
|
134
|
+
if (!d)
|
|
135
|
+
reasons.push("sell depth couldn't be measured");
|
|
136
|
+
else if (!d.moreThan && (d.ethAt10Percent ?? 0) < p.minSellDepthEth)
|
|
137
|
+
reasons.push(`a ${p.minSellDepthEth} ETH sale would move the price more than 10% (depth about ${d.ethAt10Percent ?? 0} ETH)`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (p.blockLookalikes && r.lookalikes.length)
|
|
141
|
+
reasons.push(`${r.lookalikes.length} other token(s) use the same name or ticker`);
|
|
142
|
+
return { allowed: reasons.length === 0, reasons: [...new Set(reasons)], report: r };
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------- webhooks
|
|
145
|
+
/**
|
|
146
|
+
* Checks a Mofu-Signature header (t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">) against
|
|
147
|
+
* the raw request body. Rejects signatures older than `toleranceSeconds` (default 300).
|
|
148
|
+
*/
|
|
149
|
+
export async function verifyWebhook(signatureHeader, rawBody, secret, options = {}) {
|
|
150
|
+
const parts = Object.fromEntries((signatureHeader ?? "").split(",").map((p) => p.trim().split("=", 2)));
|
|
151
|
+
const t = Number(parts.t);
|
|
152
|
+
const now = Math.floor((options.now ?? Date.now()) / 1000);
|
|
153
|
+
if (!Number.isFinite(t) || !parts.v1 || Math.abs(now - t) > (options.toleranceSeconds ?? 300))
|
|
154
|
+
return false;
|
|
155
|
+
const enc = new TextEncoder();
|
|
156
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
157
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, enc.encode(`${t}.${rawBody}`)));
|
|
158
|
+
const expected = [...mac].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
159
|
+
const given = parts.v1.toLowerCase();
|
|
160
|
+
if (given.length !== expected.length)
|
|
161
|
+
return false;
|
|
162
|
+
let diff = 0;
|
|
163
|
+
for (let i = 0; i < expected.length; i++)
|
|
164
|
+
diff |= expected.charCodeAt(i) ^ given.charCodeAt(i);
|
|
165
|
+
return diff === 0;
|
|
166
|
+
}
|
|
167
|
+
/** Verifies and parses a webhook delivery. Throws MofuError("bad_signature") when it doesn't check out. */
|
|
168
|
+
export async function parseWebhook(signatureHeader, rawBody, secret, options) {
|
|
169
|
+
if (!(await verifyWebhook(signatureHeader, rawBody, secret, options)))
|
|
170
|
+
throw new MofuError("The webhook signature doesn't match", 401, "bad_signature");
|
|
171
|
+
return JSON.parse(rawBody);
|
|
172
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** Report shapes returned by Mofu's API (mofu.works/api/v1). */
|
|
2
|
+
export type Verdict = "clean" | "caution" | "risky";
|
|
3
|
+
export interface TokenCheck {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
status: "ok" | "warn" | "risk" | "info";
|
|
7
|
+
detail: string;
|
|
8
|
+
}
|
|
9
|
+
/** The biggest single sale before the price drops 10%. */
|
|
10
|
+
export interface SellDepth {
|
|
11
|
+
method: "simulated" | "reserves";
|
|
12
|
+
/** ETH that sale returns; null when even the smallest size moved the price more than 10%. */
|
|
13
|
+
ethAt10Percent: number | null;
|
|
14
|
+
/** True when every size tried stayed under 10%: the real depth is bigger. */
|
|
15
|
+
moreThan: boolean;
|
|
16
|
+
steps: Array<{
|
|
17
|
+
supplyPercent: number | null;
|
|
18
|
+
ethOut: number;
|
|
19
|
+
impactPercent: number;
|
|
20
|
+
}>;
|
|
21
|
+
detail: string;
|
|
22
|
+
}
|
|
23
|
+
/** A buy and a sell simulated on Robinhood Chain; nothing is sent. */
|
|
24
|
+
export interface SellTest {
|
|
25
|
+
status: "passed" | "failed" | "unknown";
|
|
26
|
+
method: "curve" | "transfer";
|
|
27
|
+
venue: string;
|
|
28
|
+
buyTaxPercent: number | null;
|
|
29
|
+
sellTaxPercent: number | null;
|
|
30
|
+
roundTripPercent: number | null;
|
|
31
|
+
depth: SellDepth | null;
|
|
32
|
+
detail: string;
|
|
33
|
+
}
|
|
34
|
+
/** Same code as tokens whose simulated sell failed. */
|
|
35
|
+
export interface CodeMatch {
|
|
36
|
+
flagged: number;
|
|
37
|
+
passed: number;
|
|
38
|
+
examples: Array<{
|
|
39
|
+
address: string;
|
|
40
|
+
symbol: string | null;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export interface TokenReport {
|
|
44
|
+
address: string;
|
|
45
|
+
chainName: string;
|
|
46
|
+
isContract: boolean;
|
|
47
|
+
codeSize: number;
|
|
48
|
+
token: {
|
|
49
|
+
name: string | null;
|
|
50
|
+
symbol: string | null;
|
|
51
|
+
decimals: number | null;
|
|
52
|
+
totalSupply: string | null;
|
|
53
|
+
};
|
|
54
|
+
owner: {
|
|
55
|
+
status: "none" | "renounced" | "active";
|
|
56
|
+
address: string | null;
|
|
57
|
+
holdsPercent: number | null;
|
|
58
|
+
};
|
|
59
|
+
proxy: {
|
|
60
|
+
kind: "none" | "eip1967" | "beacon" | "minimal";
|
|
61
|
+
implementation: string | null;
|
|
62
|
+
};
|
|
63
|
+
burnedPercent: number | null;
|
|
64
|
+
checks: TokenCheck[];
|
|
65
|
+
verdict: Verdict;
|
|
66
|
+
distribution: {
|
|
67
|
+
status: "full" | "summary" | "unavailable";
|
|
68
|
+
top10Percent: number | null;
|
|
69
|
+
largestWallet: {
|
|
70
|
+
address: string;
|
|
71
|
+
percent: number;
|
|
72
|
+
} | null;
|
|
73
|
+
holders: Array<{
|
|
74
|
+
address: string;
|
|
75
|
+
balance: string;
|
|
76
|
+
percent: number;
|
|
77
|
+
kind: "wallet" | "contract" | "burn";
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
sellTest: SellTest | null;
|
|
81
|
+
lookalikes: Array<{
|
|
82
|
+
address: string;
|
|
83
|
+
name: string | null;
|
|
84
|
+
symbol: string | null;
|
|
85
|
+
createdAt: string | null;
|
|
86
|
+
}>;
|
|
87
|
+
fingerprint: string | null;
|
|
88
|
+
codeMatch: CodeMatch | null;
|
|
89
|
+
limits: string[];
|
|
90
|
+
}
|
|
91
|
+
export interface AddressInfo {
|
|
92
|
+
address: string;
|
|
93
|
+
kind: "wallet" | "burn" | "token" | "curve" | "pool" | "contract";
|
|
94
|
+
label: string | null;
|
|
95
|
+
known: boolean;
|
|
96
|
+
txCount: number | null;
|
|
97
|
+
firstSeen: string | null;
|
|
98
|
+
ageDays: number | null;
|
|
99
|
+
}
|
|
100
|
+
export interface WalletApproval {
|
|
101
|
+
token: string;
|
|
102
|
+
tokenName: string | null;
|
|
103
|
+
tokenSymbol: string | null;
|
|
104
|
+
balance: string | null;
|
|
105
|
+
spender: AddressInfo;
|
|
106
|
+
unlimited: boolean;
|
|
107
|
+
allowance: string;
|
|
108
|
+
approvedAt: string | null;
|
|
109
|
+
txHash: string | null;
|
|
110
|
+
risk: "ok" | "warn" | "risk";
|
|
111
|
+
reasons: string[];
|
|
112
|
+
}
|
|
113
|
+
export interface WalletReport {
|
|
114
|
+
address: string;
|
|
115
|
+
isContract: boolean;
|
|
116
|
+
eth: string | null;
|
|
117
|
+
holdings: Array<{
|
|
118
|
+
token: string;
|
|
119
|
+
name: string | null;
|
|
120
|
+
symbol: string | null;
|
|
121
|
+
balance: string;
|
|
122
|
+
}>;
|
|
123
|
+
holdingsTotal: number;
|
|
124
|
+
approvals: WalletApproval[];
|
|
125
|
+
status: {
|
|
126
|
+
holdings: "ok" | "unavailable";
|
|
127
|
+
approvals: "ok" | "unavailable";
|
|
128
|
+
};
|
|
129
|
+
checkedAt: string;
|
|
130
|
+
limits: string[];
|
|
131
|
+
}
|
|
132
|
+
/** A report plus where to see it and how many calls are left today. */
|
|
133
|
+
export interface Checked<T> {
|
|
134
|
+
report: T;
|
|
135
|
+
url: string;
|
|
136
|
+
remaining: number;
|
|
137
|
+
limit: number;
|
|
138
|
+
}
|
|
139
|
+
export declare const WEBHOOK_EVENTS: readonly ["owner_changed", "supply_changed", "upgraded", "pause_changed", "liquidity_dropped", "verdict_changed"];
|
|
140
|
+
export type WebhookEventType = (typeof WEBHOOK_EVENTS)[number];
|
|
141
|
+
export interface Webhook {
|
|
142
|
+
id: string;
|
|
143
|
+
url: string;
|
|
144
|
+
tokens: string[];
|
|
145
|
+
events: WebhookEventType[];
|
|
146
|
+
createdAt: string;
|
|
147
|
+
status: "active" | "disabled";
|
|
148
|
+
disabledAt: string | null;
|
|
149
|
+
failures: number;
|
|
150
|
+
delivered: number;
|
|
151
|
+
lastDeliveryAt: string | null;
|
|
152
|
+
lastResult: string | null;
|
|
153
|
+
/** In a status lookup: when Mofu last read each token. */
|
|
154
|
+
watching?: Array<{
|
|
155
|
+
address: string;
|
|
156
|
+
symbol: string | null;
|
|
157
|
+
verdict: Verdict | null;
|
|
158
|
+
lastChecked: string | null;
|
|
159
|
+
lastScanned: string | null;
|
|
160
|
+
}>;
|
|
161
|
+
}
|
|
162
|
+
/** What Mofu POSTs to a webhook URL. */
|
|
163
|
+
export interface WebhookEvent {
|
|
164
|
+
id: string;
|
|
165
|
+
type: WebhookEventType | "ping" | "test";
|
|
166
|
+
createdAt: string;
|
|
167
|
+
token: {
|
|
168
|
+
address: string;
|
|
169
|
+
name: string | null;
|
|
170
|
+
symbol: string | null;
|
|
171
|
+
} | null;
|
|
172
|
+
summary: string;
|
|
173
|
+
before: unknown;
|
|
174
|
+
after: unknown;
|
|
175
|
+
report: string | null;
|
|
176
|
+
webhook: string;
|
|
177
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mofu-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Robinhood Chain token and wallet checks for agents, bots and scripts: Mofu's Token Scanner, Wallet Health, webhooks and a buy rule.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://mofu.works/docs/#sdk",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/dev-mofu/mofu.git",
|
|
10
|
+
"directory": "packages/sdk"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/dev-mofu/mofu/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"robinhood-chain",
|
|
17
|
+
"token-scanner",
|
|
18
|
+
"honeypot",
|
|
19
|
+
"web3",
|
|
20
|
+
"trading-agent",
|
|
21
|
+
"webhooks",
|
|
22
|
+
"security"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"prepack": "tsc -p tsconfig.build.json"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.17"
|
|
45
|
+
}
|
|
46
|
+
}
|