lightnode-sdk 0.7.2 → 0.7.4

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/dist/dao.d.ts CHANGED
@@ -329,6 +329,26 @@ export declare const GOVERNOR_ABI: readonly [{
329
329
  readonly outputs: readonly [{
330
330
  readonly type: "bool";
331
331
  }];
332
+ }, {
333
+ readonly name: "cancel";
334
+ readonly type: "function";
335
+ readonly stateMutability: "nonpayable";
336
+ readonly inputs: readonly [{
337
+ readonly type: "address[]";
338
+ readonly name: "targets";
339
+ }, {
340
+ readonly type: "uint256[]";
341
+ readonly name: "values";
342
+ }, {
343
+ readonly type: "bytes[]";
344
+ readonly name: "calldatas";
345
+ }, {
346
+ readonly type: "bytes32";
347
+ readonly name: "descriptionHash";
348
+ }];
349
+ readonly outputs: readonly [{
350
+ readonly type: "uint256";
351
+ }];
332
352
  }, {
333
353
  readonly name: "ProposalCreated";
334
354
  readonly type: "event";
@@ -530,5 +550,54 @@ export declare class DAO {
530
550
  calldatas: `0x${string}`[];
531
551
  descriptionHash: `0x${string}`;
532
552
  }): Promise<`0x${string}`>;
553
+ /**
554
+ * Cancel a proposal. OZ Governor permits the proposer (and sometimes the
555
+ * Guardian / Timelock role) to cancel a Pending or Active proposal. Same
556
+ * `descriptionHash` you'd pass to `queue` / `execute`.
557
+ */
558
+ cancel(args: {
559
+ targets: `0x${string}`[];
560
+ values: bigint[];
561
+ calldatas: `0x${string}`[];
562
+ descriptionHash: `0x${string}`;
563
+ }): Promise<`0x${string}`>;
564
+ /**
565
+ * keccak256 of the raw description string. The Governor stores proposals
566
+ * keyed by `(targets, values, calldatas, descriptionHash)` rather than
567
+ * the description itself - this is the same hash the OZ Governor computes
568
+ * internally. Pass it to `queue`, `execute`, `cancel`, `hashProposal`.
569
+ */
570
+ descriptionHash(description: string): `0x${string}`;
571
+ /**
572
+ * Predict a proposal id BEFORE submitting. Mirrors `Governor.hashProposal`
573
+ * on chain so you can compute the id deterministically (the proposer can
574
+ * persist it ahead of time, drop it into a UI, or sanity-check that a
575
+ * proposal hasn't already been submitted).
576
+ */
577
+ hashProposal(args: {
578
+ targets: `0x${string}`[];
579
+ values: bigint[];
580
+ calldatas: `0x${string}`[];
581
+ /** Either the description string or the precomputed hash; both accepted. */
582
+ description: string | `0x${string}`;
583
+ }): Promise<bigint>;
584
+ /**
585
+ * IVotes.balanceOf - wrapped voting-token balance (LCAIBallots on Ethereum
586
+ * mainnet, native LCAI via the NativeVotes precompile on LightChain
587
+ * mainnet). Returns wei.
588
+ */
589
+ getBallotsBalance(voter: `0x${string}`): Promise<bigint>;
590
+ /**
591
+ * Address the voter has delegated to. Wraps `IVotes.delegates`. The zero
592
+ * address means the voter has not yet delegated, which is the OZ default
593
+ * (no voting power for self until you self-delegate).
594
+ */
595
+ getDelegate(voter: `0x${string}`): Promise<`0x${string}`>;
596
+ /**
597
+ * Delegate voting weight to `delegatee` (use the voter's own address to
598
+ * self-delegate, which is the common first step before voting). Wallet
599
+ * required.
600
+ */
601
+ delegate(delegatee: `0x${string}`): Promise<`0x${string}`>;
533
602
  }
534
603
  export {};
package/dist/dao.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * This module covers the OZ Governor v5 surface: state machine, propose,
18
18
  * castVote, queue, execute. Plus convenience reads of the constants.
