scientific-protocol 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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -0
  3. package/dist/generated/contracts.d.ts +31442 -0
  4. package/dist/generated/contracts.js +10623 -0
  5. package/dist/sdk/client.d.ts +376 -0
  6. package/dist/sdk/client.js +427 -0
  7. package/dist/sdk/index.d.ts +8 -0
  8. package/dist/sdk/index.js +7 -0
  9. package/dist/sdk/rewards.d.ts +42 -0
  10. package/dist/sdk/rewards.js +84 -0
  11. package/dist/sdk/types.d.ts +300 -0
  12. package/dist/sdk/types.js +1 -0
  13. package/dist/shared/agent-request-envelope.d.ts +40 -0
  14. package/dist/shared/agent-request-envelope.js +51 -0
  15. package/dist/shared/artifact-storage-attestations.d.ts +64 -0
  16. package/dist/shared/artifact-storage-attestations.js +104 -0
  17. package/dist/shared/artifact-storage-bundles.d.ts +44 -0
  18. package/dist/shared/artifact-storage-bundles.js +124 -0
  19. package/dist/shared/artifact-storage-policy.d.ts +38 -0
  20. package/dist/shared/artifact-storage-policy.js +47 -0
  21. package/dist/shared/contracts.d.ts +30 -0
  22. package/dist/shared/contracts.js +46 -0
  23. package/dist/shared/deployment.d.ts +52 -0
  24. package/dist/shared/deployment.js +156 -0
  25. package/dist/shared/secrets.d.ts +6 -0
  26. package/dist/shared/secrets.js +30 -0
  27. package/dist/shared/sha256.d.ts +2 -0
  28. package/dist/shared/sha256.js +4 -0
  29. package/package.json +103 -0
  30. package/schemas/artifact-storage-attestation.schema.json +116 -0
  31. package/schemas/artifact-storage-bundle.schema.json +107 -0
  32. package/schemas/claim.schema.json +83 -0
  33. package/schemas/evaluation.schema.json +96 -0
  34. package/schemas/replication.schema.json +103 -0
