@genkaku/protocol 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/dist/bridge.d.ts +46 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +542 -0
- package/dist/chain.d.ts +496 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +763 -0
- package/dist/credit.d.ts +35 -0
- package/dist/credit.d.ts.map +1 -0
- package/dist/credit.js +112 -0
- package/dist/dial.d.ts +43 -0
- package/dist/dial.d.ts.map +1 -0
- package/dist/dial.js +266 -0
- package/dist/directory.d.ts +39 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +149 -0
- package/dist/discovery.d.ts +20 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +58 -0
- package/dist/hello.d.ts +14 -0
- package/dist/hello.d.ts.map +1 -0
- package/dist/hello.js +42 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/keys.d.ts +30 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +126 -0
- package/dist/llmBridge.d.ts +34 -0
- package/dist/llmBridge.d.ts.map +1 -0
- package/dist/llmBridge.js +151 -0
- package/dist/lp.d.ts +31 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +116 -0
- package/dist/modeManager.d.ts +30 -0
- package/dist/modeManager.d.ts.map +1 -0
- package/dist/modeManager.js +81 -0
- package/dist/netconfig.d.ts +11 -0
- package/dist/netconfig.d.ts.map +1 -0
- package/dist/netconfig.js +39 -0
- package/dist/offer.d.ts +70 -0
- package/dist/offer.d.ts.map +1 -0
- package/dist/offer.js +84 -0
- package/dist/ollama.d.ts +53 -0
- package/dist/ollama.d.ts.map +1 -0
- package/dist/ollama.js +162 -0
- package/dist/p2pNode.d.ts +50 -0
- package/dist/p2pNode.d.ts.map +1 -0
- package/dist/p2pNode.js +450 -0
- package/dist/paywall.d.ts +30 -0
- package/dist/paywall.d.ts.map +1 -0
- package/dist/paywall.js +84 -0
- package/dist/publishOptIn.d.ts +15 -0
- package/dist/publishOptIn.d.ts.map +1 -0
- package/dist/publishOptIn.js +34 -0
- package/dist/receipts.d.ts +61 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/receipts.js +135 -0
- package/dist/registry.d.ts +19 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +50 -0
- package/dist/rpc.d.ts +24 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +143 -0
- package/dist/status.d.ts +56 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +354 -0
- package/dist/userSession.d.ts +64 -0
- package/dist/userSession.d.ts.map +1 -0
- package/dist/userSession.js +125 -0
- package/dist/workerSession.d.ts +67 -0
- package/dist/workerSession.d.ts.map +1 -0
- package/dist/workerSession.js +65 -0
- package/package.json +56 -3
- package/README.md +0 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Libp2p } from 'libp2p';
|
|
2
|
+
import { type Hex } from 'viem';
|
|
3
|
+
import type { Paywall } from './paywall.ts';
|
|
4
|
+
import type { LocalCompletionSource } from './llmBridge.ts';
|
|
5
|
+
import type { ModeManager } from './modeManager.ts';
|
|
6
|
+
import { type Offer } from './offer.ts';
|
|
7
|
+
import type { SessionEvent } from './workerSession.ts';
|
|
8
|
+
export { FrameReader, lpEncode, lpFrames } from './lp.ts';
|
|
9
|
+
export type { ByteChunk } from './lp.ts';
|
|
10
|
+
export declare const GENKAKU_PROTOCOLS: readonly ["/genkaku/open/0", "/genkaku/token/0", "/genkaku/receipt/0", "/genkaku/settle/0", "/genkaku/offers/0", "/genkaku/policy/0", "/genkaku/hello/0"];
|
|
11
|
+
export type SidecarOptions = {
|
|
12
|
+
modeManager: ModeManager;
|
|
13
|
+
source: LocalCompletionSource;
|
|
14
|
+
/** ip4/tcp listen addrs; LAN first, NAT traversal (hole punching) later */
|
|
15
|
+
listen: string[];
|
|
16
|
+
/** one Offer row per (model, price) the worker publishes — visible to users */
|
|
17
|
+
catalog: Offer[];
|
|
18
|
+
sessionEvents?: (e: SessionEvent) => void;
|
|
19
|
+
/** P4.9d: refuse to spend GPU time unless the named escrow pays this worker. */
|
|
20
|
+
paywall?: Paywall;
|
|
21
|
+
/** P4.4: with a worker wallet, priced receipts MUST carry a valid signature,
|
|
22
|
+
* offers self-report the wallet, and settle co-signing becomes available. */
|
|
23
|
+
workerKey?: Hex;
|
|
24
|
+
};
|
|
25
|
+
/** One stored (latest) receipt per jobId — the evidence the settle co-sign
|
|
26
|
+
* defends against (spend == max cumulative receipt the client piped). */
|
|
27
|
+
export type StoredReceipt = {
|
|
28
|
+
seq: number;
|
|
29
|
+
amount: bigint;
|
|
30
|
+
sig: string;
|
|
31
|
+
/** the escrow/stream binding used to build the digest (jobId, escrowId) */
|
|
32
|
+
escrowId: string;
|
|
33
|
+
/** commit-challenge commitments carried by the receipt (P4.9f) */
|
|
34
|
+
inputHash: `0x${string}`;
|
|
35
|
+
outputHash: `0x${string}`;
|
|
36
|
+
};
|
|
37
|
+
/** A bare libp2p node for the USER side (no genkaku handlers — dials only). */
|
|
38
|
+
export declare function createUserNode(listen?: string[]): Promise<Libp2p>;
|
|
39
|
+
export type GenkakuSidecar = {
|
|
40
|
+
node: Libp2p;
|
|
41
|
+
peerAddrs: () => string[];
|
|
42
|
+
stop(): Promise<void>;
|
|
43
|
+
activeSessions: () => number;
|
|
44
|
+
/** P4.5: the base58 PeerId string the registry binding pins */
|
|
45
|
+
peerIdStr: () => string;
|
|
46
|
+
/** Replace the served offer board without restarting the sidecar. */
|
|
47
|
+
setCatalog: (offers: Offer[]) => void;
|
|
48
|
+
};
|
|
49
|
+
export declare function createSidecar(opts: SidecarOptions): Promise<GenkakuSidecar>;
|
|
50
|
+
//# sourceMappingURL=p2pNode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"p2pNode.d.ts","sourceRoot":"","sources":["../src/p2pNode.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAE,KAAK,GAAG,EAAW,MAAM,MAAM,CAAA;AAIxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGnD,OAAO,EAAoB,KAAK,KAAK,EAAU,MAAM,YAAY,CAAA;AAajE,OAAO,KAAK,EAAkB,YAAY,EAAiB,MAAM,oBAAoB,CAAA;AAGrF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,eAAO,MAAM,iBAAiB,2JAQpB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,WAAW,CAAA;IACxB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,+EAA+E;IAC/E,OAAO,EAAE,KAAK,EAAE,CAAA;IAChB,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;kFAC8E;IAC9E,SAAS,CAAC,EAAE,GAAG,CAAA;CAChB,CAAA;AAED;0EAC0E;AAC1E,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,SAAS,EAAE,KAAK,MAAM,EAAE,CAAA;IACxB,UAAU,EAAE,KAAK,MAAM,EAAE,CAAA;CAC1B,CAAA;AAkBD,+EAA+E;AAC/E,wBAAsB,cAAc,CAAC,MAAM,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAY3E;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,MAAM,EAAE,CAAA;IACzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,cAAc,EAAE,MAAM,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,SAAS,EAAE,MAAM,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,CAAA;CACtC,CAAA;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CA8ejF"}
|
package/dist/p2pNode.js
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
// GENKAKU: libp2p nodes (P4.2 sidecar / P4.3 client / P4.4 settle co-sign).
|
|
2
|
+
// Workers serve the /genkaku/* protocols (sidecar); users get a bare dial node.
|
|
3
|
+
// The mode manager gates handler installs — user mode never constructs the
|
|
4
|
+
// worker side, and worker modes never open remote sessions (D-block defense
|
|
5
|
+
// in depth, P4-ADAPTATION §2).
|
|
6
|
+
import { noise } from '@chainsafe/libp2p-noise';
|
|
7
|
+
import { yamux } from '@chainsafe/libp2p-yamux';
|
|
8
|
+
import { Secp256k1PrivateKey } from '@libp2p/crypto/keys';
|
|
9
|
+
import { identify } from '@libp2p/identify';
|
|
10
|
+
import { createFromPrivKey, createSecp256k1PeerId } from '@libp2p/peer-id-factory';
|
|
11
|
+
import { tcp } from '@libp2p/tcp';
|
|
12
|
+
import { createLibp2p } from 'libp2p';
|
|
13
|
+
import { toBytes } from 'viem';
|
|
14
|
+
import { settleDigest, walletAddress } from "./chain.js";
|
|
15
|
+
import { signHello } from "./hello.js";
|
|
16
|
+
import { FrameReader, lpEncode, WriteQueue } from "./lp.js";
|
|
17
|
+
import { CHAIN_ID, CONTRACTS } from "./netconfig.js";
|
|
18
|
+
import { cumulativeBilled, toWire } from "./offer.js";
|
|
19
|
+
import { digestForHex, inputHashOf, outputHashOf, ReceiptLedger, recoverSigner, sessionIdHex, signDigest, ZERO_HASH, } from "./receipts.js";
|
|
20
|
+
import { POLICY_VERSION, policyHashOf } from "./registry.js";
|
|
21
|
+
// re-exported for consumers of the codec (tests, user-side dialer)
|
|
22
|
+
export { FrameReader, lpEncode, lpFrames } from "./lp.js";
|
|
23
|
+
export const GENKAKU_PROTOCOLS = [
|
|
24
|
+
'/genkaku/open/0',
|
|
25
|
+
'/genkaku/token/0',
|
|
26
|
+
'/genkaku/receipt/0',
|
|
27
|
+
'/genkaku/settle/0',
|
|
28
|
+
'/genkaku/offers/0',
|
|
29
|
+
'/genkaku/policy/0',
|
|
30
|
+
'/genkaku/hello/0',
|
|
31
|
+
];
|
|
32
|
+
// libp2p 1.9.4's own default nodeInfo omits the REQUIRED `userAgent` field,
|
|
33
|
+
// which makes @libp2p/identify crash on start (getAgentVersion →
|
|
34
|
+
// nodeInfo.userAgent → undefined → uint8ArrayFromString throws). Provide a
|
|
35
|
+
// full NodeInfo; also advertised in identify metadata.
|
|
36
|
+
const NODE_INFO = { name: 'genkaku', version: '0.1.0', userAgent: 'genkaku/0.1.0 (desktop)' };
|
|
37
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
38
|
+
/** Bounded window AFTER the token EOF in which the client's through-end
|
|
39
|
+
* receipt may still land before we stop watching. Sidecar-scope: the receipt
|
|
40
|
+
* protocol stream stays alive independently of the released token stream. */
|
|
41
|
+
const RECEIPT_DRAIN_MS = 250;
|
|
42
|
+
/** Slack for worker/client clock differences in the receipt clock bound. */
|
|
43
|
+
const CLOCK_SLACK_MS = 2_000;
|
|
44
|
+
/** A bare libp2p node for the USER side (no genkaku handlers — dials only). */
|
|
45
|
+
export async function createUserNode(listen = []) {
|
|
46
|
+
const peerId = await createSecp256k1PeerId();
|
|
47
|
+
return createLibp2p({
|
|
48
|
+
peerId,
|
|
49
|
+
nodeInfo: NODE_INFO,
|
|
50
|
+
addresses: { listen },
|
|
51
|
+
transports: [tcp()],
|
|
52
|
+
connectionEncryption: [noise()],
|
|
53
|
+
streamMuxers: [yamux()],
|
|
54
|
+
services: { identify: identify() },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function createSidecar(opts) {
|
|
58
|
+
// D-block: do not even construct listeners without a sharing-capable mode.
|
|
59
|
+
const mode = opts.modeManager.current();
|
|
60
|
+
if (mode.mode === 'user') {
|
|
61
|
+
throw new Error('[genkaku:sidecar] refusing to build worker listeners in user mode (D-block)');
|
|
62
|
+
}
|
|
63
|
+
opts.modeManager.installWorkerListeners();
|
|
64
|
+
const ledger = new ReceiptLedger();
|
|
65
|
+
const inbox = new Map();
|
|
66
|
+
/** jobId → escrowId as declared in the open envelope (receipt/bind side). */
|
|
67
|
+
const escrowByJob = new Map();
|
|
68
|
+
/** jobId → wall time the open envelope arrived (the clock-bound anchor). */
|
|
69
|
+
const openedAtByJob = new Map();
|
|
70
|
+
/** jobId → the escrow payer proven by the paywall (receipt signer must match). */
|
|
71
|
+
const payerByJob = new Map();
|
|
72
|
+
/** jobId → commitment to the request the open envelope asked for. */
|
|
73
|
+
const inputHashByJob = new Map();
|
|
74
|
+
/** jobId → the answer text emitted so far (the reveal side of a challenge). */
|
|
75
|
+
const outputByJob = new Map();
|
|
76
|
+
/** jobId → the model the open envelope asked for (bounds price lookup). */
|
|
77
|
+
const sessionModelByJob = new Map();
|
|
78
|
+
/** jobId → the client's billing tick (granularity, wallet: the worker owns price). */
|
|
79
|
+
const sessionUnitMsByJob = new Map();
|
|
80
|
+
let sessions = 0;
|
|
81
|
+
let currentCatalog = [...opts.catalog];
|
|
82
|
+
// The worker-side receipt constructor: EXACTLY what the user signed —
|
|
83
|
+
// chainId/escrow from NETCONFIG (one deployment per env), sessionId from
|
|
84
|
+
// (jobId, escrowId). Nothing the caller controls feeds the struct, so a
|
|
85
|
+
// forged sig can't bind a different amount or stream handle.
|
|
86
|
+
const receiptOf = (jobId, held) => {
|
|
87
|
+
if (held == null) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
chainId: CHAIN_ID[ENV],
|
|
92
|
+
escrow: (CONTRACTS[ENV].escrow || ZERO_ADDRESS),
|
|
93
|
+
sessionId: sessionIdHex(jobId, held.escrowId),
|
|
94
|
+
seq: held.seq,
|
|
95
|
+
amount: held.amount,
|
|
96
|
+
inputHash: held.inputHash,
|
|
97
|
+
outputHash: held.outputHash,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
// Identity: secp256k1 peer key — libp2p supports it natively and it is the
|
|
101
|
+
// SAME curve as the EVM wallet, so the P4.4 registry binding proves one key
|
|
102
|
+
// owns both sides of (PeerId, wallet) address. With a worker wallet, derive
|
|
103
|
+
// the PeerId from that exact key; otherwise keep a random identity.
|
|
104
|
+
const peerId = opts.workerKey != null
|
|
105
|
+
? await createFromPrivKey(new Secp256k1PrivateKey(toBytes(opts.workerKey)))
|
|
106
|
+
: await createSecp256k1PeerId();
|
|
107
|
+
const node = await createLibp2p({
|
|
108
|
+
peerId,
|
|
109
|
+
nodeInfo: NODE_INFO,
|
|
110
|
+
addresses: { listen: opts.listen },
|
|
111
|
+
transports: [tcp()],
|
|
112
|
+
connectionEncryption: [noise()],
|
|
113
|
+
streamMuxers: [yamux()],
|
|
114
|
+
services: { identify: identify() },
|
|
115
|
+
});
|
|
116
|
+
await node.handle('/genkaku/open/0', ({ connection, stream }) => {
|
|
117
|
+
void (async () => {
|
|
118
|
+
// Read ONLY the open envelope, then start serving — the client keeps its
|
|
119
|
+
// write half open for the session, so a drain-to-EOS read would deadlock
|
|
120
|
+
// with muxer window backpressure.
|
|
121
|
+
const reader = new FrameReader();
|
|
122
|
+
let env;
|
|
123
|
+
for await (const chunk of stream.source) {
|
|
124
|
+
reader.push(chunk);
|
|
125
|
+
const frame = reader.next();
|
|
126
|
+
if (frame != null) {
|
|
127
|
+
env = frame;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!env || env.proto !== 'open/0') {
|
|
132
|
+
await stream.close();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let payer = null;
|
|
136
|
+
if (opts.paywall != null) {
|
|
137
|
+
const verdict = await opts.paywall({
|
|
138
|
+
jobId: env.jobId,
|
|
139
|
+
escrowId: env.escrowId || '0',
|
|
140
|
+
clientPeer: env.clientPeer ?? '',
|
|
141
|
+
}).catch(() => ({ ok: false, code: 'paywall-error', reason: 'paywall check failed' }));
|
|
142
|
+
if (!verdict.ok) {
|
|
143
|
+
opts.sessionEvents?.({
|
|
144
|
+
type: 'session-error',
|
|
145
|
+
message: `refused ${env.jobId}: ${verdict.code} (${verdict.reason})`,
|
|
146
|
+
});
|
|
147
|
+
try {
|
|
148
|
+
await stream.sink([lpEncode({
|
|
149
|
+
proto: 'err/0',
|
|
150
|
+
jobId: env.jobId,
|
|
151
|
+
code: verdict.code,
|
|
152
|
+
message: verdict.reason,
|
|
153
|
+
})]);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// the client may already have gone; the close below still ends it
|
|
157
|
+
}
|
|
158
|
+
await stream.close().catch(() => { });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
payer = verdict.payer;
|
|
162
|
+
}
|
|
163
|
+
if (payer != null)
|
|
164
|
+
payerByJob.set(env.jobId, payer);
|
|
165
|
+
inputHashByJob.set(env.jobId, inputHashOf({
|
|
166
|
+
model: env.model,
|
|
167
|
+
prompt: env.prompt,
|
|
168
|
+
system: env.system,
|
|
169
|
+
maxTokens: env.maxTokens,
|
|
170
|
+
}));
|
|
171
|
+
outputByJob.set(env.jobId, '');
|
|
172
|
+
escrowByJob.set(env.jobId, env.escrowId || '0');
|
|
173
|
+
openedAtByJob.set(env.jobId, Date.now());
|
|
174
|
+
sessionModelByJob.set(env.jobId, env.model);
|
|
175
|
+
sessionUnitMsByJob.set(env.jobId, env.billing?.unitMs ?? 60_000);
|
|
176
|
+
// ONE sink for the whole session: token frames push through a durable
|
|
177
|
+
// write queue (a second stream.sink() call would explode it).
|
|
178
|
+
const dq = new WriteQueue();
|
|
179
|
+
const sinkDone = stream.sink(dq.source()).catch(() => { });
|
|
180
|
+
// Admission ack (P4.9d): the client waits for this before it reports a
|
|
181
|
+
// success to its own caller, so a paywall refusal reaches the caller as
|
|
182
|
+
// an error instead of an empty completion.
|
|
183
|
+
dq.push(lpEncode({ proto: 'ok/0', jobId: env.jobId }));
|
|
184
|
+
const emit = (e) => opts.sessionEvents?.(e);
|
|
185
|
+
const session = await import("./workerSession.js").then(({ createWorkerSession }) => createWorkerSession(env, {
|
|
186
|
+
modeManager: opts.modeManager,
|
|
187
|
+
source: opts.source,
|
|
188
|
+
ledger,
|
|
189
|
+
emit,
|
|
190
|
+
onDelta: (delta) => {
|
|
191
|
+
outputByJob.set(env.jobId, (outputByJob.get(env.jobId) ?? '') + delta);
|
|
192
|
+
dq.push(lpEncode({ proto: 'tok/0', delta }));
|
|
193
|
+
},
|
|
194
|
+
latestReceipt: () => inbox.get(env.jobId) ?? null,
|
|
195
|
+
}));
|
|
196
|
+
sessions += 1;
|
|
197
|
+
try {
|
|
198
|
+
await session.run();
|
|
199
|
+
// P2 §3 / P4.4: the client sends its THROUGH-END receipt only after
|
|
200
|
+
// the token EOF reaches it — i.e. after this handler would have
|
|
201
|
+
// closed. Release the token stream first (never meter artificial
|
|
202
|
+
// hold time), then poll the shared inbox for one bounded window and
|
|
203
|
+
// push any post-end receipt through the SAME strictly-increasing
|
|
204
|
+
// ledger rule as the live session did. The settle co-sign (below)
|
|
205
|
+
// defends the final evidence either way.
|
|
206
|
+
dq.end();
|
|
207
|
+
await sinkDone;
|
|
208
|
+
await stream.close();
|
|
209
|
+
const deadline = Date.now() + RECEIPT_DRAIN_MS;
|
|
210
|
+
while (Date.now() < deadline) {
|
|
211
|
+
const held = inbox.get(env.jobId);
|
|
212
|
+
if (held != null) {
|
|
213
|
+
const verdict = ledger.offer(env.jobId, held.seq, held.amount);
|
|
214
|
+
if (verdict.accepted) {
|
|
215
|
+
emit({ type: 'receipt-accepted', seq: held.seq, amount: held.amount });
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
// Stale (already merged into the ledger during the live session):
|
|
219
|
+
// keep the window open — a newer receipt may still be in flight.
|
|
220
|
+
}
|
|
221
|
+
await new Promise(r => setTimeout(r, 20));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
sessions -= 1;
|
|
226
|
+
if (!dq.ended) { // drain-parked path already closed — safe no-op here
|
|
227
|
+
dq.end();
|
|
228
|
+
await sinkDone;
|
|
229
|
+
await stream.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
void connection;
|
|
233
|
+
})();
|
|
234
|
+
});
|
|
235
|
+
await node.handle('/genkaku/receipt/0', ({ stream }) => {
|
|
236
|
+
void (async () => {
|
|
237
|
+
const reader = new FrameReader();
|
|
238
|
+
for await (const chunk of stream.source) {
|
|
239
|
+
reader.push(chunk);
|
|
240
|
+
while (true) {
|
|
241
|
+
const f = reader.next();
|
|
242
|
+
if (f == null) {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
if (f.proto === 'receipt/0' && typeof f.seq === 'number' && typeof f.amount === 'string' && typeof f.jobId === 'string') {
|
|
246
|
+
// escrowId comes from the OPEN envelope, never from this frame —
|
|
247
|
+
// the binding is the session's, not the receipt-sender's claim.
|
|
248
|
+
const stored = {
|
|
249
|
+
seq: f.seq,
|
|
250
|
+
amount: BigInt(f.amount),
|
|
251
|
+
sig: f.sig ?? '',
|
|
252
|
+
escrowId: escrowByJob.get(f.jobId) ?? '0',
|
|
253
|
+
inputHash: (f.inputHash ?? ZERO_HASH),
|
|
254
|
+
outputHash: (f.outputHash ?? ZERO_HASH),
|
|
255
|
+
};
|
|
256
|
+
// P4.9f commit-challenge: the client may not pay for a different
|
|
257
|
+
// request, and a non-zero output commitment must match the text
|
|
258
|
+
// this worker actually emitted.
|
|
259
|
+
const expectedInput = inputHashByJob.get(f.jobId);
|
|
260
|
+
if (expectedInput != null && stored.inputHash !== expectedInput) {
|
|
261
|
+
opts.sessionEvents?.({ type: 'session-error', message: `receipt ${stored.seq}: input commitment mismatch` });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (stored.outputHash !== ZERO_HASH
|
|
265
|
+
&& stored.outputHash !== outputHashOf(outputByJob.get(f.jobId) ?? '')) {
|
|
266
|
+
opts.sessionEvents?.({ type: 'session-error', message: `receipt ${stored.seq}: output commitment mismatch` });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// P4.4: priced receipts are SIGNATURE-BOUND evidence — a forged
|
|
270
|
+
// or anonymous offer never reaches the escrow at settle.
|
|
271
|
+
if (opts.workerKey != null && stored.escrowId !== '0') {
|
|
272
|
+
const bad = await rejectUnsigned(stored, f.jobId);
|
|
273
|
+
if (bad) {
|
|
274
|
+
opts.sessionEvents?.({ type: 'session-error', message: `receipt ${stored.seq}: unsigned/malformed offer dropped` });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const over = overClockBound(stored, f.jobId);
|
|
278
|
+
if (over != null) {
|
|
279
|
+
opts.sessionEvents?.({ type: 'session-error', message: `receipt ${stored.seq} for ${stored.amount}: ${over}` });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Inbox ONLY: the ledger entry is claimed by the session that owns
|
|
284
|
+
// this jobId (single ingestion point — a pre-offer here would make
|
|
285
|
+
// the session's own offer stale).
|
|
286
|
+
inbox.set(f.jobId, stored);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
await stream.close();
|
|
291
|
+
})();
|
|
292
|
+
});
|
|
293
|
+
// P4.4 settle co-sign: one request frame, one reply frame. The digest is
|
|
294
|
+
// rebuilt WORKER-SIDE from the held receipt (the caller's receiptHash is
|
|
295
|
+
// only compared against it), and the spend must equal the held max
|
|
296
|
+
// cumulative receipt — anti-inflation by construction (P1 §8). JobEscrow.
|
|
297
|
+
// settle() then recovers this EIP-191 sig against the session's worker.
|
|
298
|
+
await node.handle('/genkaku/settle/0', ({ stream }) => {
|
|
299
|
+
void (async () => {
|
|
300
|
+
const reader = new FrameReader();
|
|
301
|
+
let req;
|
|
302
|
+
for await (const chunk of stream.source) {
|
|
303
|
+
reader.push(chunk);
|
|
304
|
+
const f = reader.next();
|
|
305
|
+
if (f != null) {
|
|
306
|
+
req = f;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const reply = (ok, sig, reason) => stream.sink([lpEncode({ proto: 'settle/0', ok, sig, reason })]).then(() => stream.close());
|
|
311
|
+
if (req?.proto !== 'settle/0') {
|
|
312
|
+
await reply(false, undefined, 'bad frame');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (opts.workerKey == null) {
|
|
316
|
+
await reply(false, undefined, 'worker has no wallet');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const jobId = String(req.jobId ?? '');
|
|
320
|
+
const escrowId = escrowByJob.get(jobId) ?? '0';
|
|
321
|
+
const held = inbox.get(jobId);
|
|
322
|
+
if (escrowId === '0' || held == null) {
|
|
323
|
+
await reply(false, undefined, 'no priced session bound for this jobId');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// Defense in depth: the caller's escrowId must agree with the session's
|
|
327
|
+
// binding. A mismatch can never settle on chain anyway (the worker's
|
|
328
|
+
// digest carries ONLY the envelope escrowId; JobEscrow.settle recovers
|
|
329
|
+
// against the session id), but failing here keeps wire errors local.
|
|
330
|
+
if (req.escrowId != null && String(req.escrowId) !== escrowId) {
|
|
331
|
+
await reply(false, undefined, 'escrowId does not match the session envelope');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (req.spend == null) {
|
|
335
|
+
await reply(false, undefined, 'spend is required');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const spend = BigInt(req.spend);
|
|
339
|
+
if (spend !== held.amount) {
|
|
340
|
+
await reply(false, undefined, 'spend does not match the held max cumulative receipt');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const receipt = receiptOf(jobId, held);
|
|
344
|
+
const receiptHash = receipt == null ? null : digestForHex(receipt);
|
|
345
|
+
if (receiptHash == null || String(req.receiptHash ?? '').toLowerCase() !== receiptHash.toLowerCase()) {
|
|
346
|
+
await reply(false, undefined, 'receiptHash does not match the held receipt');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const sig = await signDigest(opts.workerKey, settleDigest(BigInt(escrowId), receiptHash, spend));
|
|
350
|
+
await reply(true, sig);
|
|
351
|
+
})();
|
|
352
|
+
});
|
|
353
|
+
// P4.9e identity proof: one nonce in, one signed proof out. The client
|
|
354
|
+
// recovers the signer and refuses offers whose wallet it cannot prove.
|
|
355
|
+
await node.handle('/genkaku/hello/0', ({ stream }) => {
|
|
356
|
+
void (async () => {
|
|
357
|
+
const reader = new FrameReader();
|
|
358
|
+
let req;
|
|
359
|
+
for await (const chunk of stream.source) {
|
|
360
|
+
reader.push(chunk);
|
|
361
|
+
const f = reader.next();
|
|
362
|
+
if (f != null) {
|
|
363
|
+
req = f;
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const reply = (body) => stream.sink([lpEncode({ proto: 'hello/0', ...body })]).then(() => stream.close());
|
|
368
|
+
if (req?.proto !== 'hello/0' || typeof req.nonce !== 'string' || !req.nonce.startsWith('0x')) {
|
|
369
|
+
await reply({ ok: false, reason: 'bad frame' });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (opts.workerKey == null) {
|
|
373
|
+
await reply({ ok: false, reason: 'worker has no wallet' });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const proof = await signHello(opts.workerKey, node.peerId.toString(), req.nonce);
|
|
377
|
+
await reply({ ok: true, peerId: proof.peerId, wallet: proof.wallet, sig: proof.sig });
|
|
378
|
+
})();
|
|
379
|
+
});
|
|
380
|
+
// The offer board: a user dials this on every discovered peer and sorts
|
|
381
|
+
// locally (P4.3). bigint prices travel as decimal strings (OfferWire); the
|
|
382
|
+
// wallet here is a CLAIM — the client only trusts it after /genkaku/hello/0
|
|
383
|
+
// proves the key (P4.9e) or the registry binds it (P4.5).
|
|
384
|
+
await node.handle('/genkaku/offers/0', ({ stream }) => {
|
|
385
|
+
const wallet = opts.workerKey != null ? walletAddress(opts.workerKey) : undefined;
|
|
386
|
+
const board = currentCatalog.map(o => toWire(wallet == null ? o : { ...o, workerWallet: wallet }));
|
|
387
|
+
void stream.sink([lpEncode({ proto: 'offers/0', board })]).then(() => stream.close());
|
|
388
|
+
});
|
|
389
|
+
// token is served in-session (open handler); policy is reserved v0.
|
|
390
|
+
await node.handle('/genkaku/token/0', ({ stream }) => { void stream.close(); });
|
|
391
|
+
await node.handle('/genkaku/policy/0', ({ stream }) => {
|
|
392
|
+
// P4.5: the wire advertises the SAME digest the worker pins on
|
|
393
|
+
// WorkerRegistry, so a user can compare registry.policyHash against it.
|
|
394
|
+
void stream.sink([lpEncode({ proto: 'policy/0', policyHash: policyHashOf(POLICY_VERSION), policyVersion: POLICY_VERSION })]).then(() => stream.close());
|
|
395
|
+
});
|
|
396
|
+
// ── helpers (close over netconfig + maps, never over the wire) ──────────
|
|
397
|
+
/** Starts true when a priced receipt cannot be signature-verified. P4.9e:
|
|
398
|
+
* the signer must also equal the escrow's payer, so evidence and money name
|
|
399
|
+
* the same party (a third party can no longer sign for someone's escrow). */
|
|
400
|
+
async function rejectUnsigned(stored, jobId) {
|
|
401
|
+
if (stored.sig === '' || !stored.sig.startsWith('0x')) {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
const receipt = receiptOf(jobId, stored);
|
|
405
|
+
if (receipt == null) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
const signer = await recoverSigner(toBytes(digestForHex(receipt)), stored.sig);
|
|
409
|
+
if (signer == null) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
const payer = payerByJob.get(jobId);
|
|
413
|
+
if (payer != null && payer !== ZERO_ADDRESS && signer.toLowerCase() !== payer.toLowerCase()) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
/** The P2 §3 clock bound: a receipt can never exceed what the worker's OWN
|
|
419
|
+
* catalog price + wall time since the open envelope allow. Returns a
|
|
420
|
+
* reason string when the receipt is over, null when within the bound. */
|
|
421
|
+
function overClockBound(stored, jobId) {
|
|
422
|
+
const openedAt = openedAtByJob.get(jobId);
|
|
423
|
+
if (openedAt == null) {
|
|
424
|
+
return 'session was never opened here';
|
|
425
|
+
}
|
|
426
|
+
const env = currentCatalog.find(o => o.model === sessionModelByJob.get(jobId)) ?? currentCatalog[0];
|
|
427
|
+
if (env == null) {
|
|
428
|
+
return 'worker has no priced model';
|
|
429
|
+
}
|
|
430
|
+
const unitMs = sessionUnitMsByJob.get(jobId) ?? 60_000;
|
|
431
|
+
const elapsed = Date.now() - openedAt + CLOCK_SLACK_MS;
|
|
432
|
+
const max = cumulativeBilled(env.pricePerHour, unitMs, Math.max(elapsed, unitMs));
|
|
433
|
+
if (stored.amount > max) {
|
|
434
|
+
return `exceeds the clock bound (${stored.amount} > ${max})`;
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
node,
|
|
440
|
+
peerAddrs: () => node.getMultiaddrs().map(m => m.toString()),
|
|
441
|
+
stop: async () => { await opts.modeManager.removeWorkerListeners(); await node.stop(); },
|
|
442
|
+
activeSessions: () => sessions,
|
|
443
|
+
peerIdStr: () => peerId.toString(),
|
|
444
|
+
setCatalog: (offers) => { currentCatalog = [...offers]; },
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
// data-plane net env the sidecar binds receipts to (single deployment — the
|
|
448
|
+
// base mainnet bindings stay empty until P6).
|
|
449
|
+
const ENV = 'base-sepolia';
|
|
450
|
+
//# sourceMappingURL=p2pNode.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type GenkakuPublic } from './chain.ts';
|
|
2
|
+
import type { NetEnvironment } from './netconfig.ts';
|
|
3
|
+
export type PaywallRequest = {
|
|
4
|
+
jobId: string;
|
|
5
|
+
escrowId: string;
|
|
6
|
+
clientPeer: string;
|
|
7
|
+
};
|
|
8
|
+
export type PaywallVerdict = {
|
|
9
|
+
ok: true; /** the escrow's payer — receipts must be signed by this wallet */
|
|
10
|
+
payer: `0x${string}`;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
code: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
export type Paywall = (request: PaywallRequest) => Promise<PaywallVerdict>;
|
|
17
|
+
export type PaywallOptions = {
|
|
18
|
+
publicC: GenkakuPublic;
|
|
19
|
+
env: NetEnvironment;
|
|
20
|
+
/** the wallet that must be the escrow's payee */
|
|
21
|
+
worker: `0x${string}`;
|
|
22
|
+
/** dev escape hatch: allow `escrowId: "0"` (unpriced) sessions */
|
|
23
|
+
allowUnpriced?: boolean;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
log?: {
|
|
26
|
+
warn: (msg: string, ...args: unknown[]) => void;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export declare function createPaywall(opts: PaywallOptions): Paywall;
|
|
30
|
+
//# sourceMappingURL=paywall.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paywall.d.ts","sourceRoot":"","sources":["../src/paywall.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC,CAAC,kEAAkE;IAAC,KAAK,EAAE,KAAK,MAAM,EAAE,CAAA;CAAE,GACrG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/C,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;AAK1E,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,aAAa,CAAA;IACtB,GAAG,EAAE,cAAc,CAAA;IACnB,iDAAiD;IACjD,MAAM,EAAE,KAAK,MAAM,EAAE,CAAA;IACrB,kEAAkE;IAClE,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;KAAE,CAAA;CAC1D,CAAA;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CA4C3D"}
|
package/dist/paywall.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// GENKAKU: worker paywall (P4.9d).
|
|
2
|
+
//
|
|
3
|
+
// A public directory makes a worker dialable by strangers. Without a gate,
|
|
4
|
+
// "dialable" means "free GPU": anyone who scans the chain can open a stream and
|
|
5
|
+
// burn inference time. The gate is therefore not a billing nicety — it is the
|
|
6
|
+
// condition for publishing an address at all.
|
|
7
|
+
//
|
|
8
|
+
// The check is cheap and self-contained: the client names an escrow id in the
|
|
9
|
+
// OPEN envelope; the worker reads that session from JobEscrow and serves only
|
|
10
|
+
// if the record exists, pays THIS worker, is still open, and has money in it.
|
|
11
|
+
// The money itself never moves here — settlement stays the client's signed
|
|
12
|
+
// receipt + JobEscrow.settle.
|
|
13
|
+
//
|
|
14
|
+
// Deliberate properties:
|
|
15
|
+
// * a verdict is cached for a few seconds (a retry storm must not become RPC
|
|
16
|
+
// load) but never longer than the escrow can change state;
|
|
17
|
+
// * the amount is a ceiling, not a reservation: two clients may name the same
|
|
18
|
+
// escrow, and the ledger still caps what can be settled;
|
|
19
|
+
// * `unpriced` (escrowId "0") is allowed ONLY when the operator explicitly
|
|
20
|
+
// disabled the paywall, so a dev session can never be reached by a stranger
|
|
21
|
+
// that merely guessed "0".
|
|
22
|
+
import { readEscrowSession } from "./chain.js";
|
|
23
|
+
const VERDICT_TTL_MS = 15_000;
|
|
24
|
+
const ZERO = '0x0000000000000000000000000000000000000000';
|
|
25
|
+
export function createPaywall(opts) {
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
const now = opts.now ?? Date.now;
|
|
28
|
+
return async (request) => {
|
|
29
|
+
const raw = (request.escrowId ?? '').trim();
|
|
30
|
+
if (raw === '' || raw === '0') {
|
|
31
|
+
// Unpriced sessions have no payer to certify; the dev escape hatch keeps
|
|
32
|
+
// them working for an explicit opt-out only.
|
|
33
|
+
return opts.allowUnpriced === true
|
|
34
|
+
? { ok: true, payer: ZERO }
|
|
35
|
+
: { ok: false, code: 'payment-required', reason: 'unpriced session rejected: open an escrow first' };
|
|
36
|
+
}
|
|
37
|
+
let id;
|
|
38
|
+
try {
|
|
39
|
+
id = BigInt(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { ok: false, code: 'bad-escrow', reason: 'escrow id is not an integer' };
|
|
43
|
+
}
|
|
44
|
+
const key = id.toString();
|
|
45
|
+
const hit = cache.get(key);
|
|
46
|
+
if (hit != null && now() - hit.at < VERDICT_TTL_MS)
|
|
47
|
+
return hit.verdict;
|
|
48
|
+
let verdict;
|
|
49
|
+
try {
|
|
50
|
+
const session = await readEscrowSession(opts.publicC, opts.env, id);
|
|
51
|
+
verdict = judge(session, opts.worker);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// An RPC failure must not silently turn into free compute: fail closed,
|
|
55
|
+
// but say so, because a flaky endpoint should be retried by the client.
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
opts.log?.warn('paywall: escrow %s read failed (%s)', key, message);
|
|
58
|
+
verdict = { ok: false, code: 'chain-unavailable', reason: 'could not verify the escrow; retry shortly' };
|
|
59
|
+
}
|
|
60
|
+
// Cache failures too: a stranger replaying a dead escrow id must not turn
|
|
61
|
+
// into one chain read per attempt.
|
|
62
|
+
cache.set(key, { at: now(), verdict });
|
|
63
|
+
return verdict;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function judge(session, worker) {
|
|
67
|
+
if (session == null) {
|
|
68
|
+
return { ok: false, code: 'unknown-escrow', reason: 'escrow session does not exist' };
|
|
69
|
+
}
|
|
70
|
+
if (session.worker.toLowerCase() !== worker.toLowerCase()) {
|
|
71
|
+
return { ok: false, code: 'wrong-worker', reason: 'escrow pays a different worker' };
|
|
72
|
+
}
|
|
73
|
+
if (session.settled || session.refunded) {
|
|
74
|
+
return { ok: false, code: 'closed-escrow', reason: 'escrow is already settled or refunded' };
|
|
75
|
+
}
|
|
76
|
+
if (session.amount <= 0n) {
|
|
77
|
+
return { ok: false, code: 'empty-escrow', reason: 'escrow has no funds' };
|
|
78
|
+
}
|
|
79
|
+
if (session.user.toLowerCase() === ZERO) {
|
|
80
|
+
return { ok: false, code: 'unknown-escrow', reason: 'escrow has no payer' };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, payer: session.user };
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=paywall.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** File name under the GenKaKu key directory. */
|
|
2
|
+
export declare const PUBLISH_OPT_IN_FILE = "publish.json";
|
|
3
|
+
/**
|
|
4
|
+
* Read the worker's directory opt-in.
|
|
5
|
+
* @param keyDir - GenKaKu state directory.
|
|
6
|
+
* @returns true only when the worker explicitly opted in.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readPublishOptIn(keyDir: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Persist the worker's directory opt-in.
|
|
11
|
+
* @param keyDir - GenKaKu state directory.
|
|
12
|
+
* @param publish - whether a public address may be written to the chain.
|
|
13
|
+
*/
|
|
14
|
+
export declare function writePublishOptIn(keyDir: string, publish: boolean): void;
|
|
15
|
+
//# sourceMappingURL=publishOptIn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publishOptIn.d.ts","sourceRoot":"","sources":["../src/publishOptIn.ts"],"names":[],"mappings":"AAUA,iDAAiD;AACjD,eAAO,MAAM,mBAAmB,iBAAiB,CAAA;AAEjD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAOxD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAGxE"}
|