19
19
  */
20
- import { parseAbi } from "viem";
20
+ import { keccak256, parseAbi, toBytes } from "viem";
21
21
  /**
22
22
  * Confirmed deployment addresses. Each entry is what an SDK user passes to
23
23
  * `new DAO(client, chainKey, walletClient?)`.
@@ -98,6 +98,7 @@ export const GOVERNOR_ABI = parseAbi([
98
98
  "function proposalEta(uint256 proposalId) external view returns (uint256)",
99
99
  "function getVotes(address account, uint256 timepoint) external view returns (uint256)",
100
100
  "function hasVoted(uint256 proposalId, address account) external view returns (bool)",
101
+ "function cancel(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) external returns (uint256)",
101
102
  // Events - needed for recentProposals() event scan.
102
103
  "event ProposalCreated(uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 voteStart, uint256 voteEnd, string description)",
103
104
  ]);
@@ -318,4 +319,97 @@ export class DAO {
318
319
  value: totalValue,
319
320
  });
320
321
  }
322
+ /**
323
+ * Cancel a proposal. OZ Governor permits the proposer (and sometimes the
324
+ * Guardian / Timelock role) to cancel a Pending or Active proposal. Same
325
+ * `descriptionHash` you'd pass to `queue` / `execute`.
326
+ */
327
+ cancel(args) {
328
+ if (!this.walletClient)
329
+ throw new Error("DAO: no wallet client; pass one to the DAO constructor for writes");
330
+ return this.walletClient.writeContract({
331
+ address: this.addresses.governor,
332
+ abi: GOVERNOR_ABI,
333
+ functionName: "cancel",
334
+ args: [args.targets, args.values, args.calldatas, args.descriptionHash],
335
+ });
336
+ }
337
+ // -------- Helpers (pure, no chain reads) --------
338
+ /**
339
+ * keccak256 of the raw description string. The Governor stores proposals
340
+ * keyed by `(targets, values, calldatas, descriptionHash)` rather than
341
+ * the description itself - this is the same hash the OZ Governor computes
342
+ * internally. Pass it to `queue`, `execute`, `cancel`, `hashProposal`.
343
+ */
344
+ descriptionHash(description) {
345
+ return keccak256(toBytes(description));
346
+ }
347
+ /**
348
+ * Predict a proposal id BEFORE submitting. Mirrors `Governor.hashProposal`
349
+ * on chain so you can compute the id deterministically (the proposer can
350
+ * persist it ahead of time, drop it into a UI, or sanity-check that a
351
+ * proposal hasn't already been submitted).
352
+ */
353
+ async hashProposal(args) {
354
+ const descHash = typeof args.description === "string" && args.description.startsWith("0x") && args.description.length === 66
355
+ ? args.description
356
+ : keccak256(toBytes(args.description));
357
+ return this.publicClient.readContract({
358
+ address: this.addresses.governor,
359
+ abi: GOVERNOR_ABI,
360
+ functionName: "hashProposal",
361
+ args: [args.targets, args.values, args.calldatas, descHash],
362
+ });
363
+ }
364
+ // -------- IVotes helpers (LCAIBallots wrapped token) --------
365
+ /**
366
+ * IVotes.balanceOf - wrapped voting-token balance (LCAIBallots on Ethereum
367
+ * mainnet, native LCAI via the NativeVotes precompile on LightChain
368
+ * mainnet). Returns wei.
369
+ */
370
+ getBallotsBalance(voter) {
371
+ const ballots = this.addresses.ballots;
372
+ if (!ballots)
373
+ throw new Error("DAO.getBallotsBalance: this chain has no Ballots address");
374
+ return this.publicClient.readContract({
375
+ address: ballots,
376
+ abi: VOTES_ABI,
377
+ functionName: "balanceOf",
378
+ args: [voter],
379
+ });
380
+ }
381
+ /**
382
+ * Address the voter has delegated to. Wraps `IVotes.delegates`. The zero
383
+ * address means the voter has not yet delegated, which is the OZ default
384
+ * (no voting power for self until you self-delegate).
385
+ */
386
+ getDelegate(voter) {
387
+ const ballots = this.addresses.ballots;
388
+ if (!ballots)
389
+ throw new Error("DAO.getDelegate: this chain has no Ballots address");
390
+ return this.publicClient.readContract({
391
+ address: ballots,
392
+ abi: VOTES_ABI,
393
+ functionName: "delegates",
394
+ args: [voter],
395
+ });
396
+ }
397
+ /**
398
+ * Delegate voting weight to `delegatee` (use the voter's own address to
399
+ * self-delegate, which is the common first step before voting). Wallet
400
+ * required.
401
+ */
402
+ delegate(delegatee) {
403
+ if (!this.walletClient)
404
+ throw new Error("DAO: no wallet client; pass one to the DAO constructor for writes");
405
+ const ballots = this.addresses.ballots;
406
+ if (!ballots)
407
+ throw new Error("DAO.delegate: this chain has no Ballots address");
408
+ return this.walletClient.writeContract({
409
+ address: ballots,
410
+ abi: VOTES_ABI,
411
+ functionName: "delegate",
412
+ args: [delegatee],
413
+ });
414
+ }
321
415
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
- import { fromWei } from "./subgraph.js";
2
+ import { fetchWorkerModels, fromWei, resolveJobTransactions } from "./subgraph.js";
3
3
  import { aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv } from "./analytics.js";