@@ -0,0 +1,124 @@
1
+ import { sha256Hex } from "./sha256.js";
2
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
3
+ function stableSerialize(value) {
4
+ if (Array.isArray(value)) {
5
+ return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
6
+ }
7
+ if (value && typeof value === "object") {
8
+ const entries = Object.entries(value)
9
+ .sort(([left], [right]) => left.localeCompare(right))
10
+ .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableSerialize(entryValue)}`);
11
+ return `{${entries.join(",")}}`;
12
+ }
13
+ return JSON.stringify(value) ?? "null";
14
+ }
15
+ function normalizeRequiredText(value, fieldName) {
16
+ const trimmed = value.trim();
17
+ if (!trimmed) {
18
+ throw new Error(`${fieldName} is required`);
19
+ }
20
+ return trimmed;
21
+ }
22
+ function normalizeOptionalText(value) {
23
+ const trimmed = value?.trim();
24
+ return trimmed ? trimmed : null;
25
+ }
26
+ function normalizeMemberPath(value) {
27
+ const normalized = normalizeRequiredText(value, "artifact bundle memberPath").replace(/\\/gu, "/");
28
+ const segments = normalized.split("/");
29
+ if (normalized.startsWith("/") ||
30
+ segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
31
+ throw new Error("artifact bundle memberPath must be relative and safe");
32
+ }
33
+ return normalized;
34
+ }
35
+ function normalizeByteLength(value, artifactKey) {
36
+ if (!Number.isSafeInteger(value) || value < 0) {
37
+ throw new Error(`artifact ${artifactKey} byteLength must be a non-negative safe integer`);
38
+ }
39
+ return value;
40
+ }
41
+ function normalizeSha256(value, artifactKey) {
42
+ const normalized = normalizeRequiredText(value, `artifact ${artifactKey} sha256`).toLowerCase();
43
+ if (!SHA256_PATTERN.test(normalized)) {
44
+ throw new Error(`artifact ${artifactKey} sha256 must be 64 lowercase hex characters`);
45
+ }
46
+ return normalized;
47
+ }
48
+ function normalizeArtifact(artifact) {
49
+ const artifactKey = normalizeRequiredText(artifact.artifactKey, "artifactKey");
50
+ return {
51
+ artifactKey,
52
+ byteLength: normalizeByteLength(artifact.byteLength, artifactKey),
53
+ cid: normalizeRequiredText(artifact.cid, `artifact ${artifactKey} cid`),
54
+ contentType: normalizeRequiredText(artifact.contentType, `artifact ${artifactKey} contentType`),
55
+ durabilityClass: artifact.durabilityClass,
56
+ memberPath: normalizeMemberPath(artifact.memberPath),
57
+ metadata: artifact.metadata ?? {},
58
+ sha256: normalizeSha256(artifact.sha256, artifactKey),
59
+ sourceUri: normalizeOptionalText(artifact.sourceUri),
60
+ };
61
+ }
62
+ export function buildArtifactStorageBundleManifest(input) {
63
+ const bundleKey = normalizeRequiredText(input.bundleKey, "bundleKey");
64
+ const storageRail = normalizeRequiredText(input.storageRail, "storageRail");
65
+ if (input.artifacts.length === 0) {
66
+ throw new Error("artifact storage bundle must include at least one artifact");
67
+ }
68
+ const artifacts = input.artifacts
69
+ .map(normalizeArtifact)
70
+ .sort((left, right) => left.artifactKey.localeCompare(right.artifactKey));
71
+ const duplicatePaths = new Set();
72
+ const duplicateKeys = new Set();
73
+ for (const artifact of artifacts) {
74
+ if (duplicatePaths.has(artifact.memberPath)) {
75
+ throw new Error(`artifact bundle memberPath duplicates ${artifact.memberPath}`);
76
+ }
77
+ if (duplicateKeys.has(artifact.artifactKey)) {
78
+ throw new Error(`artifact bundle artifactKey duplicates ${artifact.artifactKey}`);
79
+ }
80
+ duplicatePaths.add(artifact.memberPath);
81
+ duplicateKeys.add(artifact.artifactKey);
82
+ }
83
+ const baseManifest = {
84
+ artifacts,
85
+ bundleCid: normalizeOptionalText(input.bundleCid),
86
+ bundleKey,
87
+ bundleUri: normalizeOptionalText(input.bundleUri),
88
+ generatedAt: input.generatedAt ?? new Date().toISOString(),
89
+ kind: "scientific.artifact-storage-bundle",
90
+ metadata: input.metadata ?? {},
91
+ storageRail,
92
+ version: 1,
93
+ };
94
+ return {
95
+ ...baseManifest,
96
+ manifestDigest: `sha256:${sha256Hex(stableSerialize(baseManifest))}`,
97
+ };
98
+ }
99
+ export function verifyArtifactStorageBundleManifest(manifest) {
100
+ const { manifestDigest: declaredDigest, ...digestInput } = manifest;
101
+ const expectedDigest = `sha256:${sha256Hex(stableSerialize(digestInput))}`;
102
+ if (declaredDigest !== expectedDigest) {
103
+ throw new Error(`artifact storage bundle manifestDigest mismatch: expected ${expectedDigest}, received ${declaredDigest}`);
104
+ }
105
+ return expectedDigest;
106
+ }
107
+ export function createArtifactStorageBundlePolicyInputs(manifest) {
108
+ verifyArtifactStorageBundleManifest(manifest);
109
+ return manifest.artifacts.map((artifact) => ({
110
+ artifactKey: artifact.artifactKey,
111
+ policy: {
112
+ bundleCid: manifest.bundleCid,
113
+ bundleMemberPath: artifact.memberPath,
114
+ durabilityClass: artifact.durabilityClass,
115
+ metadata: {
116
+ artifactCid: artifact.cid,
117
+ bundleKey: manifest.bundleKey,
118
+ bundleUri: manifest.bundleUri,
119
+ manifestDigest: manifest.manifestDigest,
120
+ storageRail: manifest.storageRail,
121
+ },
122
+ },
123
+ }));
124
+ }
@@ -0,0 +1,38 @@
1
+ export type ArtifactDurabilityClass = "A" | "B" | "C" | "D";
2
+ export type ArtifactStorageCommitmentKind = "filecoin" | "hot" | "institutional" | "mirror" | "provider" | "temporary";
3
+ export type ArtifactStoragePolicyInput = {
4
+ bundleCid?: string | null;
5
+ bundleMemberPath?: string | null;
6
+ durabilityClass: ArtifactDurabilityClass;
7
+ metadata?: Record<string, unknown>;
8
+ repairPriority?: number;
9
+ requiredIndependentRetrievalPaths?: number;
10
+ requiredReplicaCount?: number;
11
+ requiresFilecoinOrEquivalent?: boolean;
12
+ retentionUntil?: string | null;
13
+ };
14
+ export type ArtifactStorageAttestationInput = {
15
+ attestorAddress: string;
16
+ cid: string;
17
+ commitmentKind: ArtifactStorageCommitmentKind;
18
+ evidenceRef?: string | null;
19
+ nodeId?: string | null;
20
+ provider: string;
21
+ providerMetadata?: Record<string, unknown>;
22
+ retentionUntil?: string | null;
23
+ retrievalUrl?: string | null;
24
+ signature: string;
25
+ signedPayloadHash: string;
26
+ storageClass: ArtifactDurabilityClass;
27
+ storageStartedAt?: string;
28
+ };
29
+ export type ArtifactStorageClassPolicy = {
30
+ durabilityClass: ArtifactDurabilityClass;
31
+ repairPriority: number;
32
+ requiredIndependentRetrievalPaths: number;
33
+ requiredReplicaCount: number;
34
+ requiresFilecoinOrEquivalent: boolean;
35
+ };
36
+ export declare const ARTIFACT_STORAGE_CLASS_POLICIES: Record<ArtifactDurabilityClass, ArtifactStorageClassPolicy>;
37
+ export declare function defaultArtifactStoragePolicy(durabilityClass: ArtifactDurabilityClass): ArtifactStorageClassPolicy;
38
+ export declare function resolveArtifactStoragePolicyInput(input: ArtifactStoragePolicyInput): Required<Pick<ArtifactStoragePolicyInput, "durabilityClass" | "metadata" | "repairPriority" | "requiredIndependentRetrievalPaths" | "requiredReplicaCount" | "requiresFilecoinOrEquivalent">> & Pick<ArtifactStoragePolicyInput, "bundleCid" | "bundleMemberPath" | "retentionUntil">;
@@ -0,0 +1,47 @@
1
+ export const ARTIFACT_STORAGE_CLASS_POLICIES = {
2
+ A: {
3
+ durabilityClass: "A",
4
+ repairPriority: 100,
5
+ requiredIndependentRetrievalPaths: 2,
6
+ requiredReplicaCount: 2,
7
+ requiresFilecoinOrEquivalent: true,
8
+ },
9
+ B: {
10
+ durabilityClass: "B",
11
+ repairPriority: 50,
12
+ requiredIndependentRetrievalPaths: 1,
13
+ requiredReplicaCount: 1,
14
+ requiresFilecoinOrEquivalent: true,
15
+ },
16
+ C: {
17
+ durabilityClass: "C",
18
+ repairPriority: 80,
19
+ requiredIndependentRetrievalPaths: 1,
20
+ requiredReplicaCount: 1,
21
+ requiresFilecoinOrEquivalent: true,
22
+ },
23
+ D: {
24
+ durabilityClass: "D",
25
+ repairPriority: 10,
26
+ requiredIndependentRetrievalPaths: 0,
27
+ requiredReplicaCount: 0,
28
+ requiresFilecoinOrEquivalent: false,
29
+ },
30
+ };
31
+ export function defaultArtifactStoragePolicy(durabilityClass) {
32
+ return ARTIFACT_STORAGE_CLASS_POLICIES[durabilityClass];
33
+ }
34
+ export function resolveArtifactStoragePolicyInput(input) {
35
+ const defaults = defaultArtifactStoragePolicy(input.durabilityClass);
36
+ return {
37
+ bundleCid: input.bundleCid ?? null,
38
+ bundleMemberPath: input.bundleMemberPath ?? null,
39
+ durabilityClass: input.durabilityClass,
40
+ metadata: input.metadata ?? {},
41
+ repairPriority: input.repairPriority ?? defaults.repairPriority,
42
+ requiredIndependentRetrievalPaths: input.requiredIndependentRetrievalPaths ?? defaults.requiredIndependentRetrievalPaths,
43
+ requiredReplicaCount: input.requiredReplicaCount ?? defaults.requiredReplicaCount,
44
+ requiresFilecoinOrEquivalent: input.requiresFilecoinOrEquivalent ?? defaults.requiresFilecoinOrEquivalent,
45
+ retentionUntil: input.retentionUntil ?? null,
46
+ };
47
+ }
@@ -0,0 +1,30 @@
1
+ import { Contract, type ContractRunner, type InterfaceAbi, JsonRpcProvider } from "ethers";
2
+ import { type EnvRecord } from "./secrets.js";
3
+ export type ContractName = "AccessController" | "AgentRegistry" | "AppealsRegistry" | "ArtifactRegistry" | "BondEscrow" | "ClaimRewardVault" | "ClaimRegistry" | "EpistemicMarket" | "ProtocolGovernanceToken" | "ProtocolGovernor" | "ProtocolParameters" | "ProtocolTimelock" | "ProtocolTreasury" | "ReplicationRegistry" | "ReputationCheckpointRegistry" | "ResolutionModuleRegistry";
4
+ export type Artifact = {
5
+ abi: InterfaceAbi;
6
+ bytecode: string;
7
+ contractName: string;
8
+ sourceName: string;
9
+ };
10
+ export declare function loadArtifact(name: ContractName): Promise<Artifact>;
11
+ export declare function getContract(name: ContractName, address: string, runner: ContractRunner): Promise<Contract & Record<string, any>>;
12
+ export declare function getRpcUrl(env?: EnvRecord): string;
13
+ export declare function getProvider(rpcUrl?: string): JsonRpcProvider;
14
+ export declare function extractContractEventId(contract: {
15
+ interface: {
16
+ parseLog(log: unknown): {
17
+ args: Record<string, {
18
+ toString(): string;
19
+ }>;
20
+ name?: string;
21
+ } | null;
22
+ };
23
+ }, receipt: {
24
+ logs: Array<unknown>;
25
+ }, eventName: string, argName: string): string | null;
26
+ export declare function requireContractEventId(contract: Parameters<typeof extractContractEventId>[0], receipt: ({
27
+ hash?: string;
28
+ } & {
29
+ logs: Array<unknown>;
30
+ }) | null, eventName: string, argName: string): string;
@@ -0,0 +1,46 @@
1
+ import { Contract, JsonRpcProvider } from "ethers";
2
+ import { generatedContractArtifacts } from "../generated/contracts.js";
3
+ import { readEnvValue } from "./secrets.js";
4
+ export async function loadArtifact(name) {
5
+ const artifact = generatedContractArtifacts[name];
6
+ return {
7
+ abi: artifact.abi,
8
+ bytecode: artifact.bytecode,
9
+ contractName: artifact.contractName,
10
+ sourceName: artifact.sourceName,
11
+ };
12
+ }
13
+ export async function getContract(name, address, runner) {
14
+ const artifact = await loadArtifact(name);
15
+ return new Contract(address, artifact.abi, runner);
16
+ }
17
+ export function getRpcUrl(env = process.env) {
18
+ return readEnvValue(env, "SP_RPC_URL") ?? "http://127.0.0.1:8545";
19
+ }
20
+ export function getProvider(rpcUrl = getRpcUrl()) {
21
+ return new JsonRpcProvider(rpcUrl);
22
+ }
23
+ export function extractContractEventId(contract, receipt, eventName, argName) {
24
+ for (const log of receipt.logs) {
25
+ try {
26
+ const parsed = contract.interface.parseLog(log);
27
+ if (parsed?.name === eventName) {
28
+ return parsed.args[argName].toString();
29
+ }
30
+ }
31
+ catch {
32
+ // Ignore unrelated logs.
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ export function requireContractEventId(contract, receipt, eventName, argName) {
38
+ if (!receipt) {
39
+ throw new Error(`missing transaction receipt for ${eventName}.${argName}`);
40
+ }
41
+ const value = extractContractEventId(contract, receipt, eventName, argName);
42
+ if (!value) {
43
+ throw new Error(`missing ${eventName}.${argName} in transaction ${receipt.hash ?? "unknown"}`);
44
+ }
45
+ return value;
46
+ }
@@ -0,0 +1,52 @@
1
+ import { type EnvRecord } from "./secrets.js";
2
+ export type DeploymentAddresses = {
3
+ accessController: string;
4
+ protocolParameters: string;
5
+ protocolGovernanceToken: string;
6
+ protocolTimelock: string;
7
+ protocolGovernor: string;
8
+ protocolTreasury: string;
9
+ resolutionModuleRegistry: string;
10
+ claimRegistry: string;
11
+ artifactRegistry: string;
12
+ bondEscrow: string;
13
+ claimRewardVault: string;
14
+ agentRegistry: string;
15
+ replicationRegistry: string;
16
+ reputationCheckpointRegistry: string;
17
+ epistemicMarket: string;
18
+ appealsRegistry: string;
19
+ computationalModule: string;
20
+ benchmarkModule: string;
21
+ wetLabModule: string;
22
+ };
23
+ export type DeploymentFile = {
24
+ network: string;
25
+ chainId: number;
26
+ deploymentBlock: number;
27
+ deployedAt: string;
28
+ addresses: DeploymentAddresses;
29
+ };
30
+ export declare const DEFAULT_DEPLOYMENT_PATH: string;
31
+ export declare function getDeploymentPath(env?: EnvRecord): string;
32
+ type DeploymentFileOptions = {
33
+ env?: EnvRecord;
34
+ gcsClient?: GcsLikeClient;
35
+ };
36
+ type GcsObjectLocator = {
37
+ bucket: string;
38
+ key: string;
39
+ };
40
+ type GcsLikeClient = {
41
+ saveObject(input: GcsObjectLocator & {
42
+ body?: string | Uint8Array;
43
+ contentType: string;
44
+ metadata?: Record<string, string>;
45
+ }): Promise<void>;
46
+ readObject(input: GcsObjectLocator): Promise<string | Uint8Array>;
47
+ objectExists(input: GcsObjectLocator): Promise<boolean>;
48
+ };
49
+ export declare function deploymentFileExists(filePath?: string, options?: DeploymentFileOptions): Promise<boolean>;
50
+ export declare function loadDeploymentFile(filePath?: string, options?: DeploymentFileOptions): Promise<DeploymentFile>;
51
+ export declare function saveDeploymentFile(deployment: DeploymentFile, filePath?: string, options?: DeploymentFileOptions): Promise<void>;
52
+ export {};
@@ -0,0 +1,156 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { isAddress } from "ethers";
4
+ import { readEnvValue } from "./secrets.js";
5
+ export const DEFAULT_DEPLOYMENT_PATH = path.resolve(process.cwd(), "ops", "local.addresses.json");
6
+ export function getDeploymentPath(env = process.env) {
7
+ return readEnvValue(env, "SP_DEPLOYMENT_PATH") ?? DEFAULT_DEPLOYMENT_PATH;
8
+ }
9
+ function isGcsUrl(value) {
10
+ return value.startsWith("gs://");
11
+ }
12
+ function parseGcsUrl(value) {
13
+ const match = value.match(/^gs:\/\/([^/]+)\/(.+)$/u);
14
+ if (!match) {
15
+ throw new Error(`invalid gcs path: ${value}`);
16
+ }
17
+ return { bucket: match[1], key: match[2] };
18
+ }
19
+ async function resolveDeploymentGcsClient(options) {
20
+ if (options.gcsClient) {
21
+ return options.gcsClient;
22
+ }
23
+ throw new Error("gs:// deployment paths require DeploymentFileOptions.gcsClient");
24
+ }
25
+ const DEPLOYMENT_ADDRESS_KEYS = [
26
+ "accessController",
27
+ "agentRegistry",
28
+ "appealsRegistry",
29
+ "artifactRegistry",
30
+ "benchmarkModule",
31
+ "bondEscrow",
32
+ "claimRegistry",
33
+ "claimRewardVault",
34
+ "computationalModule",
35
+ "epistemicMarket",
36
+ "protocolGovernor",
37
+ "protocolGovernanceToken",
38
+ "protocolParameters",
39
+ "protocolTimelock",
40
+ "protocolTreasury",
41
+ "replicationRegistry",
42
+ "reputationCheckpointRegistry",
43
+ "resolutionModuleRegistry",
44
+ "wetLabModule",
45
+ ];
46
+ function recordLike(value) {
47
+ return value && typeof value === "object" && !Array.isArray(value)
48
+ ? value
49
+ : null;
50
+ }
51
+ function requireString(record, key, source) {
52
+ const value = record[key];
53
+ if (typeof value !== "string" || value.trim().length === 0) {
54
+ throw new Error(`deployment file from ${source} is missing ${key}`);
55
+ }
56
+ return value;
57
+ }
58
+ function requireNonNegativeInteger(record, key, source) {
59
+ const value = record[key];
60
+ if (!Number.isInteger(value) || value < 0) {
61
+ throw new Error(`deployment file from ${source} has invalid ${key}`);
62
+ }
63
+ return value;
64
+ }
65
+ function requireTimestamp(record, key, source) {
66
+ const value = requireString(record, key, source);
67
+ if (Number.isNaN(Date.parse(value))) {
68
+ throw new Error(`deployment file from ${source} has invalid ${key}`);
69
+ }
70
+ return value;
71
+ }
72
+ function validateDeploymentFile(value, source) {
73
+ const record = recordLike(value);
74
+ if (!record) {
75
+ throw new Error(`deployment file from ${source} must be an object`);
76
+ }
77
+ const addresses = recordLike(record.addresses);
78
+ if (!addresses) {
79
+ throw new Error(`deployment file from ${source} is missing addresses`);
80
+ }
81
+ for (const key of DEPLOYMENT_ADDRESS_KEYS) {
82
+ const address = requireString(addresses, key, source);
83
+ if (!isAddress(address)) {
84
+ throw new Error(`deployment file from ${source} has invalid address for ${key}`);
85
+ }
86
+ }
87
+ return {
88
+ addresses: Object.fromEntries(DEPLOYMENT_ADDRESS_KEYS.map((key) => [key, addresses[key]])),
89
+ chainId: requireNonNegativeInteger(record, "chainId", source),
90
+ deployedAt: requireTimestamp(record, "deployedAt", source),
91
+ deploymentBlock: requireNonNegativeInteger(record, "deploymentBlock", source),
92
+ network: requireString(record, "network", source),
93
+ };
94
+ }
95
+ function parseDeploymentJson(raw, source) {
96
+ try {
97
+ return validateDeploymentFile(JSON.parse(raw), source);
98
+ }
99
+ catch (error) {
100
+ throw new Error(`failed to parse deployment file from ${source}: ${error instanceof Error ? error.message : String(error)}`);
101
+ }
102
+ }
103
+ export async function deploymentFileExists(filePath = DEFAULT_DEPLOYMENT_PATH, options = {}) {
104
+ const env = options.env ?? process.env;
105
+ const inline = readEnvValue(env, "SP_DEPLOYMENT_JSON");
106
+ if (inline) {
107
+ return true;
108
+ }
109
+ if (isGcsUrl(filePath)) {
110
+ const gcsClient = await resolveDeploymentGcsClient(options);
111
+ const locator = parseGcsUrl(filePath);
112
+ return gcsClient.objectExists(locator);
113
+ }
114
+ try {
115
+ await access(filePath);
116
+ return true;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
122
+ export async function loadDeploymentFile(filePath = DEFAULT_DEPLOYMENT_PATH, options = {}) {
123
+ const env = options.env ?? process.env;
124
+ const inline = readEnvValue(env, "SP_DEPLOYMENT_JSON");
125
+ if (inline) {
126
+ return parseDeploymentJson(inline, "SP_DEPLOYMENT_JSON");
127
+ }
128
+ if (isGcsUrl(filePath)) {
129
+ const gcsClient = await resolveDeploymentGcsClient(options);
130
+ const locator = parseGcsUrl(filePath);
131
+ const object = await gcsClient.readObject(locator);
132
+ const content = typeof object === "string" ? object : new TextDecoder().decode(object);
133
+ return parseDeploymentJson(content, filePath);
134
+ }
135
+ return parseDeploymentJson(await readFile(filePath, "utf8"), filePath);
136
+ }
137
+ export async function saveDeploymentFile(deployment, filePath = DEFAULT_DEPLOYMENT_PATH, options = {}) {
138
+ const serialized = `${JSON.stringify(deployment, null, 2)}\n`;
139
+ if (isGcsUrl(filePath)) {
140
+ const gcsClient = await resolveDeploymentGcsClient(options);
141
+ const locator = parseGcsUrl(filePath);
142
+ await gcsClient.saveObject({
143
+ ...locator,
144
+ body: serialized,
145
+ contentType: "application/json",
146
+ metadata: {
147
+ chainId: String(deployment.chainId),
148
+ deploymentBlock: String(deployment.deploymentBlock),
149
+ network: deployment.network,
150
+ },
151
+ });
152
+ return;
153
+ }
154
+ await mkdir(path.dirname(filePath), { recursive: true });
155
+ await writeFile(filePath, serialized);
156
+ }
@@ -0,0 +1,6 @@
1
+ export type EnvRecord = Record<string, string | undefined>;
2
+ export declare function readEnvValue(env: EnvRecord, key: string, options?: {
3
+ trim?: boolean;
4
+ }): string | undefined;
5
+ export declare function hasConfiguredEnvValue(env: EnvRecord, key: string): boolean;
6
+ export declare function hasSecretRef(env: EnvRecord, key: string): boolean;
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from "node:fs";
2
+ function normalizeValue(value, trim) {
3
+ return trim ? value.trim() : value;
4
+ }
5
+ export function readEnvValue(env, key, options = {}) {
6
+ const trim = options.trim !== false;
7
+ const directValue = env[key];
8
+ if (typeof directValue === "string" && directValue.trim() !== "") {
9
+ return normalizeValue(directValue, trim);
10
+ }
11
+ const filePath = env[`${key}_FILE`]?.trim();
12
+ if (!filePath) {
13
+ return undefined;
14
+ }
15
+ try {
16
+ const fileValue = readFileSync(filePath, "utf8");
17
+ return normalizeValue(fileValue, trim);
18
+ }
19
+ catch (error) {
20
+ const detail = error instanceof Error ? error.message : String(error);
21
+ throw new Error(`failed to read ${key}_FILE (${filePath}): ${detail}`);
22
+ }
23
+ }
24
+ export function hasConfiguredEnvValue(env, key) {
25
+ return Boolean(readEnvValue(env, key) || hasSecretRef(env, key));
26
+ }
27
+ export function hasSecretRef(env, key) {
28
+ const secretRef = env[`${key}_SECRET_REF`];
29
+ return typeof secretRef === "string" && secretRef.trim() !== "";
30
+ }
@@ -0,0 +1,2 @@
1
+ export type HashableContent = string | Uint8Array;
2
+ export declare function sha256Hex(content: HashableContent): string;
@@ -0,0 +1,4 @@
1
+ import { createHash } from "node:crypto";
2
+ export function sha256Hex(content) {
3
+ return createHash("sha256").update(content).digest("hex");
4
+ }
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "scientific-protocol",
3
+ "version": "0.1.0",
4
+ "description": "Claim-centric protocol for scientific publication, replication, review, and reward.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/emgun/scientific-protocol.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/emgun/scientific-protocol/issues"
12
+ },
13
+ "homepage": "https://github.com/emgun/scientific-protocol#readme",
14
+ "keywords": [
15
+ "science",
16
+ "ethereum",
17
+ "protocol",
18
+ "replication",
19
+ "reputation",
20
+ "solidity"
21
+ ],
22
+ "main": "./dist/sdk/index.js",
23
+ "types": "./dist/sdk/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/sdk/index.d.ts",
27
+ "import": "./dist/sdk/index.js"
28
+ },
29
+ "./contracts": {
30
+ "types": "./dist/generated/contracts.d.ts",
31
+ "import": "./dist/generated/contracts.js"
32
+ },
33
+ "./schemas/claim": "./schemas/claim.schema.json",
34
+ "./schemas/replication": "./schemas/replication.schema.json",
35
+ "./schemas/evaluation": "./schemas/evaluation.schema.json",
36
+ "./schemas/artifact-storage-attestation": "./schemas/artifact-storage-attestation.schema.json",
37
+ "./schemas/artifact-storage-bundle": "./schemas/artifact-storage-bundle.schema.json"
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "schemas",
42
+ "README.md"
43
+ ],
44
+ "type": "module",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "engines": {
49
+ "node": ">=22 <23"
50
+ },
51
+ "scripts": {
52
+ "build": "bash script/run-with-node22-env.sh hardhat clean && bash script/run-with-node22-env.sh hardhat compile && npm run generate:contracts",
53
+ "build:package": "npm run build && bash script/run-with-node22-env.sh tsc -p tsconfig.package.json",
54
+ "prepack": "npm run build:package",
55
+ "lint": "npm run lint:biome && npm run lint:prettier && npm run lint:shell",
56
+ "lint:biome": "bash script/run-with-node22-env.sh npx biome check --diagnostic-level=error --files-ignore-unknown=true src test script hardhat.config.ts package.json tsconfig.json biome.json .prettierrc.json",
57
+ "lint:prettier": "bash script/run-with-node22-env.sh prettier --check .",
58
+ "lint:shell": "bash -c 'for file in script/*.sh; do bash -n \"$file\"; done'",
59
+ "format": "npm run format:biome && npm run format:prettier",
60
+ "format:biome": "bash script/run-with-node22-env.sh npx biome check --write --files-ignore-unknown=true src test script hardhat.config.ts package.json tsconfig.json biome.json .prettierrc.json",
61
+ "format:prettier": "bash script/run-with-node22-env.sh prettier --write .",
62
+ "generate:contracts": "bash script/run-with-node22-env.sh node --import tsx script/generate-contract-artifacts.ts && bash script/run-with-node22-env.sh npx biome format --write src/generated/contracts.ts",
63
+ "typecheck": "bash script/run-with-node22-env.sh npx tsc --noEmit",
64
+ "test": "bash script/run-with-node22-env.sh hardhat test",
65
+ "test:forge": "forge test",
66
+ "test:all": "npm run test:forge && HOME=$PWD npm test",
67
+ "validate:env": "bash script/run-with-node22-env.sh node --import tsx script/validate-env.ts",
68
+ "node": "bash script/run-with-node22-env.sh hardhat node",
69
+ "node:env:init": "bash script/init-node22-env.sh",
70
+ "deploy": "bash script/run-with-node22-env.sh hardhat run --network localhost script/deploy-protocol.ts",
71
+ "gas:snapshot": "forge snapshot --snap .gas-snapshot",
72
+ "gas:check": "forge snapshot --check .gas-snapshot"
73
+ },
74
+ "devDependencies": {
75
+ "@aws-sdk/client-s3": "^3.1011.0",
76
+ "@biomejs/biome": "^2.4.9",
77
+ "@google-cloud/storage": "^7.19.0",
78
+ "@nomicfoundation/hardhat-ethers": "^4.0.0",
79
+ "@nomicfoundation/hardhat-node-test-runner": "^3.0.11",
80
+ "@openzeppelin/contracts": "^4.9.6",
81
+ "@types/pg": "^8.11.10",
82
+ "chai": "^5.1.1",
83
+ "hardhat": "^3.1.12",
84
+ "pdf-parse": "^2.4.5",
85
+ "pg": "^8.13.1",
86
+ "prettier": "^3.8.1",
87
+ "prettier-plugin-solidity": "^2.3.1",
88
+ "ts-node": "^10.9.2",
89
+ "tsx": "^4.19.4",
90
+ "typescript": "^5.5.4"
91
+ },
92
+ "overrides": {
93
+ "@tootallnate/once": "3.0.1",
94
+ "@actions/http-client": "2.2.3",
95
+ "@aws-sdk/xml-builder": "3.972.22",
96
+ "fast-xml-parser": "5.8.0",
97
+ "undici": "^6.24.0",
98
+ "ws": "8.21.0"
99
+ },
100
+ "dependencies": {
101
+ "ethers": "^6.13.2"
102
+ }
103
+ }