@bitsocial/pubsub-votes 0.0.2
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 +674 -0
- package/README.md +212 -0
- package/dist/chain/bucket.d.ts +13 -0
- package/dist/chain/bucket.js +24 -0
- package/dist/chain/ticker.d.ts +15 -0
- package/dist/chain/ticker.js +25 -0
- package/dist/chain/types.d.ts +90 -0
- package/dist/chain/types.js +1 -0
- package/dist/checkpoint/codec.d.ts +54 -0
- package/dist/checkpoint/codec.js +99 -0
- package/dist/client/root-puller.d.ts +49 -0
- package/dist/client/root-puller.js +140 -0
- package/dist/client/voter.d.ts +225 -0
- package/dist/client/voter.js +1195 -0
- package/dist/crdt/codec.d.ts +41 -0
- package/dist/crdt/codec.js +137 -0
- package/dist/crdt/crdt.d.ts +22 -0
- package/dist/crdt/crdt.js +127 -0
- package/dist/crdt/store.d.ts +8 -0
- package/dist/crdt/store.js +23 -0
- package/dist/crdt/types.d.ts +87 -0
- package/dist/crdt/types.js +1 -0
- package/dist/encoding/canonical.d.ts +22 -0
- package/dist/encoding/canonical.js +26 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.js +111 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +39 -0
- package/dist/rules/constant.d.ts +14 -0
- package/dist/rules/constant.js +18 -0
- package/dist/rules/erc20-balance.d.ts +30 -0
- package/dist/rules/erc20-balance.js +44 -0
- package/dist/rules/erc721-min-balance.d.ts +18 -0
- package/dist/rules/erc721-min-balance.js +56 -0
- package/dist/rules/registry.d.ts +42 -0
- package/dist/rules/registry.js +61 -0
- package/dist/rules/types.d.ts +74 -0
- package/dist/rules/types.js +1 -0
- package/dist/schema/common.d.ts +25 -0
- package/dist/schema/common.js +24 -0
- package/dist/schema/criteria.d.ts +78 -0
- package/dist/schema/criteria.js +76 -0
- package/dist/schema/directory.d.ts +42 -0
- package/dist/schema/directory.js +52 -0
- package/dist/schema/votes.d.ts +55 -0
- package/dist/schema/votes.js +116 -0
- package/dist/signer/eip712.d.ts +103 -0
- package/dist/signer/eip712.js +85 -0
- package/dist/signer/types.d.ts +31 -0
- package/dist/signer/types.js +1 -0
- package/dist/storage/browser.d.ts +3 -0
- package/dist/storage/browser.js +110 -0
- package/dist/storage/memory.d.ts +10 -0
- package/dist/storage/memory.js +56 -0
- package/dist/storage/node.d.ts +5 -0
- package/dist/storage/node.js +106 -0
- package/dist/storage/types.d.ts +46 -0
- package/dist/storage/types.js +1 -0
- package/dist/store/indexeddb.d.ts +9 -0
- package/dist/store/indexeddb.js +72 -0
- package/dist/store/memory.d.ts +15 -0
- package/dist/store/memory.js +22 -0
- package/dist/store/select.d.ts +15 -0
- package/dist/store/select.js +64 -0
- package/dist/store/sqlite.d.ts +11 -0
- package/dist/store/sqlite.js +68 -0
- package/dist/store/types.d.ts +57 -0
- package/dist/store/types.js +1 -0
- package/dist/tally/tally.d.ts +44 -0
- package/dist/tally/tally.js +89 -0
- package/dist/tally/types.d.ts +51 -0
- package/dist/tally/types.js +13 -0
- package/dist/topic.d.ts +20 -0
- package/dist/topic.js +28 -0
- package/dist/transport/accepted-dedup.d.ts +30 -0
- package/dist/transport/accepted-dedup.js +34 -0
- package/dist/transport/announce/browser.d.ts +9 -0
- package/dist/transport/announce/browser.js +14 -0
- package/dist/transport/announce/node.d.ts +38 -0
- package/dist/transport/announce/node.js +162 -0
- package/dist/transport/announce/types.d.ts +74 -0
- package/dist/transport/announce/types.js +16 -0
- package/dist/transport/bundle-store.d.ts +11 -0
- package/dist/transport/bundle-store.js +34 -0
- package/dist/transport/chase.d.ts +83 -0
- package/dist/transport/chase.js +88 -0
- package/dist/transport/gossip-validator.d.ts +107 -0
- package/dist/transport/gossip-validator.js +99 -0
- package/dist/transport/helia.d.ts +41 -0
- package/dist/transport/helia.js +98 -0
- package/dist/transport/integration/harness.d.ts +60 -0
- package/dist/transport/integration/harness.js +262 -0
- package/dist/transport/messages.d.ts +97 -0
- package/dist/transport/messages.js +117 -0
- package/dist/transport/rate-limit.d.ts +11 -0
- package/dist/transport/rate-limit.js +20 -0
- package/dist/transport/transport.d.ts +20 -0
- package/dist/transport/transport.js +35 -0
- package/dist/transport/types.d.ts +165 -0
- package/dist/transport/types.js +1 -0
- package/dist/verify/background.d.ts +81 -0
- package/dist/verify/background.js +236 -0
- package/dist/verify/bundle.d.ts +58 -0
- package/dist/verify/bundle.js +84 -0
- package/dist/verify/cache.d.ts +48 -0
- package/dist/verify/cache.js +62 -0
- package/dist/verify/constraints.d.ts +16 -0
- package/dist/verify/constraints.js +35 -0
- package/dist/verify/gate-result-cache.d.ts +65 -0
- package/dist/verify/gate-result-cache.js +91 -0
- package/dist/verify/name-resolution-cache.d.ts +59 -0
- package/dist/verify/name-resolution-cache.js +64 -0
- package/dist/verify/signature.d.ts +9 -0
- package/dist/verify/signature.js +55 -0
- package/dist/verify/types.d.ts +101 -0
- package/dist/verify/types.js +1 -0
- package/package.json +77 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library error types.
|
|
3
|
+
*
|
|
4
|
+
* The engine and client lifecycle are implemented — schemas, encoding, topic derivation,
|
|
5
|
+
* the verify pipeline, the LWW winner-set CRDT, the tally, the transport's
|
|
6
|
+
* validate-before-forward gossip gate, and the reactive `PubsubVoter` / `Contest` / `ContestVote`
|
|
7
|
+
* facade. Republishing a live vote is the client's job (no scheduler, no persistence); see
|
|
8
|
+
* DESIGN.md "Republishing is the client's job".
|
|
9
|
+
*/
|
|
10
|
+
/** Thrown by any facade path deferred to a later version (none in v1's shipped surface). */
|
|
11
|
+
export class NotImplementedError extends Error {
|
|
12
|
+
constructor(what) {
|
|
13
|
+
super(`${what} is not implemented yet. See DESIGN.md / ROADMAP.md.`);
|
|
14
|
+
this.name = "NotImplementedError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when a criteria document names a rule `type` this client does not
|
|
19
|
+
* implement (in the `rule`/`weight` slot or in `requires.rules`). A
|
|
20
|
+
* client that hits this is too old (or missing a host override) and must recuse
|
|
21
|
+
* itself from the contest rather than miscount. See DESIGN.md "Rules".
|
|
22
|
+
*/
|
|
23
|
+
export class UnknownRuleError extends Error {
|
|
24
|
+
slot;
|
|
25
|
+
type;
|
|
26
|
+
constructor(slot, type) {
|
|
27
|
+
super(`Unknown rule "${type}" referenced by the criteria ${slot}. This client does not ` +
|
|
28
|
+
`implement it; pass it via the \`rules\` option to shadow/extend the built-ins, ` +
|
|
29
|
+
`or recuse this contest. Built-ins: see registry.ts.`);
|
|
30
|
+
this.slot = slot;
|
|
31
|
+
this.type = type;
|
|
32
|
+
this.name = "UnknownRuleError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Thrown at construction when the injected Helia node's libp2p has no usable pubsub
|
|
37
|
+
* (gossipsub) service at `libp2p.services.pubsub`. The library broadcasts and receives
|
|
38
|
+
* winner bundle CIDs over gossipsub, so a node without it cannot participate. Helia's default
|
|
39
|
+
* libp2p services do NOT include pubsub — the host must register a gossipsub service
|
|
40
|
+
* (e.g. `@chainsafe/libp2p-gossipsub`) before passing the node in. We fail fast here
|
|
41
|
+
* rather than letting a later `publish`/`subscribe` fail obscurely. See DESIGN.md
|
|
42
|
+
* "Transport".
|
|
43
|
+
*/
|
|
44
|
+
export class MissingPubsubError extends Error {
|
|
45
|
+
constructor() {
|
|
46
|
+
super("The injected Helia node's libp2p has no usable pubsub service at " +
|
|
47
|
+
"`libp2p.services.pubsub`. This library needs a gossipsub service to broadcast " +
|
|
48
|
+
"and receive votes. Register one before constructing PubsubVoter (Helia's " +
|
|
49
|
+
"default services do not include pubsub; e.g. add `@chainsafe/libp2p-gossipsub` " +
|
|
50
|
+
"as `services.pubsub`). See DESIGN.md \"Transport\".");
|
|
51
|
+
this.name = "MissingPubsubError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Thrown at construction when the injected Helia node has no usable `blockstore`. Vote
|
|
56
|
+
* bundles are immutable content-addressed blocks fetched by CID through the host's
|
|
57
|
+
* blockstore (bitswap retrieves through it), so the engine cannot resolve a bundle without
|
|
58
|
+
* one. Note "bitswap" is not a separately introspectable property of a Helia node — it
|
|
59
|
+
* is a block broker wired *beneath* `blockstore` — so the checkable guarantee is a
|
|
60
|
+
* well-formed blockstore, the surface bitswap retrieves through. See DESIGN.md
|
|
61
|
+
* "Transport".
|
|
62
|
+
*/
|
|
63
|
+
export class MissingBlockstoreError extends Error {
|
|
64
|
+
constructor() {
|
|
65
|
+
super("The injected Helia node has no usable `blockstore`. This library fetches vote " +
|
|
66
|
+
"bundles by CID through it (bitswap retrieves through the blockstore), so a " +
|
|
67
|
+
"node without one cannot resolve votes. Pass a full Helia instance (e.g. the " +
|
|
68
|
+
"result of `createHelia`). See DESIGN.md \"Transport\".");
|
|
69
|
+
this.name = "MissingBlockstoreError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Thrown at construction when the injected Helia node's libp2p has no usable **fetch
|
|
74
|
+
* service** at `libp2p.services.fetch`. The root-record pull — cold-start / reconnect
|
|
75
|
+
* checkpoint sync — rides the libp2p fetch protocol (this library registers its own lookup
|
|
76
|
+
* and runs its own requester), so the host must register `@libp2p/fetch` on the shared
|
|
77
|
+
* node. See DESIGN.md "Checkpoints" and "Deferred pkc-js work".
|
|
78
|
+
*/
|
|
79
|
+
export class MissingFetchError extends Error {
|
|
80
|
+
constructor() {
|
|
81
|
+
super("The injected Helia node's libp2p has no usable fetch service at " +
|
|
82
|
+
"`libp2p.services.fetch`. This library pulls peers' checkpoint root records " +
|
|
83
|
+
"over the libp2p fetch protocol on cold start, so the service is required. " +
|
|
84
|
+
"Register `@libp2p/fetch` as `services.fetch` before constructing PubsubVoter. " +
|
|
85
|
+
"See DESIGN.md \"Checkpoints\".");
|
|
86
|
+
this.name = "MissingFetchError";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Thrown once a voter has been `destroy()`ed and something tries to keep using it. Unlike `stop()`
|
|
91
|
+
* (which leaves every topic but keeps the client reusable), `destroy()` is terminal: every contest
|
|
92
|
+
* is stopped and can no longer update or publish. Surfaced by `createContest` / `createContestVote`,
|
|
93
|
+
* and by a pre-existing `Contest.update()` / `ContestVote.publish()`. Construct a new
|
|
94
|
+
* `PubsubVoter` to participate again.
|
|
95
|
+
*/
|
|
96
|
+
export class VoterDestroyedError extends Error {
|
|
97
|
+
constructor() {
|
|
98
|
+
super("This voter has been destroyed: its contests are stopped and can no longer update or " +
|
|
99
|
+
"publish. `destroy()` is terminal (unlike the reusable `stop()`). Construct a new " +
|
|
100
|
+
"PubsubVoter to participate again.");
|
|
101
|
+
this.name = "VoterDestroyedError";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Thrown when a publish (vote/withdraw) is attempted on a voter constructed without a signer. */
|
|
105
|
+
export class ReadOnlyError extends Error {
|
|
106
|
+
constructor() {
|
|
107
|
+
super("This voter is read-only: it was constructed without a `signer`. " +
|
|
108
|
+
"Provide a VoteSigner to publish or withdraw votes; reading tallies needs no signer.");
|
|
109
|
+
this.name = "ReadOnlyError";
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry.
|
|
3
|
+
*
|
|
4
|
+
* The engine is implemented and unit-tested: the zod schemas, canonical dag-cbor encoding,
|
|
5
|
+
* topic derivation, the verify pipeline (signature + constraints +
|
|
6
|
+
* gate + name resolution), the state-based grow-only LWW winner-set CRDT, the tally, and the
|
|
7
|
+
* transport's validate-before-forward gossip gate. The public facade is the reactive
|
|
8
|
+
* `PubsubVoter` / `Contest` (`createContest`) / `ContestVote` (`createContestVote`) trio. Keeping a
|
|
9
|
+
* live vote from decaying is the consuming client's job — this library publishes each vote once
|
|
10
|
+
* (see `republishIntervalBuckets` and DESIGN.md "Republishing is the client's job"). See DESIGN.md
|
|
11
|
+
* for architecture, and "Transport" for the forward-gate that verifies a bundle (signature,
|
|
12
|
+
* on-chain gate, community-name resolution) before gossipsub re-forwards it.
|
|
13
|
+
*/
|
|
14
|
+
export * from "./schema/common.js";
|
|
15
|
+
export * from "./schema/votes.js";
|
|
16
|
+
export * from "./schema/criteria.js";
|
|
17
|
+
export * from "./rules/erc721-min-balance.js";
|
|
18
|
+
export * from "./rules/constant.js";
|
|
19
|
+
export * from "./rules/registry.js";
|
|
20
|
+
export * from "./encoding/canonical.js";
|
|
21
|
+
export * from "./topic.js";
|
|
22
|
+
export * from "./errors.js";
|
|
23
|
+
export * from "./client/voter.js";
|
|
24
|
+
export * from "./signer/eip712.js";
|
|
25
|
+
export type { VoteSigner } from "./signer/types.js";
|
|
26
|
+
export type * from "./rules/types.js";
|
|
27
|
+
export type * from "./chain/types.js";
|
|
28
|
+
export type * from "./verify/types.js";
|
|
29
|
+
export type * from "./crdt/types.js";
|
|
30
|
+
export type * from "./transport/types.js";
|
|
31
|
+
export type * from "./tally/types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry.
|
|
3
|
+
*
|
|
4
|
+
* The engine is implemented and unit-tested: the zod schemas, canonical dag-cbor encoding,
|
|
5
|
+
* topic derivation, the verify pipeline (signature + constraints +
|
|
6
|
+
* gate + name resolution), the state-based grow-only LWW winner-set CRDT, the tally, and the
|
|
7
|
+
* transport's validate-before-forward gossip gate. The public facade is the reactive
|
|
8
|
+
* `PubsubVoter` / `Contest` (`createContest`) / `ContestVote` (`createContestVote`) trio. Keeping a
|
|
9
|
+
* live vote from decaying is the consuming client's job — this library publishes each vote once
|
|
10
|
+
* (see `republishIntervalBuckets` and DESIGN.md "Republishing is the client's job"). See DESIGN.md
|
|
11
|
+
* for architecture, and "Transport" for the forward-gate that verifies a bundle (signature,
|
|
12
|
+
* on-chain gate, community-name resolution) before gossipsub re-forwards it.
|
|
13
|
+
*/
|
|
14
|
+
// Schemas (runtime values) and their inferred types.
|
|
15
|
+
export * from "./schema/common.js";
|
|
16
|
+
export * from "./schema/votes.js";
|
|
17
|
+
export * from "./schema/criteria.js";
|
|
18
|
+
// Rules: a single kind, one file per `type`, each owning its option schema and an
|
|
19
|
+
// `evaluate` returning `{ score: bigint }` (the `rule` slot gates on `score > 0n`,
|
|
20
|
+
// the weight slot uses the magnitude), plus the registry (built-ins, the host-shadowing resolver, and criteria
|
|
21
|
+
// validation). The leaf rules read through the injected viem `PublicClient`
|
|
22
|
+
// (`ctx.chain`). Rule composition (combining several into one slot) is a
|
|
23
|
+
// documented future extension, not a built-in — see DESIGN.md "Future improvements".
|
|
24
|
+
// v1 ships the NFT path only: `erc721-min-balance` + `constant`. `erc20-balance` stays in
|
|
25
|
+
// the tree but is not registered or re-exported — see ROADMAP.md ("Deferred").
|
|
26
|
+
export * from "./rules/erc721-min-balance.js";
|
|
27
|
+
export * from "./rules/constant.js";
|
|
28
|
+
export * from "./rules/registry.js";
|
|
29
|
+
// Implemented runtime: encoding, topic, errors, identity seam, facade.
|
|
30
|
+
export * from "./encoding/canonical.js";
|
|
31
|
+
export * from "./topic.js";
|
|
32
|
+
export * from "./errors.js";
|
|
33
|
+
export * from "./client/voter.js";
|
|
34
|
+
// There is no library-side vote persistence: republishing a live vote is the client's job, so
|
|
35
|
+
// the client tracks what it voted for (see DESIGN.md "Republishing is the client's job"). The
|
|
36
|
+
// facade exports `republishIntervalBuckets` to help the client schedule its own refreshes.
|
|
37
|
+
// Identity seam: the EIP-712 ballot builder + constants a host needs to implement a
|
|
38
|
+
// signer, plus the signer interface itself.
|
|
39
|
+
export * from "./signer/eip712.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Rule } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A fixed score for every wallet. v1.
|
|
5
|
+
*
|
|
6
|
+
* In the weight slot this is "1 pass = 1 vote" (`value: 1`). In the rule slot a
|
|
7
|
+
* positive `value` admits everyone (a no-op gate). No chain read.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ConstantOptionsSchema: z.ZodObject<{
|
|
10
|
+
type: z.ZodLiteral<"constant">;
|
|
11
|
+
value: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
export type ConstantOptions = z.infer<typeof ConstantOptionsSchema>;
|
|
14
|
+
export declare const constant: Rule<ConstantOptions>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* A fixed score for every wallet. v1.
|
|
4
|
+
*
|
|
5
|
+
* In the weight slot this is "1 pass = 1 vote" (`value: 1`). In the rule slot a
|
|
6
|
+
* positive `value` admits everyone (a no-op gate). No chain read.
|
|
7
|
+
*/
|
|
8
|
+
export const ConstantOptionsSchema = z.object({
|
|
9
|
+
type: z.literal("constant"),
|
|
10
|
+
value: z.number().int().positive().default(1)
|
|
11
|
+
});
|
|
12
|
+
export const constant = {
|
|
13
|
+
type: "constant",
|
|
14
|
+
optionsSchema: ConstantOptionsSchema,
|
|
15
|
+
async evaluate({ options }) {
|
|
16
|
+
return { score: BigInt(options.value) };
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Rule } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Score by ERC-20 balance (for example BSO). Reserved for the pass + BSO combo path.
|
|
5
|
+
*
|
|
6
|
+
* Score = the wallet's raw balance (base units) at the bucket block if it meets `min`,
|
|
7
|
+
* else 0n. `min` (in whole tokens, default 0) is what lets this single rule serve
|
|
8
|
+
* BOTH slots: in the weight slot leave `min` at 0 and the score is the magnitude; in the
|
|
9
|
+
* rule slot set `min` and a wallet below it scores 0n (rejected).
|
|
10
|
+
*
|
|
11
|
+
* The score is the exact `bigint` viem returns — no `Number()` cast, no precision loss.
|
|
12
|
+
* We deliberately do NOT divide by `decimals`: that divide is monotonic, so it never
|
|
13
|
+
* changes the ranking, and doing it in a JS `number` is exactly what used to lose
|
|
14
|
+
* precision. `decimals` is retained only to convert `min` (whole tokens) to base units
|
|
15
|
+
* via viem `parseUnits`, so the gate compares like-for-like. The score's absolute
|
|
16
|
+
* magnitude is therefore base units; formatting to whole tokens for display is a caller
|
|
17
|
+
* concern (`viem.formatUnits`). See DESIGN.md "Rules".
|
|
18
|
+
*
|
|
19
|
+
* (Edge: `min` is a normal token threshold. A `min` large enough that `.toString()`
|
|
20
|
+
* yields scientific notation would break `parseUnits` — out of scope for real thresholds.)
|
|
21
|
+
*/
|
|
22
|
+
export declare const Erc20BalanceOptionsSchema: z.ZodObject<{
|
|
23
|
+
type: z.ZodLiteral<"erc20-balance">;
|
|
24
|
+
chain: z.ZodString;
|
|
25
|
+
contract: z.ZodString;
|
|
26
|
+
decimals: z.ZodDefault<z.ZodNumber>;
|
|
27
|
+
min: z.ZodDefault<z.ZodNumber>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export type Erc20BalanceOptions = z.infer<typeof Erc20BalanceOptionsSchema>;
|
|
30
|
+
export declare const erc20Balance: Rule<Erc20BalanceOptions>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { erc20Abi, getAddress, parseUnits } from "viem";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ChainTickerSchema } from "../schema/common.js";
|
|
4
|
+
/**
|
|
5
|
+
* Score by ERC-20 balance (for example BSO). Reserved for the pass + BSO combo path.
|
|
6
|
+
*
|
|
7
|
+
* Score = the wallet's raw balance (base units) at the bucket block if it meets `min`,
|
|
8
|
+
* else 0n. `min` (in whole tokens, default 0) is what lets this single rule serve
|
|
9
|
+
* BOTH slots: in the weight slot leave `min` at 0 and the score is the magnitude; in the
|
|
10
|
+
* rule slot set `min` and a wallet below it scores 0n (rejected).
|
|
11
|
+
*
|
|
12
|
+
* The score is the exact `bigint` viem returns — no `Number()` cast, no precision loss.
|
|
13
|
+
* We deliberately do NOT divide by `decimals`: that divide is monotonic, so it never
|
|
14
|
+
* changes the ranking, and doing it in a JS `number` is exactly what used to lose
|
|
15
|
+
* precision. `decimals` is retained only to convert `min` (whole tokens) to base units
|
|
16
|
+
* via viem `parseUnits`, so the gate compares like-for-like. The score's absolute
|
|
17
|
+
* magnitude is therefore base units; formatting to whole tokens for display is a caller
|
|
18
|
+
* concern (`viem.formatUnits`). See DESIGN.md "Rules".
|
|
19
|
+
*
|
|
20
|
+
* (Edge: `min` is a normal token threshold. A `min` large enough that `.toString()`
|
|
21
|
+
* yields scientific notation would break `parseUnits` — out of scope for real thresholds.)
|
|
22
|
+
*/
|
|
23
|
+
export const Erc20BalanceOptionsSchema = z.object({
|
|
24
|
+
type: z.literal("erc20-balance"),
|
|
25
|
+
chain: ChainTickerSchema,
|
|
26
|
+
contract: z.string(),
|
|
27
|
+
decimals: z.number().int().nonnegative().default(18),
|
|
28
|
+
min: z.number().nonnegative().default(0)
|
|
29
|
+
});
|
|
30
|
+
export const erc20Balance = {
|
|
31
|
+
type: "erc20-balance",
|
|
32
|
+
optionsSchema: Erc20BalanceOptionsSchema,
|
|
33
|
+
async evaluate({ options, walletAddress, ctx }) {
|
|
34
|
+
const raw = await ctx.chain.readContract({
|
|
35
|
+
address: getAddress(options.contract),
|
|
36
|
+
abi: erc20Abi,
|
|
37
|
+
functionName: "balanceOf",
|
|
38
|
+
args: [getAddress(walletAddress)],
|
|
39
|
+
blockNumber: BigInt(ctx.blockNumber)
|
|
40
|
+
});
|
|
41
|
+
const minUnits = parseUnits(options.min.toString(), options.decimals);
|
|
42
|
+
return { score: raw >= minUnits ? raw : 0n };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Rule } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Hold at least `min` of an ERC-721 (the 5chan Pass). v1.
|
|
5
|
+
*
|
|
6
|
+
* Score = the wallet's holding at the bucket block if it meets `min`, else 0. In the
|
|
7
|
+
* rule slot, `> 0` admits the wallet (it holds the Pass); in the weight slot it
|
|
8
|
+
* weights by the number of Passes held. The body reads its own `balanceOf` via the
|
|
9
|
+
* injected viem client (no libp2p/helia import), unit-testable against a stubbed client.
|
|
10
|
+
*/
|
|
11
|
+
export declare const Erc721MinBalanceOptionsSchema: z.ZodObject<{
|
|
12
|
+
type: z.ZodLiteral<"erc721-min-balance">;
|
|
13
|
+
chain: z.ZodString;
|
|
14
|
+
contract: z.ZodString;
|
|
15
|
+
min: z.ZodDefault<z.ZodNumber>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export type Erc721MinBalanceOptions = z.infer<typeof Erc721MinBalanceOptionsSchema>;
|
|
18
|
+
export declare const erc721MinBalance: Rule<Erc721MinBalanceOptions>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { erc721Abi, getAddress } from "viem";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ChainTickerSchema } from "../schema/common.js";
|
|
4
|
+
/**
|
|
5
|
+
* Hold at least `min` of an ERC-721 (the 5chan Pass). v1.
|
|
6
|
+
*
|
|
7
|
+
* Score = the wallet's holding at the bucket block if it meets `min`, else 0. In the
|
|
8
|
+
* rule slot, `> 0` admits the wallet (it holds the Pass); in the weight slot it
|
|
9
|
+
* weights by the number of Passes held. The body reads its own `balanceOf` via the
|
|
10
|
+
* injected viem client (no libp2p/helia import), unit-testable against a stubbed client.
|
|
11
|
+
*/
|
|
12
|
+
export const Erc721MinBalanceOptionsSchema = z.object({
|
|
13
|
+
type: z.literal("erc721-min-balance"),
|
|
14
|
+
chain: ChainTickerSchema,
|
|
15
|
+
contract: z.string(),
|
|
16
|
+
min: z.number().int().positive().default(1)
|
|
17
|
+
});
|
|
18
|
+
/** Score from one balance: the holding when it meets `min`, else `0n` (does not qualify). */
|
|
19
|
+
function scoreOf(balance, min) {
|
|
20
|
+
return balance >= BigInt(min) ? balance : 0n;
|
|
21
|
+
}
|
|
22
|
+
export const erc721MinBalance = {
|
|
23
|
+
type: "erc721-min-balance",
|
|
24
|
+
optionsSchema: Erc721MinBalanceOptionsSchema,
|
|
25
|
+
async evaluate({ options, walletAddress, ctx }) {
|
|
26
|
+
const balance = await ctx.chain.readContract({
|
|
27
|
+
address: getAddress(options.contract),
|
|
28
|
+
abi: erc721Abi,
|
|
29
|
+
functionName: "balanceOf",
|
|
30
|
+
args: [getAddress(walletAddress)],
|
|
31
|
+
blockNumber: BigInt(ctx.blockNumber)
|
|
32
|
+
});
|
|
33
|
+
return { score: scoreOf(balance, options.min) };
|
|
34
|
+
},
|
|
35
|
+
async evaluateMany({ options, walletAddresses, ctx }) {
|
|
36
|
+
const contract = getAddress(options.contract);
|
|
37
|
+
// One multicall3 `aggregate3` eth_call covers every wallet — the batched path the
|
|
38
|
+
// background chain verifier rides on a cold join (N wallets, ~1 RPC round trip). It
|
|
39
|
+
// needs the client to know its chain's multicall3 deployment; a client built without a
|
|
40
|
+
// `chain` (or on a chain without multicall3) takes the per-wallet fallback below.
|
|
41
|
+
if (typeof ctx.chain.multicall === "function" && ctx.chain.chain?.contracts?.multicall3) {
|
|
42
|
+
const balances = await ctx.chain.multicall({
|
|
43
|
+
contracts: walletAddresses.map((wallet) => ({
|
|
44
|
+
address: contract,
|
|
45
|
+
abi: erc721Abi,
|
|
46
|
+
functionName: "balanceOf",
|
|
47
|
+
args: [getAddress(wallet)]
|
|
48
|
+
})),
|
|
49
|
+
allowFailure: false,
|
|
50
|
+
blockNumber: BigInt(ctx.blockNumber)
|
|
51
|
+
});
|
|
52
|
+
return balances.map((balance) => ({ score: scoreOf(balance, options.min) }));
|
|
53
|
+
}
|
|
54
|
+
return Promise.all(walletAddresses.map((walletAddress) => this.evaluate({ options, walletAddress, ctx })));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Criteria } from "../schema/criteria.js";
|
|
2
|
+
import type { RuleRegistry } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The rule registry: builtins, the shadowing resolver, and criteria validation.
|
|
5
|
+
*
|
|
6
|
+
* One flat `type -> rule` map (single kind; see types.ts), mirroring the pkc-js
|
|
7
|
+
* challenge registry (`pkc?.settings?.challenges?.[name] ?? pkcJsChallenges[name]`): a
|
|
8
|
+
* host's overrides shadow built-ins by `type`, so clients like 5chan or seedit register
|
|
9
|
+
* custom rules by passing `{ "their-type": rule }` to `PubsubVoter` without
|
|
10
|
+
* forking this library. A custom `type` becomes part of the criteria bytes, so it is
|
|
11
|
+
* provably pinned to the topic it runs on. Both criteria slots (rule, weight) draw
|
|
12
|
+
* from this single registry.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The library's built-in rules, before any host override.
|
|
16
|
+
*
|
|
17
|
+
* v1 ships exactly the NFT path: `erc721-min-balance` (Pass gate) + `constant` weight.
|
|
18
|
+
* `erc20-balance` is intentionally NOT registered — it stays in the tree (`erc20-balance.ts`,
|
|
19
|
+
* unit-tested) as the design-open weight path, but is unshipped so a criteria naming it
|
|
20
|
+
* recuses via `UnknownRuleError` rather than silently enabling token-weighting. See
|
|
21
|
+
* ROADMAP.md ("Deferred") for when it re-ships.
|
|
22
|
+
*/
|
|
23
|
+
export declare const builtinRegistry: RuleRegistry;
|
|
24
|
+
/** type ids the v1 implementation guarantees; checked against `requires.rules`. */
|
|
25
|
+
export declare const V1_BUILTIN_RULE_TYPES: readonly ["erc721-min-balance", "constant"];
|
|
26
|
+
/**
|
|
27
|
+
* Merge host overrides over the built-ins. Overrides shadow built-ins by `type`. The
|
|
28
|
+
* override map is a plain `RuleRegistry` (a flat record already allows any subset
|
|
29
|
+
* of `type`s), so a host passes only the rules it adds or replaces.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveRegistry(overrides?: RuleRegistry): RuleRegistry;
|
|
32
|
+
/**
|
|
33
|
+
* Validate a criteria document against a resolved registry: the `rule` and
|
|
34
|
+
* `weight` refs must name rules this registry implements, their options must
|
|
35
|
+
* parse against the rule's own schema, and every name in `requires.rules`
|
|
36
|
+
* must be resolvable (so an out-of-date client recuses itself instead of miscounting).
|
|
37
|
+
*
|
|
38
|
+
* Throws `UnknownRuleError` / a zod error on the first failure. This is a check,
|
|
39
|
+
* not a transform: it never mutates `criteria`, so the topic-bearing bytes are untouched
|
|
40
|
+
* (option defaults applied here do not leak back into the encoded criteria).
|
|
41
|
+
*/
|
|
42
|
+
export declare function validateCriteriaRules(criteria: Criteria, registry: RuleRegistry): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { UnknownRuleError } from "../errors.js";
|
|
2
|
+
import { erc721MinBalance } from "./erc721-min-balance.js";
|
|
3
|
+
import { constant } from "./constant.js";
|
|
4
|
+
/**
|
|
5
|
+
* The rule registry: builtins, the shadowing resolver, and criteria validation.
|
|
6
|
+
*
|
|
7
|
+
* One flat `type -> rule` map (single kind; see types.ts), mirroring the pkc-js
|
|
8
|
+
* challenge registry (`pkc?.settings?.challenges?.[name] ?? pkcJsChallenges[name]`): a
|
|
9
|
+
* host's overrides shadow built-ins by `type`, so clients like 5chan or seedit register
|
|
10
|
+
* custom rules by passing `{ "their-type": rule }` to `PubsubVoter` without
|
|
11
|
+
* forking this library. A custom `type` becomes part of the criteria bytes, so it is
|
|
12
|
+
* provably pinned to the topic it runs on. Both criteria slots (rule, weight) draw
|
|
13
|
+
* from this single registry.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The library's built-in rules, before any host override.
|
|
17
|
+
*
|
|
18
|
+
* v1 ships exactly the NFT path: `erc721-min-balance` (Pass gate) + `constant` weight.
|
|
19
|
+
* `erc20-balance` is intentionally NOT registered — it stays in the tree (`erc20-balance.ts`,
|
|
20
|
+
* unit-tested) as the design-open weight path, but is unshipped so a criteria naming it
|
|
21
|
+
* recuses via `UnknownRuleError` rather than silently enabling token-weighting. See
|
|
22
|
+
* ROADMAP.md ("Deferred") for when it re-ships.
|
|
23
|
+
*/
|
|
24
|
+
export const builtinRegistry = {
|
|
25
|
+
[erc721MinBalance.type]: erc721MinBalance,
|
|
26
|
+
[constant.type]: constant
|
|
27
|
+
};
|
|
28
|
+
/** type ids the v1 implementation guarantees; checked against `requires.rules`. */
|
|
29
|
+
export const V1_BUILTIN_RULE_TYPES = ["erc721-min-balance", "constant"];
|
|
30
|
+
/**
|
|
31
|
+
* Merge host overrides over the built-ins. Overrides shadow built-ins by `type`. The
|
|
32
|
+
* override map is a plain `RuleRegistry` (a flat record already allows any subset
|
|
33
|
+
* of `type`s), so a host passes only the rules it adds or replaces.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveRegistry(overrides) {
|
|
36
|
+
return { ...builtinRegistry, ...overrides };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate a criteria document against a resolved registry: the `rule` and
|
|
40
|
+
* `weight` refs must name rules this registry implements, their options must
|
|
41
|
+
* parse against the rule's own schema, and every name in `requires.rules`
|
|
42
|
+
* must be resolvable (so an out-of-date client recuses itself instead of miscounting).
|
|
43
|
+
*
|
|
44
|
+
* Throws `UnknownRuleError` / a zod error on the first failure. This is a check,
|
|
45
|
+
* not a transform: it never mutates `criteria`, so the topic-bearing bytes are untouched
|
|
46
|
+
* (option defaults applied here do not leak back into the encoded criteria).
|
|
47
|
+
*/
|
|
48
|
+
export function validateCriteriaRules(criteria, registry) {
|
|
49
|
+
const rule = registry[criteria.rule.type];
|
|
50
|
+
if (!rule)
|
|
51
|
+
throw new UnknownRuleError("rule", criteria.rule.type);
|
|
52
|
+
rule.optionsSchema.parse(criteria.rule);
|
|
53
|
+
const weight = registry[criteria.weight.type];
|
|
54
|
+
if (!weight)
|
|
55
|
+
throw new UnknownRuleError("weight", criteria.weight.type);
|
|
56
|
+
weight.optionsSchema.parse(criteria.weight);
|
|
57
|
+
for (const name of criteria.requires.rules) {
|
|
58
|
+
if (!registry[name])
|
|
59
|
+
throw new UnknownRuleError("requires", name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { ChainClient } from "../chain/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Rule interface, design + leaves implemented.
|
|
5
|
+
*
|
|
6
|
+
* A rule turns a criteria `{ type, ...options }` reference into a non-negative
|
|
7
|
+
* score for one wallet, evaluated at the bundle's bucket block. There is a SINGLE kind
|
|
8
|
+
* (mirroring the flat pkc-js challenge registry: `Record<string, rule>`, user
|
|
9
|
+
* entries shadow builtins). The criteria still has two slots that draw from this one
|
|
10
|
+
* registry:
|
|
11
|
+
*
|
|
12
|
+
* - rule slot: the score is a GATE. `> 0n` admits the wallet, `0n` rejects it.
|
|
13
|
+
* - weight slot: the score is the vote's MAGNITUDE.
|
|
14
|
+
*
|
|
15
|
+
* Final vote value = `evaluate(rule).score === 0n ? 0n : evaluate(weight).score`.
|
|
16
|
+
*
|
|
17
|
+
* A single numeric return covers both roles: a rule that needs a threshold
|
|
18
|
+
* (min Passes, min balance) bakes it in by returning 0n when the wallet falls short.
|
|
19
|
+
* That is why the gate slot does not need a separate boolean kind.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The result of one evaluation. `score` is a non-negative `bigint`; `0n` means "does not
|
|
23
|
+
* qualify" (rejected in the rule slot, no weight in the weight slot). It is an
|
|
24
|
+
* object, not a bare `bigint`, so slot-specific fields can be added without changing the
|
|
25
|
+
* signature again — e.g. a self-declared `ceiling` for balance-derived weight, which the
|
|
26
|
+
* lazy tally needs as a wire-side upper bound (see DESIGN.md "Open questions").
|
|
27
|
+
*/
|
|
28
|
+
export interface RuleResult {
|
|
29
|
+
score: bigint;
|
|
30
|
+
}
|
|
31
|
+
/** Everything a rule needs to read chain state for one evaluation. */
|
|
32
|
+
export interface ChainReadContext {
|
|
33
|
+
/**
|
|
34
|
+
* The viem `PublicClient` for the rule's `options.chain`. Use the full viem
|
|
35
|
+
* read surface directly (`readContract`, `getBalance`, ...), pinning every call to
|
|
36
|
+
* the sampled block with `blockNumber: BigInt(ctx.blockNumber)`.
|
|
37
|
+
*/
|
|
38
|
+
chain: ChainClient;
|
|
39
|
+
/** The single sampled block for the bundle's bucket. */
|
|
40
|
+
blockNumber: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The one rule kind. `O` is the validated options type (from its `optionsSchema`).
|
|
44
|
+
* `evaluate` returns a `RuleResult` whose `score` is a non-negative `bigint`; `0n`
|
|
45
|
+
* means "does not qualify" (rejected in the rule slot, no weight in the weight slot).
|
|
46
|
+
*/
|
|
47
|
+
export interface Rule<O = unknown> {
|
|
48
|
+
readonly type: string;
|
|
49
|
+
readonly optionsSchema: z.ZodType<O>;
|
|
50
|
+
evaluate(args: {
|
|
51
|
+
options: O;
|
|
52
|
+
walletAddress: string;
|
|
53
|
+
ctx: ChainReadContext;
|
|
54
|
+
}): Promise<RuleResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Optional batched form of {@link evaluate}: score many wallets at the SAME sampled block in
|
|
57
|
+
* as few RPC round trips as the rule can manage (e.g. one multicall3 `aggregate3` for a whole
|
|
58
|
+
* checkpoint's wallets). Returns one result per input wallet, in order. Semantics MUST equal
|
|
59
|
+
* mapping `evaluate` over the wallets — this is a transport optimization, never a different
|
|
60
|
+
* answer. The background chain verifier prefers it when present and falls back to per-wallet
|
|
61
|
+
* `evaluate` calls otherwise (see verify/background.ts).
|
|
62
|
+
*/
|
|
63
|
+
evaluateMany?(args: {
|
|
64
|
+
options: O;
|
|
65
|
+
walletAddresses: string[];
|
|
66
|
+
ctx: ChainReadContext;
|
|
67
|
+
}): Promise<RuleResult[]>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The registry: a flat `type -> rule` map. Built-ins are provided by this
|
|
71
|
+
* library; hosts may pass overrides that shadow built-ins by `type`. Mirrors pkc-js
|
|
72
|
+
* src/runtime/node/community/challenges/index.ts.
|
|
73
|
+
*/
|
|
74
|
+
export type RuleRegistry = Record<string, Rule>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Shared wire primitives.
|
|
4
|
+
*
|
|
5
|
+
* There is no pkc-js author in this library: a vote is signed directly by the
|
|
6
|
+
* gating-chain wallet that holds the Pass/ERC-20 (see DESIGN.md "Identity: the
|
|
7
|
+
* voting wallet, nothing else"). What survives from the old author/wallet schema is
|
|
8
|
+
* two small, identity-agnostic primitives reused across the wire:
|
|
9
|
+
* - `ChainTickerSchema`: the chain identifier ("base", "eth", ...) used by criteria
|
|
10
|
+
* and chain-reading rules.
|
|
11
|
+
* - `SignatureSchema`: the detached `{ signature, type }` shape (kept from pkc-js),
|
|
12
|
+
* here carrying the wallet's EIP-712 ballot signature.
|
|
13
|
+
*/
|
|
14
|
+
/** Chain ticker is unrestricted ("base", "eth", ...), matching pkc-js. */
|
|
15
|
+
export declare const ChainTickerSchema: z.ZodString;
|
|
16
|
+
/**
|
|
17
|
+
* A detached signature with an explicit scheme tag. For a Votes bundle the `type` is
|
|
18
|
+
* the EIP-712 scheme ({@link EIP712_SIGNATURE_TYPE}) and `signature` is the hex
|
|
19
|
+
* signature the voting wallet produced over the ballot typed data.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SignatureSchema: z.ZodObject<{
|
|
22
|
+
signature: z.ZodString;
|
|
23
|
+
type: z.ZodString;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export type Signature = z.infer<typeof SignatureSchema>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Shared wire primitives.
|
|
4
|
+
*
|
|
5
|
+
* There is no pkc-js author in this library: a vote is signed directly by the
|
|
6
|
+
* gating-chain wallet that holds the Pass/ERC-20 (see DESIGN.md "Identity: the
|
|
7
|
+
* voting wallet, nothing else"). What survives from the old author/wallet schema is
|
|
8
|
+
* two small, identity-agnostic primitives reused across the wire:
|
|
9
|
+
* - `ChainTickerSchema`: the chain identifier ("base", "eth", ...) used by criteria
|
|
10
|
+
* and chain-reading rules.
|
|
11
|
+
* - `SignatureSchema`: the detached `{ signature, type }` shape (kept from pkc-js),
|
|
12
|
+
* here carrying the wallet's EIP-712 ballot signature.
|
|
13
|
+
*/
|
|
14
|
+
/** Chain ticker is unrestricted ("base", "eth", ...), matching pkc-js. */
|
|
15
|
+
export const ChainTickerSchema = z.string().min(1);
|
|
16
|
+
/**
|
|
17
|
+
* A detached signature with an explicit scheme tag. For a Votes bundle the `type` is
|
|
18
|
+
* the EIP-712 scheme ({@link EIP712_SIGNATURE_TYPE}) and `signature` is the hex
|
|
19
|
+
* signature the voting wallet produced over the ballot typed data.
|
|
20
|
+
*/
|
|
21
|
+
export const SignatureSchema = z.object({
|
|
22
|
+
signature: z.string().min(1),
|
|
23
|
+
type: z.string().min(1)
|
|
24
|
+
});
|