4
4
  import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream } from "./inference.js";
5
5
  import { Conversation, chat } from "./chat.js";
@@ -13,7 +13,7 @@ import { OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI,
13
13
  import { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker } from "./errors.js";
14
14
  import { GatewayClient, GatewayHttpError } from "./gateway.js";
15
15
  import * as crypto from "./crypto.js";
16
- import type { NetworkId, NetworkConfig, Worker, Job, ModelInfo, NetworkStats, ModelStat, WorkerStat, NetworkAnalytics } from "./types.js";
16
+ import type { NetworkId, NetworkConfig, Worker, Job, JobTransactions, ModelInfo, WorkerModel, NetworkStats, ModelStat, WorkerStat, NetworkAnalytics } from "./types.js";
17
17
  /**
18
18
  * Read-only client for a LightChain AI network. Pure reads from the public indexer
19
19
  * and the chain; no keys, no writes. Independent, community-built.
@@ -33,6 +33,15 @@ export declare class LightNode {
33
33
  getWorker(address: string): Promise<Worker | null>;
34
34
  /** Recent jobs for one worker, newest first. */
35
35
  getWorkerJobs(address: string, first?: number): Promise<Job[]>;
36
+ /**
37
+ * The on-chain model whitelist for one worker (rows from WorkerRegistry
38
+ * events). Use this when you need to answer "what models is this worker
39
+ * offering" - it is the authoritative signal, not derived from past jobs.
40
+ * Rows can be `is_active: false` (operator removed the registration) or
41
+ * outlive deregister: combine with {@link getWorker}.status to decide
42
+ * whether the worker is currently serving them.
43
+ */
44
+ getWorkerModels(address: string): Promise<WorkerModel[]>;
36
45
  /** The network's registered models (name, fee, output limit, whitelist flags). */
37
46
  getModels(): Promise<ModelInfo[]>;
38
47
  /** Registered workers (default top 200). */
@@ -59,7 +68,9 @@ export declare class LightNode {
59
68
  * builder-friendly label; `raw` is the indexer's literal state string.
60
69
  * Null when the indexer has never seen the job (still pending propagation).
61
70
  */
62
- getJobStatus(jobId: string | bigint): Promise<{
71
+ getJobStatus(jobId: string | bigint, opts?: {
72
+ withTransactions?: boolean;
73
+ }): Promise<{
63
74
  id: string;
64
75
  raw: string;
65
76
  category: "submitted" | "in-flight" | "completed" | "stalled" | "disputed" | "resolved" | "unknown";
@@ -69,7 +80,27 @@ export declare class LightNode {
69
80
  completedAt: number | null;
70
81
  workerShareLcai: number;
71
82
  refundable: boolean;
83
+ /** Block numbers as the indexer recorded them. Null until indexer sees the event. */
84
+ submitBlock: number | null;
85
+ completionBlock: number | null;
86
+ /**
87
+ * Tx hashes for submitJob + jobCompleted, only resolved when
88
+ * `withTransactions: true`. Each hash deep-links to Lightscan via
89
+ * {@link Network.explorerTxUrl}. Costs one eth_getLogs RPC call per
90
+ * transaction (max two per job); skip the flag if you don't need them.
91
+ */
92
+ submitTx: `0x${string}` | null;
93
+ completionTx: `0x${string}` | null;
72
94
  } | null>;
95
+ /**
96
+ * Build a Lightscan URL for an arbitrary address or tx hash on this
97
+ * network. Useful for surfacing deep-links in builder UIs without
98
+ * each consumer needing to know which explorer corresponds to which
99
+ * chain.
100
+ */
101
+ explorerAddressUrl(address: string): string;
102
+ explorerTxUrl(hash: string): string;
103
+ explorerBlockUrl(block: number | bigint): string;
73
104
  /** keccak256 of a model tag (its on-chain + indexer id). */
74
105
  modelId(tag: string): `0x${string}`;
75
106
  /** On-chain inference fee for a model, in whole LCAI (what submitJob must be paid). */
@@ -91,8 +122,8 @@ export declare class LightNode {
91
122
  * (especially in registry-proxy environments like StackBlitz where lockfiles
92
123
  * may pin an older minor than the local install command suggests).
93
124
  */
94
- export declare const SDK_VERSION = "0.7.2";
95
- export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
125
+ export declare const SDK_VERSION = "0.7.4";
126
+ export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
96
127
  export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
97
128
  export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
98
129
  export type { ChatRole, ChatMessage, ConversationOptions, ConversationSendResult } from "./chat.js";
@@ -103,4 +134,4 @@ export type { BridgeChain, BridgeEndpoints, BridgeTransferArgs } from "./bridge.
103
134
  export type { DaoChain, DaoAddresses, ProposalSummary, ProposalRow, DaoConfig } from "./dao.js";
104
135
  export type { BaseModel, ModelVariant, AccessTier, AccessPolicy, Benchmark, OnchainModelRegistryOptions } from "./onchain-models.js";
105
136
  export type { MinimalWalletClient, MinimalPublicClient, WorkerOperatorOpts, WorkerProtocolConfig, WorkerStatus, DeregisterReadiness, StuckJob, EarningsBreakdown, OnchainJob, JobState, DecodedWorkerError, } from "./worker-operator.js";
106
- export type { NetworkId, NetworkConfig, Worker, Job, ModelInfo, NetworkStats, ModelStat, WorkerStat, NetworkAnalytics };
137
+ export type { NetworkId, NetworkConfig, Worker, Job, JobTransactions, ModelInfo, WorkerModel, NetworkStats, ModelStat, WorkerStat, NetworkAnalytics };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
- import { fetchWorker, fetchWorkerJobs, fetchRecentJobs, fetchJob, fetchModels, fetchWorkers, summarize, fromWei, } from "./subgraph.js";
2
+ import { fetchWorker, fetchWorkerJobs, fetchWorkerModels, fetchRecentJobs, fetchJob, fetchModels, fetchWorkers, summarize, fromWei, resolveJobTransactions, } from "./subgraph.js";
3
3
  import { isRegistered } from "./onchain.js";
4
4
  import { aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, } from "./analytics.js";
5
5
  import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream, } from "./inference.js";
@@ -40,6 +40,17 @@ export class LightNode {
40
40
  getWorkerJobs(address, first = 20) {
41
41
  return fetchWorkerJobs(this.network, address, first);
42
42
  }
43
+ /**
44
+ * The on-chain model whitelist for one worker (rows from WorkerRegistry
45
+ * events). Use this when you need to answer "what models is this worker
46
+ * offering" - it is the authoritative signal, not derived from past jobs.
47
+ * Rows can be `is_active: false` (operator removed the registration) or
48
+ * outlive deregister: combine with {@link getWorker}.status to decide
49
+ * whether the worker is currently serving them.
50
+ */
51
+ getWorkerModels(address) {
52
+ return fetchWorkerModels(this.network, address);
53
+ }
43
54
  /** The network's registered models (name, fee, output limit, whitelist flags). */
44
55
  getModels() {
45
56
  return fetchModels(this.network);
@@ -86,7 +97,7 @@ export class LightNode {
86
97
  * builder-friendly label; `raw` is the indexer's literal state string.
87
98
  * Null when the indexer has never seen the job (still pending propagation).
88
99
  */
89
- async getJobStatus(jobId) {
100
+ async getJobStatus(jobId, opts = {}) {
90
101
  const j = await fetchJob(this.network, jobId);
91
102
  if (!j)
92
103
  return null;
@@ -110,6 +121,11 @@ export class LightNode {
110
121
  // own timeout/dispute pipeline reclaims the fee; this flag is the SDK's
111
122
  // builder-facing hint that the on-chain refund call is the right path.
112
123
  const refundable = category === "stalled" || category === "disputed";
124
+ // Tx hashes need a second RPC roundtrip. Opt in only - the historical
125
+ // shape of getJobStatus stays pure-subgraph for callers who don't ask.
126
+ const txs = opts.withTransactions
127
+ ? await resolveJobTransactions(this.network, j.id, { job: j })
128
+ : { submit: null, completion: null };
113
129
  return {
114
130
  id: j.id,
115
131
  raw: state,
@@ -120,8 +136,27 @@ export class LightNode {
120
136
  completedAt: j.completed_at ?? null,
121
137
  workerShareLcai: fromWei(j.worker_share),
122
138
  refundable,
139
+ submitBlock: j.submit_block_number ?? null,
140
+ completionBlock: j.completion_block_number && j.completion_block_number > 0 ? j.completion_block_number : null,
141
+ submitTx: txs.submit,
142
+ completionTx: txs.completion,
123
143
  };
124
144
  }
145
+ /**
146
+ * Build a Lightscan URL for an arbitrary address or tx hash on this
147
+ * network. Useful for surfacing deep-links in builder UIs without
148
+ * each consumer needing to know which explorer corresponds to which
149
+ * chain.
150
+ */
151
+ explorerAddressUrl(address) {
152
+ return `${this.network.explorer}/address/${address}`;
153
+ }
154
+ explorerTxUrl(hash) {
155
+ return `${this.network.explorer}/tx/${hash}`;
156
+ }
157
+ explorerBlockUrl(block) {
158
+ return `${this.network.explorer}/block/${block.toString()}`;
159
+ }
125
160
  /** keccak256 of a model tag (its on-chain + indexer id). */
126
161
  modelId(tag) {
127
162
  return computeModelId(tag);
@@ -146,8 +181,14 @@ export class LightNode {
146
181
  * (especially in registry-proxy environments like StackBlitz where lockfiles
147
182
  * may pin an older minor than the local install command suggests).
148
183
  */
149
- export const SDK_VERSION = "0.7.2";
150
- export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost,
184
+ export const SDK_VERSION = "0.7.4";
185
+ export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei,
186
+ // v0.7.3 per-job transaction-hash resolver (lifts the upstream
187
+ // subgraph's "block-only" Job entity to a deep-linkable Job + tx pair).
188
+ resolveJobTransactions,
189
+ // v0.7.4 per-worker model-registration list (the authoritative "what is
190
+ // this worker offering to serve" signal, not derived from past jobs).
191
+ fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost,
151
192
  // v0.3 inference-submit surface (BETA - see README "Submitting inference").
152
193
  GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto,
153
194
  // v0.4 high-level orchestrator: one call, full flow.
@@ -1,4 +1,5 @@
1
- import type { NetworkConfig, Worker, Job, ModelInfo, NetworkStats } from "./types.js";
1
+ import type { PublicClient } from "viem";
2
+ import type { NetworkConfig, Worker, Job, JobTransactions, ModelInfo, NetworkStats, WorkerModel } from "./types.js";
2
3
  /** Convert a wei string to a number of whole tokens (18 decimals). */
3
4
  export declare function fromWei(wei?: string): number;
4
5
  export declare function fetchWorker(cfg: NetworkConfig, address: string): Promise<Worker | null>;
@@ -7,6 +8,33 @@ export declare function fetchJob(cfg: NetworkConfig, jobId: string | bigint): Pr
7
8
  export declare function fetchWorkerJobs(cfg: NetworkConfig, address: string, first?: number): Promise<Job[]>;
8
9
  /** Recent jobs across the whole network (not one worker), for analytics. */
9
10
  export declare function fetchRecentJobs(cfg: NetworkConfig, first?: number): Promise<Job[]>;
11
+ /**
12
+ * Resolve a job's tx hashes (submitJob + jobCompleted) by re-scanning the
13
+ * exact blocks the indexer recorded. The upstream subgraph entity stores
14
+ * `submit_block_number` + `completion_block_number` but NOT the
15
+ * transactionHash. Re-deriving them needs one RPC eth_getLogs call per
16
+ * block: at most two calls per job, and each is tightly bounded
17
+ * (single-block range, single contract, single topic, single jobId match).
18
+ *
19
+ * `submit` is null only if the indexer hasn't seen the job yet (we couldn't
20
+ * resolve the block). `completion` is null until the worker emits
21
+ * JobCompleted (still-in-flight or stalled jobs).
22
+ *
23
+ * Pass a `publicClient` to reuse an existing RPC connection. Without one
24
+ * the function builds a transient client from `cfg.rpc` - simple but spends
25
+ * a TCP handshake per call.
26
+ */
27
+ export declare function resolveJobTransactions(cfg: NetworkConfig, jobId: string | bigint, opts?: {
28
+ publicClient?: PublicClient;
29
+ job?: Job | null;
30
+ }): Promise<JobTransactions>;
31
+ /**
32
+ * The on-chain model registrations for one worker. The Graph entity name is
33
+ * `workermodels` (lowercase), and `is_active` flips when the operator
34
+ * removes a registration. Independent of `Worker.status`: a deregistered
35
+ * worker can still have rows here from when it was live.
36
+ */
37
+ export declare function fetchWorkerModels(cfg: NetworkConfig, address: string): Promise<WorkerModel[]>;
10
38
  export declare function fetchModels(cfg: NetworkConfig): Promise<ModelInfo[]>;
11
39
  export declare function fetchWorkers(cfg: NetworkConfig, first?: number): Promise<Worker[]>;
12
40
  export declare function summarize(workers: Worker[], models: ModelInfo[]): NetworkStats;
package/dist/subgraph.js CHANGED
@@ -1,4 +1,8 @@
1
- import { getAddress } from "viem";
1
+ import { createPublicClient, getAddress, http, toHex, pad } from "viem";
2
+ // keccak256("JobSubmitted(uint256,uint256,address)")
3
+ const JOB_SUBMITTED_TOPIC = "0xfb47370368875d7490803c5653d9496d0a3c5e1b49a17f013ec37abd9d86d356";
4
+ // keccak256("JobCompleted(uint256,address,bytes32,bytes32)")
5
+ const JOB_COMPLETED_TOPIC = "0xdb545db74bae046337ed01971cf61569fd1a1460ff8ed511ab19ceaac1326377";
2
6
  const TIMEOUT_MS = 12000;
3
7
  async function gql(url, query) {
4
8
  const ctrl = new AbortController();
@@ -59,18 +63,95 @@ export async function fetchWorker(cfg, address) {
59
63
  /** Fetch one job by its on-chain id. Null when the indexer has never seen it. */
60
64
  export async function fetchJob(cfg, jobId) {
61
65
  const id = typeof jobId === "bigint" ? jobId.toString() : jobId;
62
- const data = await gql(cfg.subgraph, `{ job(id:"${id}") { id state model_id worker submitted_at ack_at completed_at worker_share } }`);
66
+ const data = await gql(cfg.subgraph, `{ job(id:"${id}") { id state model_id worker submitted_at ack_at completed_at worker_share submit_block_number completion_block_number } }`);
63
67
  return data.job ?? null;
64
68
  }
65
69
  export async function fetchWorkerJobs(cfg, address, first = 20) {
66
- const data = await gql(cfg.subgraph, `{ jobs(first:${first}, orderBy:submitted_at, orderDirection:desc, where:{worker:"${checksum(address)}"}) { id state model_id submitted_at ack_at completed_at worker_share } }`);
70
+ const data = await gql(cfg.subgraph, `{ jobs(first:${first}, orderBy:submitted_at, orderDirection:desc, where:{worker:"${checksum(address)}"}) { id state model_id submitted_at ack_at completed_at worker_share submit_block_number completion_block_number } }`);
67
71
  return data.jobs ?? [];
68
72
  }
69
73
  /** Recent jobs across the whole network (not one worker), for analytics. */
70
74
  export async function fetchRecentJobs(cfg, first = 1000) {
71
- const data = await gql(cfg.subgraph, `{ jobs(first:${first}, orderBy:submitted_at, orderDirection:desc) { id state model_id worker ack_at completed_at worker_share } }`);
75
+ const data = await gql(cfg.subgraph, `{ jobs(first:${first}, orderBy:submitted_at, orderDirection:desc) { id state model_id worker ack_at completed_at worker_share submit_block_number completion_block_number } }`);
72
76
  return data.jobs ?? [];
73
77
  }
78
+ /**
79
+ * Resolve a job's tx hashes (submitJob + jobCompleted) by re-scanning the
80
+ * exact blocks the indexer recorded. The upstream subgraph entity stores
81
+ * `submit_block_number` + `completion_block_number` but NOT the
82
+ * transactionHash. Re-deriving them needs one RPC eth_getLogs call per
83
+ * block: at most two calls per job, and each is tightly bounded
84
+ * (single-block range, single contract, single topic, single jobId match).
85
+ *
86
+ * `submit` is null only if the indexer hasn't seen the job yet (we couldn't
87
+ * resolve the block). `completion` is null until the worker emits
88
+ * JobCompleted (still-in-flight or stalled jobs).
89
+ *
90
+ * Pass a `publicClient` to reuse an existing RPC connection. Without one
91
+ * the function builds a transient client from `cfg.rpc` - simple but spends
92
+ * a TCP handshake per call.
93
+ */
94
+ export async function resolveJobTransactions(cfg, jobId, opts = {}) {
95
+ const id = typeof jobId === "bigint" ? jobId : BigInt(jobId);
96
+ const job = opts.job !== undefined ? opts.job : await fetchJob(cfg, id);
97
+ if (!job)
98
+ return { submit: null, completion: null };
99
+ const client = opts.publicClient ??
100
+ createPublicClient({ transport: http(cfg.rpc) });
101
+ // Topic 1 is the indexed jobId, padded to 32 bytes. The subgraph keys
102
+ // entries by exact on-chain jobId so this match is unambiguous.
103
+ const jobTopic = pad(toHex(id), { size: 32 });
104
+ const submitBlock = job.submit_block_number ? BigInt(job.submit_block_number) : null;
105
+ const completionBlock = job.completion_block_number ? BigInt(job.completion_block_number) : null;
106
+ // Address the contract that emits Job* events. JobRegistry handles both.
107
+ const address = cfg.jobRegistry;
108
+ if (!address)
109
+ return { submit: null, completion: null };
110
+ const [submitTx, completionTx] = await Promise.all([
111
+ submitBlock !== null
112
+ ? fetchTxHashForJobEvent(client, address, JOB_SUBMITTED_TOPIC, jobTopic, submitBlock)
113
+ : Promise.resolve(null),
114
+ completionBlock !== null && completionBlock > 0n
115
+ ? fetchTxHashForJobEvent(client, address, JOB_COMPLETED_TOPIC, jobTopic, completionBlock)
116
+ : Promise.resolve(null),
117
+ ]);
118
+ return { submit: submitTx, completion: completionTx };
119
+ }
120
+ async function fetchTxHashForJobEvent(client, address, eventTopic, jobTopic, block) {
121
+ // Bypass viem's typed getLogs - it rejects the [topic0, topic1] tuple
122
+ // without a parsed `event` ABI, and we don't want to ship the full ABI
123
+ // through this path. The raw eth_getLogs at the transport layer is what
124
+ // we need: single block, single contract, two-topic match (event sig +
125
+ // indexed jobId), so the response is at most one log.
126
+ try {
127
+ const blockHex = `0x${block.toString(16)}`;
128
+ const logs = (await client.request({
129
+ method: "eth_getLogs",
130
+ params: [
131
+ {
132
+ address,
133
+ fromBlock: blockHex,
134
+ toBlock: blockHex,
135
+ topics: [eventTopic, jobTopic],
136
+ },
137
+ ],
138
+ }));
139
+ return logs[0]?.transactionHash ?? null;
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ /**
146
+ * The on-chain model registrations for one worker. The Graph entity name is
147
+ * `workermodels` (lowercase), and `is_active` flips when the operator
148
+ * removes a registration. Independent of `Worker.status`: a deregistered
149
+ * worker can still have rows here from when it was live.
150
+ */
151
+ export async function fetchWorkerModels(cfg, address) {
152
+ const data = await gql(cfg.subgraph, `{ workermodels(where:{worker:"${checksum(address)}"}) { id worker model_id is_active created_at updated_at } }`);
153
+ return data.workermodels ?? [];
154
+ }
74
155
  export async function fetchModels(cfg) {
75
156
  const data = await gql(cfg.subgraph, `{ modelinfos { id name fee max_output_tokens is_whitelisted is_enabled } }`);
76
157
  return data.modelinfos ?? [];
package/dist/types.d.ts CHANGED
@@ -43,6 +43,17 @@ export interface Job {
43
43
  ack_at?: number;
44
44
  completed_at?: number;
45
45
  worker_share?: string;
46
+ submit_block_number?: number;
47
+ completion_block_number?: number;
48
+ }
49
+ /**
50
+ * Per-job transaction hashes. Populated lazily by the SDK via
51
+ * resolveJobTransactions(); not present on the raw subgraph Job entity.
52
+ * `completion` is null until the worker emits JobCompleted.
53
+ */
54
+ export interface JobTransactions {
55
+ submit: `0x${string}` | null;
56
+ completion: `0x${string}` | null;
46
57
  }
47
58
  export interface ModelInfo {
48
59
  id: string;
@@ -52,6 +63,21 @@ export interface ModelInfo {
52
63
  is_whitelisted: boolean;
53
64
  is_enabled: boolean;
54
65
  }
66
+ /**
67
+ * Per-worker model registration row. The on-chain truth for "which models has
68
+ * this worker offered to serve" - indexed from WorkerRegistry events.
69
+ * `is_active` flips to false when the operator removes the registration; it
70
+ * is independent of whether the worker itself is currently registered in the
71
+ * protocol.
72
+ */
73
+ export interface WorkerModel {
74
+ id: string;
75
+ worker: string;
76
+ model_id: string;
77
+ is_active: boolean;
78
+ created_at?: number;
79
+ updated_at?: number;
80
+ }
55
81
  export interface NetworkStats {
56
82
  total: number;
57
83
  active: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",