From 8eec02946349445907d1dbb93843d9eccc67a655 Mon Sep 17 00:00:00 2001
From: 0xf965 <40121100+0xf965@users.noreply.github.com>
Date: Tue, 31 Mar 2026 21:19:42 +0200
Subject: [PATCH 01/23] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index db50db5..81d072b 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Represents a specific location (URL) where a file with a specific hash can be fo
* **R4**: `FILE_SOURCE`
* **R5**: `raw_file_hash` (Hash function digest). **This is the anchor.** Users search by this hash.
* **R6**: `false` (Unlocked). If the link dies, the owner spends this box and outputs a new one with the *same* R5 (hash) but updated R9 (URL).
-* **R9**: `Coll[Coll[Byte]]` — A single source entry (serialized as a one-element array), containing:
+* **R9**: `Coll[Byte]` JSON utf-8 tuple (ergoscript doesn't support this structure) — A single source entry (serialized as a one-element array), containing:
* `hash_function_id` — The ID of the hash function, determined by `HASH(EMPTY_INPUT)` ([spec](https://github.com/celaut-project/docs/blob/master/FAQ.md#hash-algorithm-identification))
* `url_link` — Link to a GET resource with the content file
* `content_format` — The file format extension of the content (e.g. `.tar.gz`, `.zip`)
From 715b76914e944daefde0b1b5cb591911c49e644f Mon Sep 17 00:00:00 2001
From: Captain Efficiency
Date: Wed, 1 Apr 2026 06:10:26 -0400
Subject: [PATCH 02/23] fix: calculateHashFromUrl uses selected algorithm +
chunked support
- Replaced hardcoded blake2b256 with downloadAndHash() from hashUtils
- calculateHashFromUrl now uses the selected hash algorithm (sha3_256, blake2b, sha256, keccak256)
- Chunked files: downloadAndHash fetches all chunks from manifest, concatenates, then hashes the complete content
- handleAddSource validation also uses correct algorithm + chunked support
- handleFileUpload uses computeHash with selected algorithm
- Removed unused blake2b256 and uint8ArrayToHex imports
- Custom algorithm shows 'select a known hash algorithm first' error
---
src/lib/components/FileSourceCreation.svelte | 46 ++++++++++++--------
1 file changed, 28 insertions(+), 18 deletions(-)
diff --git a/src/lib/components/FileSourceCreation.svelte b/src/lib/components/FileSourceCreation.svelte
index e0f7b14..a00fa72 100644
--- a/src/lib/components/FileSourceCreation.svelte
+++ b/src/lib/components/FileSourceCreation.svelte
@@ -9,8 +9,8 @@
import { Textarea } from "$lib/components/ui/textarea";
import { AlertTriangle, Download, Upload, Loader2 } from "lucide-svelte";
import { type ReputationProof } from "$lib/ergo/object";
- import { blake2b256 } from "@fleet-sdk/crypto";
- import { uint8ArrayToHex } from "$lib/ergo/utils";
+ import { downloadAndHash } from "$lib/ergo/hashUtils";
+
import { type Writable } from "svelte/store";
import { HASH_OPTIONS, validateHash } from "$lib/ergo/hashUtils";
@@ -175,16 +175,19 @@
const url = entryUrlLink.trim();
if (!url) return;
+ const algorithmId = entryHashFunctionId || hashFunctionId;
+ if (!algorithmId || algorithmId === '__custom__') {
+ hashError = "Cannot verify: select a known hash algorithm first";
+ return;
+ }
+
isCalculatingHash = true;
hashError = null;
urlMismatch = false;
try {
- const response = await fetch(url);
- if (!response.ok)
- throw new Error(`Failed to fetch file: ${response.statusText}`);
- const buffer = await response.arrayBuffer();
- const bytes = new Uint8Array(buffer);
- const hashResult = uint8ArrayToHex(blake2b256(bytes));
+ // downloadAndHash handles both chunked (manifest) and regular URLs
+ // and uses the correct hash algorithm
+ const hashResult = await downloadAndHash(url, algorithmId, isChunked);
if (isHashFixed && hashResult !== currentHashValue) {
urlMismatch = true;
@@ -204,14 +207,22 @@
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
+ const algorithmId = entryHashFunctionId || hashFunctionId;
+ if (!algorithmId || algorithmId === '__custom__') {
+ hashError = "Cannot verify: select a known hash algorithm first";
+ return;
+ }
+
const file = input.files[0];
isCalculatingHash = true;
hashError = null;
try {
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
- const hashResult = blake2b256(bytes);
- updateHash(uint8ArrayToHex(hashResult));
+ const { computeHash } = await import("$lib/ergo/hashUtils");
+ const hashResult = computeHash(bytes, algorithmId);
+ if (!hashResult) throw new Error(`Unsupported hash algorithm: ${algorithmId}`);
+ updateHash(hashResult);
} catch (err: any) {
console.error("Error calculating hash from file:", err);
hashError = err?.message || "Failed to calculate hash from file";
@@ -232,17 +243,16 @@
// If it's fixed, we MUST validate the URL content before adding
if (isHashFixed && entryUrlLink.trim()) {
+ const algorithmId = entryHashFunctionId || hashFunctionId;
+ if (!algorithmId || algorithmId === '__custom__') {
+ hashError = "Cannot verify: select a known hash algorithm first";
+ return;
+ }
isCalculatingHash = true;
hashError = null;
try {
- const response = await fetch(entryUrlLink.trim());
- if (!response.ok)
- throw new Error(
- `Failed to fetch file: ${response.statusText}`,
- );
- const buffer = await response.arrayBuffer();
- const bytes = new Uint8Array(buffer);
- const hashResult = uint8ArrayToHex(blake2b256(bytes));
+ // Use downloadAndHash with correct algorithm + chunked support
+ const hashResult = await downloadAndHash(entryUrlLink.trim(), algorithmId, isChunked);
if (hashResult !== currentHashValue) {
urlMismatch = true;
From 254b8559b9db39e635a78a217ef773292eaa70f2 Mon Sep 17 00:00:00 2001
From: Captain Efficiency
Date: Fri, 3 Apr 2026 07:09:31 -0400
Subject: [PATCH 03/23] feat: hash validation toggle in settings + fallback
profile detection
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. Hash Validation Toggle:
- Added hashValidationEnabled store (persisted, default false)
- Added checkbox toggle in SettingsModal under 'Verification' section
- FileSourceCreation now respects the setting — skips URL hash
verification when disabled
- Exported hashValidationEnabled from index.ts
2. Profile Detection Fallback:
- Added fallbackProfileDetection() for wallets where the
reputation-system library fails to detect profiles due to
R5 UTF-8 encoding edge case (R5 stores hex(utf8(tokenId))
instead of raw bytes)
- Queries Explorer API directly with ergoTreeTemplateHash + R4/R7
register filters, then checks both raw hex and UTF-8 decoded R5
- Falls back automatically when standard detection returns nothing
3. Fixed createProfileBox parameter order bug in sourceStore.ts
(explorerUri was in wrong position)
---
src/lib/components/FileSourceCreation.svelte | 7 +-
src/lib/components/SettingsModal.svelte | 26 +++
src/lib/ergo/envs.ts | 3 +
src/lib/ergo/sourceStore.ts | 2 +-
src/lib/ergo/store.ts | 11 +-
src/lib/index.ts | 5 +
src/routes/App.svelte | 214 ++++++++++++++++++-
7 files changed, 260 insertions(+), 8 deletions(-)
diff --git a/src/lib/components/FileSourceCreation.svelte b/src/lib/components/FileSourceCreation.svelte
index a00fa72..7e6942a 100644
--- a/src/lib/components/FileSourceCreation.svelte
+++ b/src/lib/components/FileSourceCreation.svelte
@@ -21,6 +21,9 @@
export let onSourceAdded: ((txId: string) => void) | null = null;
export let hash: Writable | undefined = undefined;
+ /** When false, skip automatic hash verification when adding a source. */
+ export let hashValidationEnabled: boolean = false;
+
export let title: string = "Add New File Source";
let className: string = "";
export { className as class };
@@ -241,8 +244,8 @@
newFileHash = currentHashValue;
}
- // If it's fixed, we MUST validate the URL content before adding
- if (isHashFixed && entryUrlLink.trim()) {
+ // If hash validation is enabled and hash is fixed, validate the URL content before adding
+ if (hashValidationEnabled && isHashFixed && entryUrlLink.trim()) {
const algorithmId = entryHashFunctionId || hashFunctionId;
if (!algorithmId || algorithmId === '__custom__') {
hashError = "Cannot verify: select a known hash algorithm first";
diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte
index ebb7bd4..b7302a7 100644
--- a/src/lib/components/SettingsModal.svelte
+++ b/src/lib/components/SettingsModal.svelte
@@ -9,11 +9,13 @@
export let webTx: string;
export let webAddr: string;
export let webTkn: string;
+ export let hashValidation: boolean = false;
export let onSave: (settings: {
explorerUri: string;
webTx: string;
webAddr: string;
webTkn: string;
+ hashValidation: boolean;
}) => void;
// Valores por defecto definidos en constantes para fácil mantenimiento
@@ -28,6 +30,7 @@
let localWebTx = webTx;
let localWebAddr = webAddr;
let localWebTkn = webTkn;
+ let localHashValidation = hashValidation;
// Sync local state when props change (e.g. when modal opens)
$: if (show) {
@@ -35,6 +38,7 @@
localWebTx = webTx;
localWebAddr = webAddr;
localWebTkn = webTkn;
+ localHashValidation = hashValidation;
}
function close() {
@@ -47,6 +51,7 @@
webTx: localWebTx,
webAddr: localWebAddr,
webTkn: localWebTkn,
+ hashValidation: localHashValidation,
});
close();
}
@@ -57,6 +62,7 @@
localWebTx = DEFAULTS.tx;
localWebAddr = DEFAULTS.addr;
localWebTkn = DEFAULTS.tkn;
+ localHashValidation = false;
}
@@ -122,6 +128,26 @@
URL prefix for viewing tokens.
+
+
+
Verification
+
+
+
+
+
+ When enabled, adding a source will download the file from the URL
+ and verify its hash matches before submitting the transaction.
+ This may cause issues with large files or CORS-restricted URLs.
+
+
+
+
{
const profileTxId = await create_profile(
+ explorerUri,
PROFILE_TOTAL_SUPPLY,
PROFILE_TYPE_NFT_ID,
- explorerUri,
{ name: "Anon" }
);
diff --git a/src/lib/ergo/store.ts b/src/lib/ergo/store.ts
index e2d8cb3..0920621 100644
--- a/src/lib/ergo/store.ts
+++ b/src/lib/ergo/store.ts
@@ -99,4 +99,13 @@ export const profileInvalidations = createPersistentStore>('source_profile_unavailabilities', {});
export const profileOpinionsGiven = createPersistentStore>('source_profile_opinions_given', {});
export const isLoading = writable(false);
-export const error = writable(null);
\ No newline at end of file
+export const error = writable(null);
+
+// --- SETTINGS ---
+
+/**
+ * When enabled, adding a source will download the file from the URL and verify
+ * its hash matches before submitting the transaction. Disabled by default to
+ * avoid large downloads and CORS issues in the browser.
+ */
+export const hashValidationEnabled = createPersistentStore('hash_validation_enabled', false);
\ No newline at end of file
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 422c8c7..5123193 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -70,6 +70,11 @@ export {
PROFILE_OPINION_TYPE_NFT_ID,
} from './ergo/envs';
+// ===== SETTINGS STORES =====
+export {
+ hashValidationEnabled
+} from './ergo/store';
+
// ===== SVELTE COMPONENTS =====
export { default as ProfileCard } from './components/ProfileCard.svelte';
export { default as FileSourceCreation } from './components/FileSourceCreation.svelte';
diff --git a/src/routes/App.svelte b/src/routes/App.svelte
index 532ee5b..ddeaf48 100644
--- a/src/routes/App.svelte
+++ b/src/routes/App.svelte
@@ -24,13 +24,15 @@
web_explorer_uri_tx,
web_explorer_uri_addr,
web_explorer_uri_tkn,
+ hashValidationEnabled,
} from "$lib/ergo/store";
- import { PROFILE_TYPE_NFT_ID } from "$lib/ergo/envs";
+ import { PROFILE_TYPE_NFT_ID, ERGO_TREE_HASH } from "$lib/ergo/envs";
import { User, Settings, Search, Plus, UserPlus } from "lucide-svelte";
import { get, writable } from "svelte/store";
import SettingsModal from "$lib/components/SettingsModal.svelte";
import ProfileModal from "$lib/components/ProfileModal.svelte";
- import { fetchAllUserProfiles, fetchTypeNfts } from "reputation-system";
+ import { fetchAllUserProfiles, fetchTypeNfts, convertToRPBox } from "reputation-system";
+ import type { TypeNFT, ApiBox } from "reputation-system";
import { createProfileBox } from "$lib/ergo/sourceStore";
import { searchByHash, loadProfileData } from "$lib/ergo/sourceFetch";
import ProfileSources from "$lib/components/ProfileSources.svelte";
@@ -154,18 +156,218 @@
}
}
+ /**
+ * Decode a hex string to its UTF-8 text representation.
+ */
+ function hexToUtf8(hex: string): string | null {
+ try {
+ if (hex.length % 2 !== 0) return null;
+ const bytes = new Uint8Array(hex.match(/.{1,2}/g)!.map(b => parseInt(b, 16)));
+ return new TextDecoder('utf-8').decode(bytes);
+ } catch { return null; }
+ }
+
+ /**
+ * Fallback profile detection that handles the R5 encoding issue.
+ *
+ * The reputation-system library compares parseCollByteToHex(R5.renderedValue)
+ * with the token ID for `is_self_defined` detection. However, profile boxes
+ * store R5 as the UTF-8 encoding of the token ID hex string (64 ASCII bytes)
+ * rather than the raw 32 bytes. This causes `parseCollByteToHex` to return
+ * the hex of the ASCII representation, which doesn't match the token ID.
+ *
+ * This fallback directly queries the explorer API for boxes matching the
+ * user's ErgoTree (R7), then checks both the raw hex and the UTF-8 decoded
+ * form of R5 to find self-referencing profile boxes.
+ */
+ async function fallbackProfileDetection(
+ explorerUri: string,
+ availableTypes: Map
+ ): Promise {
+ try {
+ if (typeof ergo === 'undefined') return null;
+
+ const { ErgoAddress, SColl, SByte } = await import('@fleet-sdk/core');
+ const changeAddress = await ergo.get_change_address();
+ if (!changeAddress) return null;
+
+ const userAddress = ErgoAddress.fromBase58(changeAddress);
+
+ // Build serialized R7 and strip type prefix for rendered form
+ function hexToBytes(h: string): Uint8Array | null {
+ if (!h || !/^[0-9a-fA-F]*$/.test(h) || h.length % 2 !== 0) return null;
+ const arr = new Uint8Array(h.length / 2);
+ for (let i = 0; i < arr.length; i++) arr[i] = parseInt(h.substring(i*2, i*2+2), 16);
+ return arr;
+ }
+
+ const r7Serialized = SColl(SByte, userAddress.ergoTree).toHex();
+ // Strip Coll[Byte] prefix (0e + length byte(s))
+ const r7Rendered = r7Serialized.startsWith('0e') ? r7Serialized.substring(4) : r7Serialized;
+
+ // Also compute the R4 rendered value for PROFILE_TYPE_NFT_ID
+ const profileTypeBytes = hexToBytes(PROFILE_TYPE_NFT_ID);
+ if (!profileTypeBytes) return null;
+
+ // Query the Explorer API for boxes matching our ergo tree, R7, and R4
+ const ergo_tree_hash = ERGO_TREE_HASH;
+
+ const allBoxes: ApiBox[] = [];
+ let offset = 0;
+ const limit = 100;
+ let more = true;
+
+ while (more) {
+ const url = `${explorerUri}/api/v1/boxes/unspent/search?offset=${offset}&limit=${limit}`;
+ const body = {
+ ergoTreeTemplateHash: ergo_tree_hash,
+ registers: {
+ R4: PROFILE_TYPE_NFT_ID,
+ R7: r7Rendered,
+ },
+ assets: [],
+ };
+
+ const resp = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+
+ if (!resp.ok) { more = false; continue; }
+ const data = await resp.json();
+ if (!data.items || data.items.length === 0) { more = false; continue; }
+ allBoxes.push(...data.items);
+ offset += limit;
+ if (data.items.length < limit) more = false;
+ }
+
+ if (allBoxes.length === 0) return null;
+
+ // Find profile (self-referencing) boxes
+ // Check both raw hex and UTF-8 decoded forms of R5
+ const profileBoxes = allBoxes.filter(box => {
+ if (!box.assets?.length) return false;
+ if (!box.additionalRegisters?.R5?.renderedValue) return false;
+ if (!box.additionalRegisters?.R6) return false;
+
+ // R6 must be false (unlocked)
+ const r6 = box.additionalRegisters.R6.renderedValue;
+ if (r6 !== 'false' && r6 !== false) return false;
+
+ const tokenId = box.assets[0].tokenId;
+ const r5Rendered = box.additionalRegisters.R5.renderedValue as string;
+
+ // Direct match: R5 rendered value equals token ID
+ if (r5Rendered === tokenId) return true;
+
+ // UTF-8 decode: R5 might be hex(utf8(tokenId))
+ const decoded = hexToUtf8(r5Rendered);
+ if (decoded === tokenId) return true;
+
+ return false;
+ });
+
+ if (profileBoxes.length === 0) return null;
+
+ // Group by token ID and build a ReputationProof for the first profile
+ const tokenId = profileBoxes[0].assets[0].tokenId;
+
+ // Fetch emission amount for this token
+ let totalAmount = 0;
+ try {
+ const tokenResp = await fetch(`${explorerUri}/api/v1/tokens/${tokenId}`);
+ if (tokenResp.ok) {
+ const tokenData = await tokenResp.json();
+ totalAmount = Number(tokenData.emissionAmount || 0);
+ }
+ } catch (e) {
+ console.warn('Error fetching token emission amount:', e);
+ }
+
+ // Fetch ALL boxes for this token to build complete proof
+ const allTokenBoxes: ApiBox[] = [];
+ let tokenOffset = 0;
+ let tokenMore = true;
+ while (tokenMore) {
+ const url = `${explorerUri}/api/v1/boxes/unspent/byTokenId/${tokenId}?offset=${tokenOffset}&limit=100`;
+ try {
+ const resp = await fetch(url);
+ if (!resp.ok) { tokenMore = false; continue; }
+ const data = await resp.json();
+ if (!data.items || data.items.length === 0) { tokenMore = false; continue; }
+ allTokenBoxes.push(...data.items);
+ tokenOffset += 100;
+ if (data.items.length < 100) tokenMore = false;
+ } catch { tokenMore = false; }
+ }
+
+ // Build the ReputationProof
+ const r7Val = profileBoxes[0].additionalRegisters.R7;
+ const proof: import('reputation-system').ReputationProof = {
+ token_id: tokenId,
+ types: [],
+ data: {},
+ total_amount: totalAmount,
+ owner_ergotree: (r7Val?.renderedValue as string) ?? '',
+ owner_serialized: r7Val?.serializedValue ?? '',
+ can_be_spend: true,
+ current_boxes: [],
+ number_of_boxes: 0,
+ network: 'ergo',
+ };
+
+ const uniqueTypeIds = new Set();
+ for (const box of allTokenBoxes) {
+ const rpbox = convertToRPBox(box, tokenId, availableTypes);
+ if (rpbox) {
+ proof.current_boxes.push(rpbox);
+ proof.number_of_boxes += 1;
+
+ // Check if this is a self-referencing box (profile type)
+ // Handle both raw and UTF-8 encoded R5 values
+ const r5Match = rpbox.object_pointer === tokenId
+ || hexToUtf8(rpbox.object_pointer) === tokenId;
+ if (r5Match) {
+ const typeId = rpbox.type.tokenId;
+ if (!uniqueTypeIds.has(typeId)) {
+ uniqueTypeIds.add(typeId);
+ proof.types.push(rpbox.type);
+ }
+ }
+ }
+ }
+
+ console.log('Fallback profile detection found proof:', proof);
+ return proof;
+ } catch (err) {
+ console.error('Fallback profile detection error:', err);
+ return null;
+ }
+ }
+
async function loadUserProfile() {
try {
const types = await fetchTypeNfts(get(explorer_uri));
+
+ // First try the library's standard profile detection
const proofs = await fetchAllUserProfiles(
get(explorer_uri),
true,
[PROFILE_TYPE_NFT_ID],
types,
);
- const proof = proofs[0]; // TODO Select one.
+ let proof = proofs[0];
+
+ // If the library didn't find a profile, try the fallback detection
+ // which handles the R5 UTF-8 encoding edge case
+ if (!proof) {
+ console.log('Standard profile detection found nothing, trying fallback...');
+ proof = await fallbackProfileDetection(get(explorer_uri), types) ?? undefined as any;
+ }
+
console.log("Fetched profile proof:", proof);
- reputation_proof.set(proof);
+ reputation_proof.set(proof ?? null);
console.log("Profile loaded:", proof);
} catch (err) {
console.error("Error loading profile:", err);
@@ -231,11 +433,13 @@
webTx: string;
webAddr: string;
webTkn: string;
+ hashValidation: boolean;
}) {
explorer_uri.set(settings.explorerUri);
web_explorer_uri_tx.set(settings.webTx);
web_explorer_uri_addr.set(settings.webAddr);
web_explorer_uri_tkn.set(settings.webTkn);
+ hashValidationEnabled.set(settings.hashValidation);
}
// --- Centralized State Actions ---
@@ -408,6 +612,7 @@
webTx={$web_explorer_uri_tx}
webAddr={$web_explorer_uri_addr}
webTkn={$web_explorer_uri_tkn}
+ hashValidation={$hashValidationEnabled}
onSave={handleSettingsSave}
/>
@@ -498,6 +703,7 @@
explorerUri={$explorer_uri}
{source_explorer_url}
hash={creationHashStore}
+ hashValidationEnabled={$hashValidationEnabled}
/>
{/if}
From 137a2e6eb0ddee3ed0eb70e843dd6a0c052a2fcd Mon Sep 17 00:00:00 2001
From: Captain Efficiency
Date: Sat, 4 Apr 2026 11:41:46 -0400
Subject: [PATCH 04/23] fix: accept any profile type for detection, not just
PROFILE_TYPE_NFT_ID
Josemi's wallet has a JUDGE-type profile (from GoP) but the source app
was filtering by PROFILE_TYPE_NFT_ID only. Now:
1. fetchAllUserProfiles called with empty types array [] to accept any type
2. Fallback detection no longer filters by R4 register
This matches Game of Prompts behavior where profiles of any type are detected.
---
src/routes/App.svelte | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/src/routes/App.svelte b/src/routes/App.svelte
index ddeaf48..2ec8aa7 100644
--- a/src/routes/App.svelte
+++ b/src/routes/App.svelte
@@ -205,11 +205,8 @@
// Strip Coll[Byte] prefix (0e + length byte(s))
const r7Rendered = r7Serialized.startsWith('0e') ? r7Serialized.substring(4) : r7Serialized;
- // Also compute the R4 rendered value for PROFILE_TYPE_NFT_ID
- const profileTypeBytes = hexToBytes(PROFILE_TYPE_NFT_ID);
- if (!profileTypeBytes) return null;
-
- // Query the Explorer API for boxes matching our ergo tree, R7, and R4
+ // Query the Explorer API for boxes matching our ergo tree and R7
+ // Don't filter by R4 type — accept any profile type
const ergo_tree_hash = ERGO_TREE_HASH;
const allBoxes: ApiBox[] = [];
@@ -222,7 +219,6 @@
const body = {
ergoTreeTemplateHash: ergo_tree_hash,
registers: {
- R4: PROFILE_TYPE_NFT_ID,
R7: r7Rendered,
},
assets: [],
@@ -351,10 +347,11 @@
const types = await fetchTypeNfts(get(explorer_uri));
// First try the library's standard profile detection
+ // Pass empty array to accept any profile type (JUDGE, PROFILE, etc.)
const proofs = await fetchAllUserProfiles(
get(explorer_uri),
true,
- [PROFILE_TYPE_NFT_ID],
+ [],
types,
);
let proof = proofs[0];
From b9cbf1777d65965a845858a53e353d946d1ee2bf Mon Sep 17 00:00:00 2001
From: Captain Efficiency
Date: Sat, 4 Apr 2026 12:10:37 -0400
Subject: [PATCH 05/23] feat: add GitHub Pages deployment with dynamic base
path and esRawPlugin
- Update svelte.config.js with dynamic BASE_PATH for GitHub Pages
- Add esRawPlugin to vite.config.ts for .es contract files
- Clean up vite.config.ts (remove unused config block)
- Update deploy.yml to follow standardized template
- Layout already has ssr=false and prerender=true
---
.github/workflows/deploy.yml | 43 ++++++++++++++----------------------
svelte.config.js | 15 ++++++-------
vite.config.ts | 36 +++++++++++++++++-------------
3 files changed, 45 insertions(+), 49 deletions(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index bcbb02e..83620ae 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,4 +1,4 @@
-name: Deploy to GitHub Pages & Package
+name: Deploy to GitHub Pages
on:
push:
@@ -11,41 +11,32 @@ permissions:
id-token: write
concurrency:
- group: pages
- cancel-in-progress: true
+ group: "pages"
+ cancel-in-progress: false
jobs:
- build-and-deploy:
+ build:
runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build site
- run: npm run build
+ - run: npm ci
+ - run: npm run build
env:
NODE_ENV: production
-
- - name: Package library
- run: npm run package
-
- - name: Upload Pages artifact
- uses: actions/upload-pages-artifact@v3
+ - uses: actions/upload-pages-artifact@v3
with:
path: build
- - name: Deploy to GitHub Pages
- id: deployment
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
uses: actions/deploy-pages@v4
diff --git a/svelte.config.js b/svelte.config.js
index 025e1c0..b0db292 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -1,24 +1,23 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/kit/vite';
+const base = process.env.BASE_PATH ?? (process.env.NODE_ENV === 'production'
+ ? `/${(process.env.GITHUB_REPOSITORY ?? '').split('/')[1] ?? ''}`.replace(/\/$/, '')
+ : '');
+
/** @type {import('@sveltejs/kit').Config} */
const config = {
- // Consult https://kit.svelte.dev/docs/integrations#preprocessors
- // for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
- // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
- // If your environment is not supported or you settled on a specific environment, switch out the adapter.
- // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter({
pages: 'build',
assets: 'build',
- fallback: null
+ fallback: 'index.html'
}),
paths: {
- base: ''
- }
+ base
+ }
}
};
diff --git a/vite.config.ts b/vite.config.ts
index 5e7dd1f..a491ec0 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,18 +1,31 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
+import { readFileSync } from 'node:fs';
import path from "path";
+function esRawPlugin() {
+ return {
+ name: 'es-raw',
+ enforce: 'pre' as const,
+ load(id: string) {
+ if (!id.endsWith('.es')) return null;
+ const source = readFileSync(id, 'utf-8');
+ return `export default ${JSON.stringify(source)};`;
+ }
+ };
+}
+
export default defineConfig({
- plugins: [sveltekit()],
+ plugins: [esRawPlugin(), sveltekit()],
test: {
- globals: true,
- environment: 'node',
+ globals: true,
+ environment: 'node',
+ },
+ resolve: {
+ alias: {
+ $lib: path.resolve("./src/lib"),
+ },
},
- resolve: {
- alias: {
- $lib: path.resolve("./src/lib"),
- },
- },
optimizeDeps: {
esbuildOptions: {
loader: {
@@ -21,10 +34,3 @@ export default defineConfig({
},
},
});
-
-const config = {
- // …
- ssr: {
- noExternal: ['three']
- }
- }
From f195df9b47d24a12cdfd15fd077b836f302397ae Mon Sep 17 00:00:00 2001
From: Captain Efficiency
Date: Mon, 6 Apr 2026 06:55:33 -0400
Subject: [PATCH 06/23] feat: export all library components (SearchByHash,
ProfileSources, Timeline, modals, etc.)
Previously only 4 components were exported. Now all components
from src/lib/components/ are available to library consumers,
matching the full web app experience.
---
src/lib/index.ts | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 5123193..7fef254 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -75,8 +75,22 @@ export {
hashValidationEnabled
} from './ergo/store';
+// ===== HASH UTILITIES =====
+export {
+ HASH_OPTIONS,
+ SEARCH_HASH_ALGORITHMS
+} from './ergo/hashUtils';
+
// ===== SVELTE COMPONENTS =====
export { default as ProfileCard } from './components/ProfileCard.svelte';
export { default as FileSourceCreation } from './components/FileSourceCreation.svelte';
export { default as FileSourceCard } from './components/FileSourceCard.svelte';
-export { default as FileCard } from './components/FileCard.svelte';
\ No newline at end of file
+export { default as FileCard } from './components/FileCard.svelte';
+export { default as SearchByHash } from './components/SearchByHash.svelte';
+export { default as ProfileSources } from './components/ProfileSources.svelte';
+export { default as DownloadSourceCard } from './components/DownloadSourceCard.svelte';
+export { default as ProfileSourceGroupView } from './components/ProfileSourceGroup.svelte';
+export { default as Timeline } from './components/Timeline.svelte';
+export { default as SettingsModal } from './components/SettingsModal.svelte';
+export { default as ProfileModal } from './components/ProfileModal.svelte';
+export { default as AddSource } from './components/AddSource.svelte';
\ No newline at end of file
From 257556a9bc6c95c994a9b77e4b4811941f5e01ea Mon Sep 17 00:00:00 2001
From: Captain Efficiency
Date: Tue, 7 Apr 2026 06:46:33 -0400
Subject: [PATCH 07/23] build: package updated library with hash function,
format fields, and validation
FileSourceCreation now includes:
- Hash Function ID selector with known algorithms
- Content Hash + Content Format fields
- Raw Format (toggleable via content-equals-raw checkbox)
- Chunked file manifest support
- Hash validation per algorithm
- URL parameter pre-fill support
Co-Authored-By: Claude Opus 4.6
---
dist/components/AddSource.svelte | 88 +++-
dist/components/AddSource.svelte.d.ts | 3 +
dist/components/DownloadSourceCard.svelte | 13 +-
dist/components/FileCard.svelte | 28 +-
dist/components/FileSourceCard.svelte | 198 +++++++--
dist/components/FileSourceCreation.svelte | 410 ++++++++++++++----
.../components/FileSourceCreation.svelte.d.ts | 1 +
dist/components/ProfileSourceGroup.svelte | 8 +-
dist/components/ProfileSources.svelte | 5 +-
dist/components/SearchByHash.svelte | 89 ++--
dist/components/SearchByHash.svelte.d.ts | 2 +-
dist/components/SettingsModal.svelte | 27 +-
dist/components/SettingsModal.svelte.d.ts | 2 +
dist/ergo/envs.d.ts | 1 +
dist/ergo/envs.js | 2 +
dist/ergo/hashUtils.d.ts | 77 ++++
dist/ergo/hashUtils.js | 153 +++++++
dist/ergo/sourceFetch.d.ts | 2 +-
dist/ergo/sourceFetch.js | 67 +--
dist/ergo/sourceObject.d.ts | 55 ++-
dist/ergo/sourceObject.js | 143 +++++-
dist/ergo/sourceStore.d.ts | 26 +-
dist/ergo/sourceStore.js | 48 +-
dist/ergo/store.d.ts | 10 +
dist/ergo/store.js | 7 +
dist/index.d.ts | 14 +-
dist/index.js | 14 +-
27 files changed, 1259 insertions(+), 234 deletions(-)
create mode 100644 dist/ergo/hashUtils.d.ts
create mode 100644 dist/ergo/hashUtils.js
diff --git a/dist/components/AddSource.svelte b/dist/components/AddSource.svelte
index 35c5060..6facff8 100644
--- a/dist/components/AddSource.svelte
+++ b/dist/components/AddSource.svelte
@@ -1,27 +1,58 @@
@@ -107,6 +112,26 @@ function restoreDefaults() {
URL prefix for viewing tokens.
+
+
+
Verification
+
+
+
+
+
+ When enabled, adding a source will download the file from the URL
+ and verify its hash matches before submitting the transaction.
+ This may cause issues with large files or CORS-restricted URLs.
+
+
+
+
void;
};
events: {
diff --git a/dist/ergo/envs.d.ts b/dist/ergo/envs.d.ts
index 3a22e27..1ce836a 100644
--- a/dist/ergo/envs.d.ts
+++ b/dist/ergo/envs.d.ts
@@ -9,4 +9,5 @@ export declare const FILE_SOURCE_TYPE_NFT_ID = "8299d98e15ebee7fa39ad716de7c8bb1
export declare const INVALID_FILE_SOURCE_TYPE_NFT_ID = "0000000000000000000000000000000000000000000000000000000000000002";
export declare const UNAVAILABLE_SOURCE_TYPE_NFT_ID = "0000000000000000000000000000000000000000000000000000000000000003";
export declare const PROFILE_OPINION_TYPE_NFT_ID = "0000000000000000000000000000000000000000000000000000000000000004";
+export declare const ERGO_TREE_HASH = "e84b95d84a30df33aa258fe2b9d24c3e75e27a67c6453983c19703029112d147";
export declare const SOURCE_OPINION_TYPE_NFT_ID = "0000000000000000000000000000000000000000000000000000000000000005";
diff --git a/dist/ergo/envs.js b/dist/ergo/envs.js
index 7dd9c0a..fa97eaf 100644
--- a/dist/ergo/envs.js
+++ b/dist/ergo/envs.js
@@ -15,5 +15,7 @@ export const FILE_SOURCE_TYPE_NFT_ID = "8299d98e15ebee7fa39ad716de7c8bb191790a1b
export const INVALID_FILE_SOURCE_TYPE_NFT_ID = "0000000000000000000000000000000000000000000000000000000000000002";
export const UNAVAILABLE_SOURCE_TYPE_NFT_ID = "0000000000000000000000000000000000000000000000000000000000000003";
export const PROFILE_OPINION_TYPE_NFT_ID = "0000000000000000000000000000000000000000000000000000000000000004";
+// Reputation proof contract hash (from reputation-system library)
+export const ERGO_TREE_HASH = "e84b95d84a30df33aa258fe2b9d24c3e75e27a67c6453983c19703029112d147";
// Deprecated
export const SOURCE_OPINION_TYPE_NFT_ID = "0000000000000000000000000000000000000000000000000000000000000005";
diff --git a/dist/ergo/hashUtils.d.ts b/dist/ergo/hashUtils.d.ts
new file mode 100644
index 0000000..83e1193
--- /dev/null
+++ b/dist/ergo/hashUtils.d.ts
@@ -0,0 +1,77 @@
+/**
+ * Hash utility functions for source verification.
+ * Supports SHA3-256, SHA-256, Keccak-256, and Blake2b.
+ * Uses @noble/hashes (already a transitive dependency via @fleet-sdk/crypto).
+ */
+/** Known hash algorithm IDs used in the application */
+export declare const HASH_ALGORITHMS: readonly [{
+ readonly label: "SHA3-256";
+ readonly value: "sha3_256";
+}, {
+ readonly label: "Blake2b";
+ readonly value: "blake2b";
+}, {
+ readonly label: "SHA-256";
+ readonly value: "sha256";
+}, {
+ readonly label: "Keccak-256";
+ readonly value: "keccak256";
+}];
+/** All algorithm values including custom */
+export declare const HASH_OPTIONS: readonly [{
+ readonly label: "SHA3-256";
+ readonly value: "sha3_256";
+}, {
+ readonly label: "Blake2b";
+ readonly value: "blake2b";
+}, {
+ readonly label: "SHA-256";
+ readonly value: "sha256";
+}, {
+ readonly label: "Keccak-256";
+ readonly value: "keccak256";
+}, {
+ readonly label: "Custom";
+ readonly value: "__custom__";
+}];
+/** Algorithm values for search (no custom — frontend can't compute unknown algorithms) */
+export declare const SEARCH_HASH_ALGORITHMS: readonly [{
+ readonly label: "SHA3-256";
+ readonly value: "sha3_256";
+}, {
+ readonly label: "Blake2b";
+ readonly value: "blake2b";
+}, {
+ readonly label: "SHA-256";
+ readonly value: "sha256";
+}, {
+ readonly label: "Keccak-256";
+ readonly value: "keccak256";
+}];
+/**
+ * Compute a hash of the given data using the specified algorithm.
+ * @returns hex string of the hash, or null if algorithm is unknown/custom
+ */
+export declare function computeHash(data: Uint8Array, algorithmId: string): string | null;
+/**
+ * Validate a hex hash string for a given algorithm.
+ * Returns null if valid, or an error message if invalid.
+ */
+export declare function validateHash(hash: string, algorithmId: string): string | null;
+/**
+ * Get the human-readable label for an algorithm ID.
+ */
+export declare function getAlgorithmLabel(algorithmId: string): string;
+/**
+ * Download content from a URL and compute its hash.
+ * Supports chunked files (manifest-based): if isChunked is true,
+ * the URL is treated as a manifest where each line is a chunk URL.
+ *
+ * @param url - The URL to fetch (or manifest URL if chunked)
+ * @param algorithmId - Hash algorithm to use
+ * @param isChunked - Whether this is a chunked manifest
+ * @param onProgress - Optional progress callback (current, total) for chunked downloads
+ * @returns hex hash string
+ * @throws if algorithm is custom/unknown, fetch fails, etc.
+ */
+export declare function downloadAndHash(url: string, algorithmId: string, isChunked?: boolean, onProgress?: (current: number, total: number) => void): Promise;
diff --git a/dist/ergo/hashUtils.js b/dist/ergo/hashUtils.js
new file mode 100644
index 0000000..24f8c0a
--- /dev/null
+++ b/dist/ergo/hashUtils.js
@@ -0,0 +1,153 @@
+/**
+ * Hash utility functions for source verification.
+ * Supports SHA3-256, SHA-256, Keccak-256, and Blake2b.
+ * Uses @noble/hashes (already a transitive dependency via @fleet-sdk/crypto).
+ */
+import { sha256 } from '@noble/hashes/sha256';
+import { sha3_256, keccak_256 } from '@noble/hashes/sha3';
+import { blake2b } from '@noble/hashes/blake2b';
+/** Known hash algorithm IDs used in the application */
+export const HASH_ALGORITHMS = [
+ { label: "SHA3-256", value: "sha3_256" },
+ { label: "Blake2b", value: "blake2b" },
+ { label: "SHA-256", value: "sha256" },
+ { label: "Keccak-256", value: "keccak256" },
+];
+/** All algorithm values including custom */
+export const HASH_OPTIONS = [
+ ...HASH_ALGORITHMS,
+ { label: "Custom", value: "__custom__" },
+];
+/** Algorithm values for search (no custom — frontend can't compute unknown algorithms) */
+export const SEARCH_HASH_ALGORITHMS = HASH_ALGORITHMS;
+function uint8ArrayToHex(array) {
+ return [...array].map(x => x.toString(16).padStart(2, '0')).join('');
+}
+/**
+ * Compute a hash of the given data using the specified algorithm.
+ * @returns hex string of the hash, or null if algorithm is unknown/custom
+ */
+export function computeHash(data, algorithmId) {
+ switch (algorithmId) {
+ case 'sha256':
+ return uint8ArrayToHex(sha256(data));
+ case 'sha3_256':
+ return uint8ArrayToHex(sha3_256(data));
+ case 'keccak256':
+ return uint8ArrayToHex(keccak_256(data));
+ case 'blake2b':
+ // Default to 256-bit (32 bytes) output
+ return uint8ArrayToHex(blake2b(data, { dkLen: 32 }));
+ default:
+ return null;
+ }
+}
+/**
+ * Validate a hex hash string for a given algorithm.
+ * Returns null if valid, or an error message if invalid.
+ */
+export function validateHash(hash, algorithmId) {
+ if (!hash || hash.trim() === '') {
+ return 'Hash cannot be empty';
+ }
+ const trimmed = hash.trim();
+ // Check hex characters
+ if (!/^[0-9a-fA-F]+$/.test(trimmed)) {
+ return 'Hash must contain only hexadecimal characters (0-9, a-f)';
+ }
+ switch (algorithmId) {
+ case 'sha3_256':
+ case 'sha256':
+ case 'keccak256':
+ if (trimmed.length !== 64) {
+ return `${getAlgorithmLabel(algorithmId)} hash must be exactly 64 hex characters (256-bit). Got ${trimmed.length}.`;
+ }
+ break;
+ case 'blake2b':
+ if (trimmed.length !== 64 && trimmed.length !== 128) {
+ return `Blake2b hash must be 64 hex characters (256-bit) or 128 hex characters (512-bit). Got ${trimmed.length}.`;
+ }
+ break;
+ case '__custom__':
+ // Custom algorithm — only validate hex and non-empty
+ break;
+ default:
+ // Unknown algorithm id — only validate hex
+ break;
+ }
+ return null;
+}
+/**
+ * Get the human-readable label for an algorithm ID.
+ */
+export function getAlgorithmLabel(algorithmId) {
+ const found = HASH_OPTIONS.find(o => o.value === algorithmId);
+ return found ? found.label : algorithmId;
+}
+/**
+ * Download content from a URL and compute its hash.
+ * Supports chunked files (manifest-based): if isChunked is true,
+ * the URL is treated as a manifest where each line is a chunk URL.
+ *
+ * @param url - The URL to fetch (or manifest URL if chunked)
+ * @param algorithmId - Hash algorithm to use
+ * @param isChunked - Whether this is a chunked manifest
+ * @param onProgress - Optional progress callback (current, total) for chunked downloads
+ * @returns hex hash string
+ * @throws if algorithm is custom/unknown, fetch fails, etc.
+ */
+export async function downloadAndHash(url, algorithmId, isChunked = false, onProgress) {
+ if (algorithmId === '__custom__' || !HASH_ALGORITHMS.some(a => a.value === algorithmId)) {
+ throw new Error('Cannot verify: custom hash algorithm');
+ }
+ let data;
+ if (isChunked) {
+ // Fetch manifest
+ const manifestResponse = await fetch(url);
+ if (!manifestResponse.ok) {
+ throw new Error(`Failed to fetch manifest: ${manifestResponse.statusText}`);
+ }
+ const manifestText = await manifestResponse.text();
+ const chunkUrls = manifestText.trim().split('\n').filter(line => line.trim() !== '');
+ if (chunkUrls.length === 0) {
+ throw new Error('Manifest is empty — no chunk URLs found');
+ }
+ // Download all chunks in order
+ const chunks = [];
+ let totalSize = 0;
+ for (let i = 0; i < chunkUrls.length; i++) {
+ if (onProgress)
+ onProgress(i, chunkUrls.length);
+ const chunkResponse = await fetch(chunkUrls[i].trim());
+ if (!chunkResponse.ok) {
+ throw new Error(`Failed to fetch chunk ${i + 1}/${chunkUrls.length}: ${chunkResponse.statusText}`);
+ }
+ const chunkBuffer = await chunkResponse.arrayBuffer();
+ const chunkBytes = new Uint8Array(chunkBuffer);
+ chunks.push(chunkBytes);
+ totalSize += chunkBytes.length;
+ }
+ if (onProgress)
+ onProgress(chunkUrls.length, chunkUrls.length);
+ // Concatenate all chunks
+ data = new Uint8Array(totalSize);
+ let offset = 0;
+ for (const chunk of chunks) {
+ data.set(chunk, offset);
+ offset += chunk.length;
+ }
+ }
+ else {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch file: ${response.statusText}`);
+ }
+ const buffer = await response.arrayBuffer();
+ data = new Uint8Array(buffer);
+ }
+ const result = computeHash(data, algorithmId);
+ if (result === null) {
+ throw new Error(`Cannot verify: unsupported hash algorithm "${algorithmId}"`);
+ }
+ return result;
+}
diff --git a/dist/ergo/sourceFetch.d.ts b/dist/ergo/sourceFetch.d.ts
index 488bba7..588af80 100644
--- a/dist/ergo/sourceFetch.d.ts
+++ b/dist/ergo/sourceFetch.d.ts
@@ -1,7 +1,7 @@
import { type FileSource, type ProfileOpinion, type SearchResult, type ProfileData, type InvalidFileSource, type UnavailableSource } from './sourceObject';
/**
* Fetch all FILE_SOURCE boxes for a specific file hash.
- * Returns all sources (URLs) where this file can be found.
+ * Returns all sources where this file can be found.
*/
export declare function fetchFileSourcesByHash(fileHash: string, explorerUri: string): Promise;
/**
diff --git a/dist/ergo/sourceFetch.js b/dist/ergo/sourceFetch.js
index 105ceb2..4f385c1 100644
--- a/dist/ergo/sourceFetch.js
+++ b/dist/ergo/sourceFetch.js
@@ -1,10 +1,31 @@
+import { deserializeSourceEntry } from './sourceObject';
import { hexToUtf8 } from './utils';
import { FILE_SOURCE_TYPE_NFT_ID, INVALID_FILE_SOURCE_TYPE_NFT_ID, UNAVAILABLE_SOURCE_TYPE_NFT_ID, PROFILE_OPINION_TYPE_NFT_ID } from './envs';
import DOMPurify from "dompurify";
import { getTimestampFromBlockId, searchBoxes } from 'reputation-system';
+/**
+ * Parse R9 content from a box into SourceEntry[].
+ * Handles both new JSON format and legacy plain URL string.
+ */
+function parseR9Content(box) {
+ let rawContent = "[Unreadable Content]";
+ try {
+ const rawValue = box.additionalRegisters.R9?.renderedValue;
+ if (rawValue) {
+ rawContent = hexToUtf8(rawValue) ?? "[Empty Content]";
+ // Sanitize for display safety
+ rawContent = DOMPurify.sanitize(rawContent);
+ }
+ }
+ catch (e) {
+ console.warn(`Error decoding R9 for box ${box.boxId}`, e);
+ rawContent = "";
+ }
+ return { source: deserializeSourceEntry(rawContent) };
+}
/**
* Fetch all FILE_SOURCE boxes for a specific file hash.
- * Returns all sources (URLs) where this file can be found.
+ * Returns all sources where this file can be found.
*/
export async function fetchFileSourcesByHash(fileHash, explorerUri) {
console.log("Fetching file sources for hash:", fileHash);
@@ -19,22 +40,14 @@ export async function fetchFileSourcesByHash(fileHash, explorerUri) {
continue;
if (!box.additionalRegisters.R9?.renderedValue)
continue;
- let sourceUrl = "[Unreadable URL]";
- try {
- const rawValue = box.additionalRegisters.R9.renderedValue;
- if (rawValue) {
- sourceUrl = hexToUtf8(rawValue) ?? "[Empty URL]";
- // Sanitize URL for display
- sourceUrl = DOMPurify.sanitize(sourceUrl);
- }
- }
- catch (e) {
- console.warn(`Error decoding R9 for box ${box.boxId}`, e);
- }
+ const { source: sourceEntry } = parseR9Content(box);
+ // Extract hashFunctionId from the source entry
+ const hashFunctionId = sourceEntry.hashFunctionId || '';
const source = {
id: box.boxId,
fileHash: fileHash,
- sourceUrl: sourceUrl,
+ hashFunctionId: hashFunctionId,
+ source: sourceEntry,
ownerTokenId: box.assets[0].tokenId,
reputationAmount: Number(box.assets[0].amount),
timestamp: await getTimestampFromBlockId(explorerUri, box.blockId),
@@ -146,21 +159,13 @@ export async function fetchFileSourcesByProfile(profileTokenId, limit = 50, expl
catch (e) {
console.warn(`Error decoding R5 for box ${box.boxId}`, e);
}
- let sourceUrl = "[Unreadable URL]";
- try {
- const rawValue = box.additionalRegisters.R9.renderedValue;
- if (rawValue) {
- sourceUrl = hexToUtf8(rawValue) ?? "[Empty URL]";
- sourceUrl = DOMPurify.sanitize(sourceUrl);
- }
- }
- catch (e) {
- console.warn(`Error decoding R9 for box ${box.boxId}`, e);
- }
+ const { source: sourceEntry } = parseR9Content(box);
+ const hashFunctionId = sourceEntry.hashFunctionId || '';
const source = {
id: box.boxId,
fileHash: fileHash,
- sourceUrl: sourceUrl,
+ hashFunctionId: hashFunctionId,
+ source: sourceEntry,
ownerTokenId: box.assets[0].tokenId,
reputationAmount: Number(box.assets[0].amount),
timestamp: await getTimestampFromBlockId(explorerUri, box.blockId),
@@ -251,12 +256,12 @@ export async function searchByHash(fileHash, explorerUri) {
const invs = await fetchInvalidFileSources(source.id, explorerUri);
if (invs.length > 0)
invalidations[source.id] = invs;
- // Fetch unavailabilities for this URL
- // Optimization: check if we already fetched for this URL
- if (!unavailabilities[source.sourceUrl]) {
- const unavs = await fetchUnavailableSources(explorerUri, source.sourceUrl);
+ // Fetch unavailabilities for the source URL
+ const url = source.source?.urlLink;
+ if (url && !unavailabilities[url]) {
+ const unavs = await fetchUnavailableSources(url, explorerUri);
if (unavs.length > 0)
- unavailabilities[source.sourceUrl] = unavs;
+ unavailabilities[url] = unavs;
}
}
return { sources, invalidations, unavailabilities };
diff --git a/dist/ergo/sourceObject.d.ts b/dist/ergo/sourceObject.d.ts
index 35b9fc1..e0a046b 100644
--- a/dist/ergo/sourceObject.d.ts
+++ b/dist/ergo/sourceObject.d.ts
@@ -4,10 +4,19 @@
* This module defines the core interfaces for the decentralized File Discovery
* and Verification system built on Ergo blockchain.
*/
+export interface SourceEntry {
+ hashFunctionId: string;
+ contentFormat: string;
+ contentHash: string;
+ rawFormat: string;
+ urlLink: string;
+ isChunked?: boolean;
+}
export interface FileSource {
id: string;
fileHash: string;
- sourceUrl: string;
+ hashFunctionId: string;
+ source: SourceEntry;
ownerTokenId: string;
reputationAmount: number;
timestamp: number;
@@ -99,7 +108,18 @@ export interface ProfileSourceGroup {
sources: FileSource[];
}
/**
- * Group file sources by their download URL.
+ * Get the primary URL from a FileSource.
+ * Returns the first source entry's URL, or an empty string if no sources.
+ */
+export declare function getPrimaryUrl(source: FileSource): string;
+/**
+ * Get all URLs from a FileSource.
+ * With single source entry, returns an array with one URL.
+ */
+export declare function getAllUrls(source: FileSource): string[];
+/**
+ * Group file sources by their download URLs.
+ * A FileSource can contain multiple URLs; it will appear in each group.
*/
export declare function groupByDownloadSource(sources: FileSource[], invalidationsMap: Record s.id === source.id)) {
+ groups[url].sources.push(source);
+ }
+ if (!groups[url].owners.includes(source.ownerTokenId)) {
+ groups[url].owners.push(source.ownerTokenId);
}
// Add invalidations for this specific box
const boxInvalidations = invalidationsMap[source.id]?.data || [];
- groups[source.sourceUrl].invalidations.push(...boxInvalidations);
+ groups[url].invalidations.push(...boxInvalidations);
}
return Object.values(groups).sort((a, b) => b.sources.length - a.sources.length);
}
@@ -62,14 +83,15 @@ export function calculateProfileTrust(profileTokenId, opinions) {
* Aggregate opinions into score data for a file source.
*/
export function aggregateSourceScore(source, allSources, invalidations, unavailabilities, profileOpinions = []) {
- // Confirmations are other sources with same hash and URL
+ // Confirmations are other sources with same hash and same URL
+ const sourceUrl = source.source?.urlLink || '';
const confirmations = allSources.filter(s => s.id !== source.id &&
s.fileHash === source.fileHash &&
- s.sourceUrl === source.sourceUrl);
+ s.source?.urlLink === sourceUrl);
// Invalidations for this specific box
const filteredInvalidations = invalidations.filter(inv => inv.targetBoxId === source.id);
- // Unavailabilities for this specific URL
- const filteredUnavailabilities = unavailabilities.filter(un => un.sourceUrl === source.sourceUrl);
+ // Unavailabilities for the URL in this source
+ const filteredUnavailabilities = unavailabilities.filter(un => un.sourceUrl === sourceUrl);
const confirmationScore = confirmations.reduce((sum, s) => sum + s.reputationAmount, 0);
const invalidationScore = filteredInvalidations.reduce((sum, inv) => sum + inv.reputationAmount, 0);
const unavailabilityScore = filteredUnavailabilities.reduce((sum, un) => sum + un.reputationAmount, 0);
@@ -85,3 +107,98 @@ export function aggregateSourceScore(source, allSources, invalidations, unavaila
ownerTrustScore
};
}
+// --- SERIALIZATION HELPERS ---
+/**
+ * Serialize source entries to a JSON string for R9 content.
+ * The reputation-system library encodes this as Coll[Byte] (UTF-8 bytes).
+ *
+ * Format: Coll[Coll[Byte]] — a JSON array containing one tuple (array):
+ * [hash_function_id, content_format, content_hash, raw_format, url_link, is_chunked]
+ *
+ * Serialization format: Coll[Coll[Byte]]
+ * The output represents a Coll[Coll[Byte]] structure — an array containing
+ * one tuple (inner Coll[Byte]) with the source entry fields:
+ * [[hashFunctionId, contentFormat, contentHash, rawFormat, urlLink, isChunked]]
+ *
+ * The reputation-system library encodes this JSON string as Coll[Byte] for R9.
+ * Since encoding operates on the raw UTF-8 bytes of the JSON string (not on
+ * individual tuple elements), mixed types (string + boolean) within the tuple
+ * are fine — JSON.parse restores original types on deserialization.
+ */
+export function serializeSourceEntry(entry) {
+ // Coll[Coll[Byte]]: outer array = Coll, inner tuple = Coll[Byte] elements
+ const tuple = [
+ entry.hashFunctionId,
+ entry.contentFormat,
+ entry.contentHash,
+ entry.rawFormat,
+ entry.urlLink,
+ entry.isChunked ?? false
+ ];
+ return JSON.stringify([tuple]); // Coll[Coll[Byte]] serialized as JSON string
+}
+/**
+ * Deserialize source entries from R9 content string.
+ *
+ * Supports three formats (tried in order):
+ * 1. Coll[Coll[Byte]] tuple format: [[hashFnId, contentFmt, contentHash, rawFmt, urlLink, isChunked]]
+ * 2. Legacy JSON object format: [{ hashFunctionId, contentFormat, ... }]
+ * 3. Legacy plain URL string
+ *
+ * Note: tuple[5] (isChunked) is a boolean while other elements are strings.
+ * This is fine because the JSON string is what gets encoded as Coll[Byte],
+ * and JSON.parse restores the original types.
+ */
+export function deserializeSourceEntry(content) {
+ const empty = {
+ hashFunctionId: '',
+ contentFormat: '',
+ contentHash: '',
+ rawFormat: '',
+ urlLink: ''
+ };
+ if (!content || content.trim() === '')
+ return empty;
+ try {
+ const parsed = JSON.parse(content);
+ if (Array.isArray(parsed) && parsed.length > 0) {
+ const tuple = parsed[0];
+ // Format 1: Coll[Coll[Byte]] tuple array
+ // [[hashFnId, contentFmt, contentHash, rawFmt, urlLink, isChunked?]]
+ if (Array.isArray(tuple) && tuple.length >= 5) {
+ return {
+ hashFunctionId: tuple[0] || '',
+ contentFormat: tuple[1] || '',
+ contentHash: tuple[2] || '',
+ rawFormat: tuple[3] || '',
+ urlLink: tuple[4] || '',
+ isChunked: tuple[5] === true
+ };
+ }
+ // Format 2: Legacy JSON object format
+ // [{ hashFunctionId, contentFormat, contentHash, rawFormat, urlLink, isChunked }]
+ if (typeof tuple === 'object' && tuple !== null && !Array.isArray(tuple)) {
+ return {
+ hashFunctionId: tuple.hashFunctionId || '',
+ contentFormat: tuple.contentFormat || tuple.contentFormatNftId || '',
+ contentHash: tuple.contentHash || '',
+ rawFormat: tuple.rawFormat || tuple.rawFormatNftId || '',
+ urlLink: tuple.urlLink || '',
+ isChunked: tuple.isChunked === true
+ };
+ }
+ }
+ }
+ catch {
+ // Not JSON — treat as legacy plain URL string
+ }
+ // Format 3: Legacy plain URL string
+ return {
+ hashFunctionId: '',
+ contentFormat: '',
+ contentHash: '',
+ rawFormat: '',
+ urlLink: content,
+ isChunked: false
+ };
+}
diff --git a/dist/ergo/sourceStore.d.ts b/dist/ergo/sourceStore.d.ts
index 7b6319f..9d72ee2 100644
--- a/dist/ergo/sourceStore.d.ts
+++ b/dist/ergo/sourceStore.d.ts
@@ -1,24 +1,36 @@
import { type ReputationProof } from 'reputation-system';
-import { type FileSource } from './sourceObject';
+import { type FileSource, type SourceEntry } from './sourceObject';
/**
* Creates a user profile box (same as forum).
*/
export declare function createProfileBox(explorerUri: string): Promise;
/**
* Add a new FILE_SOURCE box.
- * Creates a box with R5=fileHash, R9=sourceUrl.
+ * Creates a box with R5=fileHash (raw file hash), R9=serialized source entries.
+ *
+ * @param fileHash - The raw file hash digest (R5 anchor)
+ * @param hashFunctionId - ID of the hash function used (HASH(EMPTY_INPUT))
+ * @param sourceEntry - Single SourceEntry object for R9
+ * @param proof - User's reputation proof
+ * @param explorerUri - Explorer API endpoint
*/
-export declare function addFileSource(fileHash: string, sourceUrl: string, proof: ReputationProof | null, explorerUri: string): Promise;
+export declare function addFileSource(fileHash: string, hashFunctionId: string, sourceEntry: SourceEntry, proof: ReputationProof | null, explorerUri: string): Promise;
/**
- * Update a FILE_SOURCE box (spend old, create new with same hash but new URL).
+ * Update a FILE_SOURCE box (spend old, create new with same hash but new source entries).
* The old box must be owned by the current user.
+ *
+ * @param oldBoxId - Box ID of the existing FILE_SOURCE to update
+ * @param fileHash - The raw file hash (must match existing)
+ * @param newSourceEntry - New SourceEntry object for R9
+ * @param proof - User's reputation proof
+ * @param explorerUri - Explorer API endpoint
*/
-export declare function updateFileSource(oldBoxId: string, fileHash: string, newSourceUrl: string, proof: ReputationProof | null, explorerUri: string): Promise;
+export declare function updateFileSource(oldBoxId: string, fileHash: string, newSourceEntry: SourceEntry, proof: ReputationProof | null, explorerUri: string): Promise;
/**
* Confirm a FILE_SOURCE box.
- * Creates a new FILE_SOURCE box with same hash and URL.
+ * Creates a new FILE_SOURCE box with same hash and source entries.
*/
-export declare function confirmSource(fileHash: string, sourceUrl: string, proof: ReputationProof | null, currentSources: FileSource[], explorerUri: string): Promise;
+export declare function confirmSource(fileHash: string, hashFunctionId: string, sourceEntry: SourceEntry, proof: ReputationProof | null, currentSources: FileSource[], explorerUri: string): Promise;
/**
* Mark a FILE_SOURCE box as invalid.
* Creates an INVALID_FILE_SOURCE box with R5=sourceBoxId.
diff --git a/dist/ergo/sourceStore.js b/dist/ergo/sourceStore.js
index e8af626..f0b7ff0 100644
--- a/dist/ergo/sourceStore.js
+++ b/dist/ergo/sourceStore.js
@@ -1,4 +1,5 @@
import { create_profile, create_opinion, update_opinion } from 'reputation-system';
+import { serializeSourceEntry } from './sourceObject';
import { FILE_SOURCE_TYPE_NFT_ID, INVALID_FILE_SOURCE_TYPE_NFT_ID, UNAVAILABLE_SOURCE_TYPE_NFT_ID, PROFILE_OPINION_TYPE_NFT_ID, PROFILE_TOTAL_SUPPLY, PROFILE_TYPE_NFT_ID, } from './envs';
/**
* Gets the main profile box from a ReputationProof.
@@ -14,7 +15,7 @@ function getMainProfileBox(proof) {
* Creates a user profile box (same as forum).
*/
export async function createProfileBox(explorerUri) {
- const profileTxId = await create_profile(PROFILE_TOTAL_SUPPLY, PROFILE_TYPE_NFT_ID, explorerUri, { name: "Anon" });
+ const profileTxId = await create_profile(explorerUri, PROFILE_TOTAL_SUPPLY, PROFILE_TYPE_NFT_ID, { name: "Anon" });
if (!profileTxId) {
throw new Error("Fatal error: The profile creation transaction failed to send.");
}
@@ -23,10 +24,16 @@ export async function createProfileBox(explorerUri) {
}
/**
* Add a new FILE_SOURCE box.
- * Creates a box with R5=fileHash, R9=sourceUrl.
+ * Creates a box with R5=fileHash (raw file hash), R9=serialized source entries.
+ *
+ * @param fileHash - The raw file hash digest (R5 anchor)
+ * @param hashFunctionId - ID of the hash function used (HASH(EMPTY_INPUT))
+ * @param sourceEntry - Single SourceEntry object for R9
+ * @param proof - User's reputation proof
+ * @param explorerUri - Explorer API endpoint
*/
-export async function addFileSource(fileHash, sourceUrl, proof, explorerUri) {
- console.log("API: addFileSource", { fileHash, sourceUrl });
+export async function addFileSource(fileHash, hashFunctionId, sourceEntry, proof, explorerUri) {
+ console.log("API: addFileSource", { fileHash, hashFunctionId, sourceEntry });
console.log("Proof:", proof);
if (!proof) {
throw new Error("Reputation proof is required to add a file source.");
@@ -36,12 +43,14 @@ export async function addFileSource(fileHash, sourceUrl, proof, explorerUri) {
if (!mainBox) {
throw new Error("Profile box required but not available yet. Please wait for profile creation to confirm.");
}
+ // Serialize single source entry as JSON for R9 content
+ const serializedContent = serializeSourceEntry(sourceEntry);
const tx = await create_opinion(explorerUri, // explorerUri: Explorer API endpoint
1, // token_amount: 1 token for the new file source box
FILE_SOURCE_TYPE_NFT_ID, // type_nft_id: Type NFT for FILE_SOURCE
- fileHash, // object_pointer: R5 - The file hash
+ fileHash, // object_pointer: R5 - The raw file hash
true, // polarization: R8 - Positive opinion
- sourceUrl, // content: R9 - The source URL
+ serializedContent, // content: R9 - Serialized source entry
false, // is_locked: R6 - Unlocked
mainBox // main_box: The profile box to split from
);
@@ -51,17 +60,25 @@ export async function addFileSource(fileHash, sourceUrl, proof, explorerUri) {
return tx;
}
/**
- * Update a FILE_SOURCE box (spend old, create new with same hash but new URL).
+ * Update a FILE_SOURCE box (spend old, create new with same hash but new source entries).
* The old box must be owned by the current user.
+ *
+ * @param oldBoxId - Box ID of the existing FILE_SOURCE to update
+ * @param fileHash - The raw file hash (must match existing)
+ * @param newSourceEntry - New SourceEntry object for R9
+ * @param proof - User's reputation proof
+ * @param explorerUri - Explorer API endpoint
*/
-export async function updateFileSource(oldBoxId, fileHash, newSourceUrl, proof, explorerUri) {
- console.log("API: updateFileSource", { oldBoxId, fileHash, newSourceUrl });
+export async function updateFileSource(oldBoxId, fileHash, newSourceEntry, proof, explorerUri) {
+ console.log("API: updateFileSource", { oldBoxId, fileHash, newSourceEntry });
// Find the existing file source box to update
const existingBox = proof?.current_boxes.find((b) => b.box.boxId === oldBoxId) || null;
if (!existingBox) {
throw new Error("File source box to update not found.");
}
- const tx = await update_opinion(explorerUri, existingBox, true, newSourceUrl);
+ // Serialize new source entry as JSON for R9 content
+ const serializedContent = serializeSourceEntry(newSourceEntry);
+ const tx = await update_opinion(explorerUri, existingBox, true, serializedContent);
if (!tx)
throw new Error("File source update transaction failed.");
console.log("File source update transaction sent, ID:", tx);
@@ -69,16 +86,17 @@ export async function updateFileSource(oldBoxId, fileHash, newSourceUrl, proof,
}
/**
* Confirm a FILE_SOURCE box.
- * Creates a new FILE_SOURCE box with same hash and URL.
+ * Creates a new FILE_SOURCE box with same hash and source entries.
*/
-export async function confirmSource(fileHash, sourceUrl, proof, currentSources, explorerUri) {
- console.log("API: confirmSource", { fileHash, sourceUrl });
+export async function confirmSource(fileHash, hashFunctionId, sourceEntry, proof, currentSources, explorerUri) {
+ console.log("API: confirmSource", { fileHash, sourceEntry });
// Safety check: has the user already confirmed this?
const userTokenId = proof?.token_id;
- if (userTokenId && currentSources.some(s => s.sourceUrl === sourceUrl && s.ownerTokenId === userTokenId)) {
+ const primaryUrl = sourceEntry.urlLink || '';
+ if (userTokenId && currentSources.some(s => s.source?.urlLink === primaryUrl && s.ownerTokenId === userTokenId)) {
throw new Error("You have already confirmed this source.");
}
- return await addFileSource(fileHash, sourceUrl, proof, explorerUri);
+ return await addFileSource(fileHash, hashFunctionId, sourceEntry, proof, explorerUri);
}
/**
* Mark a FILE_SOURCE box as invalid.
diff --git a/dist/ergo/store.d.ts b/dist/ergo/store.d.ts
index a5b1a10..13a6bf1 100644
--- a/dist/ergo/store.d.ts
+++ b/dist/ergo/store.d.ts
@@ -77,3 +77,13 @@ export declare const profileOpinionsGiven: {
};
export declare const isLoading: import("svelte/store").Writable;
export declare const error: import("svelte/store").Writable;
+/**
+ * When enabled, adding a source will download the file from the URL and verify
+ * its hash matches before submitting the transaction. Disabled by default to
+ * avoid large downloads and CORS issues in the browser.
+ */
+export declare const hashValidationEnabled: {
+ subscribe: (this: void, run: import("svelte/store").Subscriber, invalidate?: import("svelte/store").Invalidator | undefined) => import("svelte/store").Unsubscriber;
+ set: (value: boolean) => void;
+ update: (fn: (value: boolean) => boolean) => void;
+};
diff --git a/dist/ergo/store.js b/dist/ergo/store.js
index ebfa901..6735ca8 100644
--- a/dist/ergo/store.js
+++ b/dist/ergo/store.js
@@ -87,3 +87,10 @@ export const profileUnavailabilities = createPersistentStore('source_profile_una
export const profileOpinionsGiven = createPersistentStore('source_profile_opinions_given', {});
export const isLoading = writable(false);
export const error = writable(null);
+// --- SETTINGS ---
+/**
+ * When enabled, adding a source will download the file from the URL and verify
+ * its hash matches before submitting the transaction. Disabled by default to
+ * avoid large downloads and CORS issues in the browser.
+ */
+export const hashValidationEnabled = createPersistentStore('hash_validation_enabled', false);
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 4060215..246df74 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1,10 +1,20 @@
export { fetchFileSourcesByHash, fetchInvalidFileSources, fetchUnavailableSources, fetchProfileOpinions, fetchFileSourcesByProfile, fetchInvalidFileSourcesByProfile, fetchUnavailableSourcesByProfile, fetchProfileOpinionsByAuthor, searchByHash, loadProfileData } from './ergo/sourceFetch';
export { createProfileBox, addFileSource, updateFileSource, confirmSource, markInvalidSource, markUnavailableSource, trustProfile } from './ergo/sourceStore';
-export type { FileSource, InvalidFileSource, UnavailableSource, ProfileOpinion, TimelineEvent, FileSourceWithScore, DownloadSourceGroup, ProfileSourceGroup, SearchResult, ProfileData } from './ergo/sourceObject';
-export { groupByDownloadSource, groupByProfile, calculateProfileTrust, aggregateSourceScore } from './ergo/sourceObject';
+export type { SourceEntry, FileSource, InvalidFileSource, UnavailableSource, ProfileOpinion, TimelineEvent, FileSourceWithScore, DownloadSourceGroup, ProfileSourceGroup, SearchResult, ProfileData } from './ergo/sourceObject';
+export { groupByDownloadSource, groupByProfile, calculateProfileTrust, aggregateSourceScore, getPrimaryUrl, getAllUrls, serializeSourceEntry, deserializeSourceEntry } from './ergo/sourceObject';
export type { ReputationProof, RPBox, TypeNFT, ApiBox } from './ergo/object';
export { PROFILE_TYPE_NFT_ID, PROFILE_TOTAL_SUPPLY, FILE_SOURCE_TYPE_NFT_ID, INVALID_FILE_SOURCE_TYPE_NFT_ID, UNAVAILABLE_SOURCE_TYPE_NFT_ID, PROFILE_OPINION_TYPE_NFT_ID, } from './ergo/envs';
+export { hashValidationEnabled } from './ergo/store';
+export { HASH_OPTIONS, SEARCH_HASH_ALGORITHMS } from './ergo/hashUtils';
export { default as ProfileCard } from './components/ProfileCard.svelte';
export { default as FileSourceCreation } from './components/FileSourceCreation.svelte';
export { default as FileSourceCard } from './components/FileSourceCard.svelte';
export { default as FileCard } from './components/FileCard.svelte';
+export { default as SearchByHash } from './components/SearchByHash.svelte';
+export { default as ProfileSources } from './components/ProfileSources.svelte';
+export { default as DownloadSourceCard } from './components/DownloadSourceCard.svelte';
+export { default as ProfileSourceGroupView } from './components/ProfileSourceGroup.svelte';
+export { default as Timeline } from './components/Timeline.svelte';
+export { default as SettingsModal } from './components/SettingsModal.svelte';
+export { default as ProfileModal } from './components/ProfileModal.svelte';
+export { default as AddSource } from './components/AddSource.svelte';
diff --git a/dist/index.js b/dist/index.js
index a29b8a4..6fe630d 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -4,11 +4,23 @@
export { fetchFileSourcesByHash, fetchInvalidFileSources, fetchUnavailableSources, fetchProfileOpinions, fetchFileSourcesByProfile, fetchInvalidFileSourcesByProfile, fetchUnavailableSourcesByProfile, fetchProfileOpinionsByAuthor, searchByHash, loadProfileData } from './ergo/sourceFetch';
// ===== SOURCE STORE FUNCTIONS =====
export { createProfileBox, addFileSource, updateFileSource, confirmSource, markInvalidSource, markUnavailableSource, trustProfile } from './ergo/sourceStore';
-export { groupByDownloadSource, groupByProfile, calculateProfileTrust, aggregateSourceScore } from './ergo/sourceObject';
+export { groupByDownloadSource, groupByProfile, calculateProfileTrust, aggregateSourceScore, getPrimaryUrl, getAllUrls, serializeSourceEntry, deserializeSourceEntry } from './ergo/sourceObject';
// ===== ENVIRONMENT CONSTANTS =====
export { PROFILE_TYPE_NFT_ID, PROFILE_TOTAL_SUPPLY, FILE_SOURCE_TYPE_NFT_ID, INVALID_FILE_SOURCE_TYPE_NFT_ID, UNAVAILABLE_SOURCE_TYPE_NFT_ID, PROFILE_OPINION_TYPE_NFT_ID, } from './ergo/envs';
+// ===== SETTINGS STORES =====
+export { hashValidationEnabled } from './ergo/store';
+// ===== HASH UTILITIES =====
+export { HASH_OPTIONS, SEARCH_HASH_ALGORITHMS } from './ergo/hashUtils';
// ===== SVELTE COMPONENTS =====
export { default as ProfileCard } from './components/ProfileCard.svelte';
export { default as FileSourceCreation } from './components/FileSourceCreation.svelte';
export { default as FileSourceCard } from './components/FileSourceCard.svelte';
export { default as FileCard } from './components/FileCard.svelte';
+export { default as SearchByHash } from './components/SearchByHash.svelte';
+export { default as ProfileSources } from './components/ProfileSources.svelte';
+export { default as DownloadSourceCard } from './components/DownloadSourceCard.svelte';
+export { default as ProfileSourceGroupView } from './components/ProfileSourceGroup.svelte';
+export { default as Timeline } from './components/Timeline.svelte';
+export { default as SettingsModal } from './components/SettingsModal.svelte';
+export { default as ProfileModal } from './components/ProfileModal.svelte';
+export { default as AddSource } from './components/AddSource.svelte';
From afc0de24c95ea0a58a59dd2e36d34db05c0ae3e1 Mon Sep 17 00:00:00 2001
From: Captain Efficiency
Date: Tue, 7 Apr 2026 07:11:53 -0400
Subject: [PATCH 08/23] ci: auto-build dist on push to master
Runs `npm run package` and commits updated dist/ when src/lib changes.
No more need to run `npm run package` locally.
Co-Authored-By: Claude Opus 4.6
---
.github/workflows/package.yml | 42 +++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100644 .github/workflows/package.yml
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
new file mode 100644
index 0000000..1f6c6ff
--- /dev/null
+++ b/.github/workflows/package.yml
@@ -0,0 +1,42 @@
+name: Build & commit dist
+
+on:
+ push:
+ branches: [master]
+ paths:
+ - 'src/lib/**'
+ - 'package.json'
+ - 'package-lock.json'
+ - 'svelte.config.js'
+ - 'tsconfig.json'
+
+permissions:
+ contents: write
+
+jobs:
+ package:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - run: npm ci
+ - run: npm run package
+
+ - name: Commit dist if changed
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add dist/
+ if git diff --cached --quiet; then
+ echo "No dist changes to commit"
+ else
+ git commit -m "build: auto-package dist [skip ci]"
+ git push
+ fi
From 5c9ae48df6a526cff9696171e3074074547337af Mon Sep 17 00:00:00 2001
From: 0xf965 <40121100+0xf965@users.noreply.github.com>
Date: Wed, 8 Apr 2026 08:11:40 +0200
Subject: [PATCH 09/23] improved library - fixed mode, no hash function id
---
.gitignore | 3 +-
LIBRARY.md | 4 +
dist/components/FileSourceCreation.svelte | 92 +++++++++-------
.../components/FileSourceCreation.svelte.d.ts | 1 +
dist/ergo/hashUtils.d.ts | 4 +
dist/ergo/hashUtils.js | 24 ++++-
src/lib/components/FileSourceCreation.svelte | 100 +++++++++++-------
src/lib/ergo/hashUtils.ts | 26 ++++-
8 files changed, 165 insertions(+), 89 deletions(-)
diff --git a/.gitignore b/.gitignore
index 37af162..a2d1d00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,4 +46,5 @@ yarn-error.log
*.apk
.metals/
.scala-build/
-.test.scala
\ No newline at end of file
+.test.scala
+.codex
\ No newline at end of file
diff --git a/LIBRARY.md b/LIBRARY.md
index 4985f83..b9673c3 100644
--- a/LIBRARY.md
+++ b/LIBRARY.md
@@ -60,6 +60,7 @@ Form for adding new file sources to the network. It supports two modes: a "free"
- `explorerUri: string` - Ergo Explorer API endpoint.
- `source_explorer_url: string` - Base URL for the source explorer (used for deep links).
- `hash?: Writable` - Optional. A Svelte writable store for the file hash.
+- `fixedHashFunctionId?: string` - Optional. Hash algorithm for the fixed anchor hash. In fixed mode the default is `blake2b256`.
- `title?: string` - Optional. Custom title for the component (default: "Add New File Source").
- `onSourceAdded?: (txId: string) => void` - Callback when source is added.
@@ -67,8 +68,10 @@ Form for adding new file sources to the network. It supports two modes: a "free"
- **Always Visible**: The current hash is always displayed at the top of the component.
- **Fixed Hash Mode** (when `hash` store has a value):
- Manual hash input and file upload fields are hidden.
+ - The anchor hash function comes from `fixedHashFunctionId` instead of the form UI.
- The "Compute hash from URL" button is hidden.
- When clicking "Add Source", the component automatically downloads the file from the URL, calculates its hash, and verifies it matches the fixed hash before proceeding.
+ - Source-entry metadata such as content hash, content hash function id, and formats remain optional.
- **Free Mode** (when `hash` store is empty or undefined):
- User can provide the hash manually.
- User can upload a local file to calculate its hash.
@@ -96,6 +99,7 @@ Form for adding new file sources to the network. It supports two modes: a "free"
{explorerUri}
{source_explorer_url}
hash={fileHashStore}
+ fixedHashFunctionId="blake2b256"
title="Add Download Link"
onSourceAdded={(tx) => console.log('Source added:', tx)}
/>
diff --git a/dist/components/FileSourceCreation.svelte b/dist/components/FileSourceCreation.svelte
index aa7ae4b..8c67c59 100644
--- a/dist/components/FileSourceCreation.svelte
+++ b/dist/components/FileSourceCreation.svelte
@@ -10,12 +10,13 @@ import { AlertTriangle, Download, Upload, Loader2 } from "lucide-svelte";
import {} from "../ergo/object";
import { downloadAndHash } from "../ergo/hashUtils";
import {} from "svelte/store";
-import { HASH_OPTIONS, validateHash } from "../ergo/hashUtils";
+import { HASH_OPTIONS, getAlgorithmLabel, normalizeHashAlgorithmId, validateHash } from "../ergo/hashUtils";
export let profile = null;
export let explorerUri;
export let source_explorer_url;
export let onSourceAdded = null;
export let hash = void 0;
+export let fixedHashFunctionId = "blake2b256";
export let hashValidationEnabled = false;
export let title = "Add New File Source";
let className = "";
@@ -23,7 +24,7 @@ export { className as class };
const hasProfile = profile !== null;
const baseClasses = "bg-card p-6 rounded-lg border";
let newFileHash = "";
-let hashFunctionId = "";
+let effectiveHashFunctionId = "";
let isAddingSource = false;
let addError = null;
let isCalculatingHash = false;
@@ -43,16 +44,11 @@ let rawFormatType = "extension";
let fileHashValidationError = null;
let contentHashValidationError = null;
let rawHashValidationError = null;
+$:
+ effectiveHashFunctionId = isHashFixed ? fixedHashFunctionId.trim() || "blake2b256" : hashSelectValue === "__custom__" ? customHashFunctionId : hashSelectValue;
$: {
- if (hashSelectValue === "__custom__") {
- hashFunctionId = customHashFunctionId;
- } else {
- hashFunctionId = hashSelectValue;
- }
-}
-$: {
- if (newFileHash.trim() && hashSelectValue && hashSelectValue !== "__custom__") {
- fileHashValidationError = validateHash(newFileHash.trim(), hashSelectValue);
+ if (newFileHash.trim() && effectiveHashFunctionId) {
+ fileHashValidationError = validateHash(newFileHash.trim(), effectiveHashFunctionId);
} else if (newFileHash.trim() && hashSelectValue === "__custom__" && customHashFunctionId) {
fileHashValidationError = validateHash(newFileHash.trim(), "__custom__");
} else {
@@ -60,8 +56,11 @@ $: {
}
}
$: {
- if (entryContentHash.trim() && hashSelectValue && hashSelectValue !== "__custom__") {
- contentHashValidationError = validateHash(entryContentHash.trim(), hashSelectValue);
+ if (entryContentHash.trim() && (entryHashFunctionId.trim() || effectiveHashFunctionId)) {
+ contentHashValidationError = validateHash(
+ entryContentHash.trim(),
+ entryHashFunctionId.trim() || effectiveHashFunctionId
+ );
} else {
contentHashValidationError = null;
}
@@ -99,9 +98,10 @@ onMount(() => {
updateHash(pFileHash);
}
if (pHashFunctionId) {
- const knownOption = HASH_OPTIONS.find((o) => o.value === pHashFunctionId);
+ const normalizedHashFunctionId = normalizeHashAlgorithmId(pHashFunctionId);
+ const knownOption = HASH_OPTIONS.find((o) => o.value === normalizedHashFunctionId);
if (knownOption && knownOption.value !== "__custom__") {
- hashSelectValue = pHashFunctionId;
+ hashSelectValue = knownOption.value;
} else {
hashSelectValue = "__custom__";
customHashFunctionId = pHashFunctionId;
@@ -135,7 +135,7 @@ async function calculateHashFromUrl() {
const url = entryUrlLink.trim();
if (!url)
return;
- const algorithmId = entryHashFunctionId || hashFunctionId;
+ const algorithmId = entryHashFunctionId || effectiveHashFunctionId;
if (!algorithmId || algorithmId === "__custom__") {
hashError = "Cannot verify: select a known hash algorithm first";
return;
@@ -162,7 +162,7 @@ async function handleFileUpload(event) {
const input = event.target;
if (!input.files || input.files.length === 0)
return;
- const algorithmId = entryHashFunctionId || hashFunctionId;
+ const algorithmId = entryHashFunctionId || effectiveHashFunctionId;
if (!algorithmId || algorithmId === "__custom__") {
hashError = "Cannot verify: select a known hash algorithm first";
return;
@@ -195,7 +195,7 @@ async function handleAddSource() {
newFileHash = currentHashValue;
}
if (hashValidationEnabled && isHashFixed && entryUrlLink.trim()) {
- const algorithmId = entryHashFunctionId || hashFunctionId;
+ const algorithmId = entryHashFunctionId || effectiveHashFunctionId;
if (!algorithmId || algorithmId === "__custom__") {
hashError = "Cannot verify: select a known hash algorithm first";
return;
@@ -236,7 +236,7 @@ async function handleAddSource() {
};
const tx = await addFileSource(
newFileHash.trim(),
- hashFunctionId.trim(),
+ effectiveHashFunctionId.trim(),
entry,
profile,
explorerUri
@@ -245,7 +245,6 @@ async function handleAddSource() {
if (!isHashFixed) {
updateHash("");
}
- hashFunctionId = "";
hashSelectValue = "";
customHashFunctionId = "";
entryHashFunctionId = "";
@@ -367,29 +366,42 @@ async function handleAddSource() {
- Security Warning: Always verify URLs before downloading.
- Malicious actors may post harmful links. The URL you provide will be
- publicly visible and immutable on the blockchain.
-
-
-
- {#if addError}
-
-
{addError}
-
- {/if}
-
-
-
-
-
- {#if fileHashValidationError}
-
{fileHashValidationError}
- {:else}
-
- This is the unique identifier for the file. Users will search by
- this hash.
-
- Security Warning: Always verify URLs before downloading.
- Malicious actors may post harmful links. The URL you provide will be
- publicly visible and immutable on the blockchain.
-
- When enabled, adding a source will download the file from the URL
- and verify its hash matches before submitting the transaction.
- This may cause issues with large files or CORS-restricted URLs.
-