openshard-shared 1.0.1

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/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @openshard/shared
2
+
3
+ > Shared utilities, types, constants, Prometheus metrics, and payment verification helpers for the OpenShard ecosystem.
4
+
5
+ This package provides common foundational code consumed across buyer proxies, seller nodes, registry servers, and CLI tools in the OpenShard monorepo.
6
+
7
+ ## Modules
8
+
9
+ - `prometheus.js`: Shared Prometheus registry, metric formatting, and monitoring wrappers.
10
+ - `payments.js`: Cryptographic helpers, EIP-712 structured data signing, USDC formatting utilities, and x402 verification logic.
11
+ - `index.js`: Common constants, networking defaults, port mappings, and error formatting structures.
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "openshard-shared",
3
+ "version": "1.0.1",
4
+ "description": "Shared types, constants, and utilities for the anteseed network",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "echo 'shared has no build step'"
9
+ },
10
+ "dependencies": {
11
+ "viem": "^2.21.19"
12
+ },
13
+ "keywords": [],
14
+ "license": "ISC"
15
+ }
package/src/index.js ADDED
@@ -0,0 +1,57 @@
1
+ // ─── Shared constants & types for the anteseed network ───────────────────────
2
+
3
+ export const REGISTRY_PORT = 9000;
4
+ export const BUYER_PORT = 8377;
5
+ export const SELLER_PORT = 8378; // default; each seller can override via env
6
+
7
+ // ─── Provider capability advertised to the registry ──────────────────────────
8
+
9
+ /**
10
+ * @typedef {Object} ServicePricing
11
+ * @property {number} inputUsdPerMillion
12
+ * @property {number} cachedInputUsdPerMillion
13
+ * @property {number} outputUsdPerMillion
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} ServiceDefinition
18
+ * @property {string} id - buyer-facing model name, e.g. "claude-sonnet-4-6"
19
+ * @property {string} upstreamModel - actual model name sent to the upstream API
20
+ * @property {string[]} categories - ["chat","coding","math"]
21
+ * @property {ServicePricing} pricing
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} ProviderAdvertisement
26
+ * @property {string} peerId - hex public key / node identity
27
+ * @property {string} endpoint - http://host:port the buyer connects to
28
+ * @property {ServiceDefinition[]} services
29
+ * @property {number} registeredAt - unix ms
30
+ * @property {number} lastSeen - unix ms
31
+ */
32
+
33
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Returns true if the peer heartbeat is still considered "live"
37
+ * (seen within the last 60 seconds).
38
+ * @param {ProviderAdvertisement} peer
39
+ */
40
+ export function isAlive(peer) {
41
+ return Date.now() - peer.lastSeen < 60_000;
42
+ }
43
+
44
+ /**
45
+ * Build a standard error body used across all Fastify nodes.
46
+ * @param {string} code - machine-readable error code
47
+ * @param {string} message - human-readable detail
48
+ */
49
+ export function errorBody(code, message) {
50
+ return { error: { code, message } };
51
+ }
52
+
53
+ // ─── Re-export payment utilities ─────────────────────────────────────────────
54
+ export * from './payments.js';
55
+
56
+ // ─── Re-export prometheus utilities ───────────────────────────────────────────
57
+ export * from './prometheus.js';
@@ -0,0 +1,217 @@
1
+ /**
2
+ * openshard_ggshared — payments.js
3
+ *
4
+ * EIP-712 signing utilities for ReserveAuth and SpendingAuth messages.
5
+ * Used by both the buyer proxy (signing) and the seller node (verification).
6
+ *
7
+ * Requires: viem (npm install viem)
8
+ */
9
+
10
+ import {
11
+ createWalletClient,
12
+ createPublicClient,
13
+ http,
14
+ keccak256,
15
+ toBytes,
16
+ concat,
17
+ encodeAbiParameters,
18
+ parseAbiParameters,
19
+ recoverTypedDataAddress,
20
+ parseUnits,
21
+ formatUnits
22
+ } from 'viem';
23
+ import { privateKeyToAccount } from 'viem/accounts';
24
+ import { base, baseSepolia } from 'viem/chains';
25
+ import { hardhat } from 'viem/chains';
26
+
27
+ // ─── USDC constants ───────────────────────────────────────────────────────────
28
+
29
+ /** USDC on Base mainnet */
30
+ export const USDC_BASE_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
31
+ /** USDC on Base Sepolia */
32
+ export const USDC_SEPOLIA_ADDRESS = '0x4eF7a082726B9E4fd136ED58B212F435b7b17594';
33
+
34
+ /** USDC decimals */
35
+ export const USDC_DECIMALS = 6;
36
+
37
+ /** Convert human USDC amount (e.g. "1.5") to USDC base units (BigInt) */
38
+ export const parseUsdc = (amount) => parseUnits(String(amount), USDC_DECIMALS);
39
+ /** Convert USDC base units back to human-readable string */
40
+ export const formatUsdc = (amount) => formatUnits(amount, USDC_DECIMALS);
41
+
42
+ // ─── EIP-712 domain builder ───────────────────────────────────────────────────
43
+
44
+ /**
45
+ * Builds the EIP-712 domain object for OpenshardDeposits contract.
46
+ * @param {string} contractAddress - deployed OpenshardDeposits address
47
+ * @param {number} chainId
48
+ */
49
+ export function buildDomain(contractAddress, chainId) {
50
+ return {
51
+ name: 'OpenShardPayments',
52
+ version: '1',
53
+ chainId,
54
+ verifyingContract: contractAddress
55
+ };
56
+ }
57
+
58
+ // ─── Type definitions (viem format) ──────────────────────────────────────────
59
+
60
+ export const RESERVE_AUTH_TYPES = {
61
+ ReserveAuth: [
62
+ { name: 'channelId', type: 'bytes32' },
63
+ { name: 'buyer', type: 'address' },
64
+ { name: 'seller', type: 'address' },
65
+ { name: 'maxAmount', type: 'uint256' },
66
+ { name: 'deadline', type: 'uint256' }
67
+ ]
68
+ };
69
+
70
+ export const SPENDING_AUTH_TYPES = {
71
+ SpendingAuth: [
72
+ { name: 'channelId', type: 'bytes32' },
73
+ { name: 'cumulativeAmount', type: 'uint256' },
74
+ { name: 'metadataHash', type: 'bytes32' }
75
+ ]
76
+ };
77
+
78
+ // ─── Channel ID generation ────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * Deterministic channel ID derived from buyer + seller addresses and a nonce.
82
+ * @param {string} buyerAddress
83
+ * @param {string} sellerAddress
84
+ * @param {number} nonce - monotonically increasing per buyer-seller pair
85
+ * @returns {`0x${string}`} bytes32 hex
86
+ */
87
+ export function buildChannelId(buyerAddress, sellerAddress, nonce) {
88
+ return keccak256(
89
+ encodeAbiParameters(
90
+ parseAbiParameters('address buyer, address seller, uint256 nonce'),
91
+ [buyerAddress, sellerAddress, BigInt(nonce)]
92
+ )
93
+ );
94
+ }
95
+
96
+ // ─── Metadata hash ────────────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Build the metadata hash included in each SpendingAuth.
100
+ * Captures the token counts and request fingerprint.
101
+ *
102
+ * @param {{ inputTokens: number, outputTokens: number, requestId: string }} meta
103
+ * @returns {`0x${string}`}
104
+ */
105
+ export function buildMetadataHash({ inputTokens, outputTokens, requestId }) {
106
+ return keccak256(
107
+ encodeAbiParameters(
108
+ parseAbiParameters('uint256 inputTokens, uint256 outputTokens, string requestId'),
109
+ [BigInt(inputTokens), BigInt(outputTokens), requestId]
110
+ )
111
+ );
112
+ }
113
+
114
+ // ─── Signing (buyer side) ─────────────────────────────────────────────────────
115
+
116
+ /**
117
+ * Sign a ReserveAuth message with the buyer's private key.
118
+ * The seller will submit this signature on-chain to lock buyer funds.
119
+ *
120
+ * @param {{ privateKey: string, contractAddress: string, chainId: number }} opts
121
+ * @param {{ channelId: string, seller: string, maxAmount: bigint, deadline: bigint }} params
122
+ * @returns {Promise<string>} EIP-712 hex signature
123
+ */
124
+ export async function signReserveAuth({ privateKey, contractAddress, chainId }, params) {
125
+ const account = privateKeyToAccount(privateKey);
126
+ const domain = buildDomain(contractAddress, chainId);
127
+
128
+ return account.signTypedData({
129
+ domain,
130
+ types: RESERVE_AUTH_TYPES,
131
+ primaryType: 'ReserveAuth',
132
+ message: {
133
+ channelId: params.channelId,
134
+ buyer: account.address,
135
+ seller: params.seller,
136
+ maxAmount: params.maxAmount,
137
+ deadline: params.deadline
138
+ }
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Sign a SpendingAuth message.
144
+ * Called by the buyer after each successful inference request.
145
+ *
146
+ * @param {{ privateKey: string, contractAddress: string, chainId: number }} opts
147
+ * @param {{ channelId: string, cumulativeAmount: bigint, metadataHash: string }} params
148
+ * @returns {Promise<string>} EIP-712 hex signature
149
+ */
150
+ export async function signSpendingAuth({ privateKey, contractAddress, chainId }, params) {
151
+ const account = privateKeyToAccount(privateKey);
152
+ const domain = buildDomain(contractAddress, chainId);
153
+
154
+ return account.signTypedData({
155
+ domain,
156
+ types: SPENDING_AUTH_TYPES,
157
+ primaryType: 'SpendingAuth',
158
+ message: {
159
+ channelId: params.channelId,
160
+ cumulativeAmount: params.cumulativeAmount,
161
+ metadataHash: params.metadataHash
162
+ }
163
+ });
164
+ }
165
+
166
+ // ─── Verification (seller side) ───────────────────────────────────────────────
167
+
168
+ /**
169
+ * Recover the signer address from a ReserveAuth signature.
170
+ * Seller uses this to verify the buyer's authorization before calling reserve().
171
+ *
172
+ * @param {{ contractAddress: string, chainId: number }} opts
173
+ * @param {{ channelId: string, buyer: string, seller: string, maxAmount: bigint, deadline: bigint }} params
174
+ * @param {string} signature
175
+ * @returns {Promise<string>} recovered signer address
176
+ */
177
+ export async function recoverReserveAuthSigner({ contractAddress, chainId }, params, signature) {
178
+ return recoverTypedDataAddress({
179
+ domain: buildDomain(contractAddress, chainId),
180
+ types: RESERVE_AUTH_TYPES,
181
+ primaryType: 'ReserveAuth',
182
+ message: params,
183
+ signature
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Recover the signer address from a SpendingAuth signature.
189
+ *
190
+ * @param {{ contractAddress: string, chainId: number }} opts
191
+ * @param {{ channelId: string, cumulativeAmount: bigint, metadataHash: string }} params
192
+ * @param {string} signature
193
+ * @returns {Promise<string>} recovered signer address
194
+ */
195
+ export async function recoverSpendingAuthSigner({ contractAddress, chainId }, params, signature) {
196
+ return recoverTypedDataAddress({
197
+ domain: buildDomain(contractAddress, chainId),
198
+ types: SPENDING_AUTH_TYPES,
199
+ primaryType: 'SpendingAuth',
200
+ message: params,
201
+ signature
202
+ });
203
+ }
204
+
205
+ // ─── Chain helpers ────────────────────────────────────────────────────────────
206
+
207
+ export const CHAINS = {
208
+ 'base': base,
209
+ 'base-sepolia': baseSepolia,
210
+ 'localhost': hardhat
211
+ };
212
+
213
+ export function getChain(name) {
214
+ const chain = CHAINS[name];
215
+ if (!chain) throw new Error(`Unknown chain: ${name}. Supported: ${Object.keys(CHAINS).join(', ')}`);
216
+ return chain;
217
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * openshard_ggshared — prometheus.js
3
+ *
4
+ * Minimal Prometheus text format builder.
5
+ * No external dependencies — just string building.
6
+ *
7
+ * Supports: Counter, Gauge, Histogram
8
+ */
9
+
10
+ // ─── Metric types ─────────────────────────────────────────────────────────────
11
+
12
+ class Counter {
13
+ constructor(name, help, labelNames = []) {
14
+ this.name = name;
15
+ this.help = help;
16
+ this.labelNames = labelNames;
17
+ this._values = new Map();
18
+ }
19
+
20
+ _key(labels) { return JSON.stringify(labels ?? {}); }
21
+
22
+ inc(labels = {}, value = 1) {
23
+ const k = this._key(labels);
24
+ this._values.set(k, (this._values.get(k) ?? 0) + value);
25
+ }
26
+
27
+ render() {
28
+ const lines = [
29
+ `# HELP ${this.name} ${this.help}`,
30
+ `# TYPE ${this.name} counter`
31
+ ];
32
+ for (const [key, val] of this._values) {
33
+ const labels = JSON.parse(key);
34
+ lines.push(`${this.name}${renderLabels(labels)} ${val}`);
35
+ }
36
+ return lines.join('\n');
37
+ }
38
+ }
39
+
40
+ class Gauge {
41
+ constructor(name, help, labelNames = []) {
42
+ this.name = name;
43
+ this.help = help;
44
+ this.labelNames = labelNames;
45
+ this._values = new Map();
46
+ }
47
+
48
+ _key(labels) { return JSON.stringify(labels ?? {}); }
49
+
50
+ set(labels = {}, value) {
51
+ this._values.set(this._key(labels), value);
52
+ }
53
+
54
+ inc(labels = {}, value = 1) {
55
+ const k = this._key(labels);
56
+ this._values.set(k, (this._values.get(k) ?? 0) + value);
57
+ }
58
+
59
+ dec(labels = {}, value = 1) {
60
+ const k = this._key(labels);
61
+ this._values.set(k, (this._values.get(k) ?? 0) - value);
62
+ }
63
+
64
+ render() {
65
+ const lines = [
66
+ `# HELP ${this.name} ${this.help}`,
67
+ `# TYPE ${this.name} gauge`
68
+ ];
69
+ for (const [key, val] of this._values) {
70
+ const labels = JSON.parse(key);
71
+ lines.push(`${this.name}${renderLabels(labels)} ${val}`);
72
+ }
73
+ return lines.join('\n');
74
+ }
75
+ }
76
+
77
+ class Histogram {
78
+ /**
79
+ * @param {string} name
80
+ * @param {string} help
81
+ * @param {number[]} buckets - upper bounds in ascending order
82
+ */
83
+ constructor(name, help, buckets = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]) {
84
+ this.name = name;
85
+ this.help = help;
86
+ this.buckets = [...buckets, Infinity];
87
+ this.labelNames = [];
88
+ this._data = new Map(); // labelKey → { counts[], sum, total }
89
+ }
90
+
91
+ _key(labels) { return JSON.stringify(labels ?? {}); }
92
+
93
+ observe(labels = {}, value) {
94
+ const k = this._key(labels);
95
+ let d = this._data.get(k);
96
+ if (!d) {
97
+ d = { counts: new Array(this.buckets.length).fill(0), sum: 0, total: 0 };
98
+ this._data.set(k, d);
99
+ }
100
+ for (let i = 0; i < this.buckets.length; i++) {
101
+ if (value <= this.buckets[i]) d.counts[i]++;
102
+ }
103
+ d.sum += value;
104
+ d.total += 1;
105
+ }
106
+
107
+ render() {
108
+ const lines = [
109
+ `# HELP ${this.name} ${this.help}`,
110
+ `# TYPE ${this.name} histogram`
111
+ ];
112
+ for (const [key, d] of this._data) {
113
+ const labels = JSON.parse(key);
114
+ const baseLabel = renderLabels(labels);
115
+ for (let i = 0; i < this.buckets.length; i++) {
116
+ const le = this.buckets[i] === Infinity ? '+Inf' : String(this.buckets[i]);
117
+ const bucketLabels = renderLabels({ ...labels, le });
118
+ lines.push(`${this.name}_bucket${bucketLabels} ${d.counts[i]}`);
119
+ }
120
+ lines.push(`${this.name}_sum${baseLabel} ${d.sum}`);
121
+ lines.push(`${this.name}_count${baseLabel} ${d.total}`);
122
+ }
123
+ return lines.join('\n');
124
+ }
125
+ }
126
+
127
+ // ─── Registry ─────────────────────────────────────────────────────────────────
128
+
129
+ export class PrometheusRegistry {
130
+ constructor() {
131
+ this._metrics = [];
132
+ }
133
+
134
+ counter(name, help, labelNames = []) {
135
+ const m = new Counter(name, help, labelNames);
136
+ this._metrics.push(m);
137
+ return m;
138
+ }
139
+
140
+ gauge(name, help, labelNames = []) {
141
+ const m = new Gauge(name, help, labelNames);
142
+ this._metrics.push(m);
143
+ return m;
144
+ }
145
+
146
+ histogram(name, help, buckets, labelNames = []) {
147
+ const m = new Histogram(name, help, buckets);
148
+ m.labelNames = labelNames;
149
+ this._metrics.push(m);
150
+ return m;
151
+ }
152
+
153
+ /**
154
+ * Render all metrics in Prometheus text format.
155
+ * @returns {string}
156
+ */
157
+ format() {
158
+ return this._metrics.map(m => m.render()).join('\n\n') + '\n';
159
+ }
160
+ }
161
+
162
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
163
+
164
+ function renderLabels(obj) {
165
+ const entries = Object.entries(obj);
166
+ if (entries.length === 0) return '';
167
+ const inner = entries.map(([k, v]) => `${k}="${String(v).replace(/"/g, '\\"')}"`).join(',');
168
+ return `{${inner}}`;
169
+ }