@meddleware/walrus-storage-ui 0.0.0-rename.0 → 0.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/LICENSE +14 -0
- package/README.md +73 -0
- package/index.html +14 -0
- package/package.json +95 -1
- package/src/App.vue +56 -0
- package/src/blob-groups.ts +68 -0
- package/src/components/MyBlobs.vue +442 -0
- package/src/components/WalrusStorageView.vue +376 -0
- package/src/composables/useExtendQuotes.ts +69 -0
- package/src/composables/useOwnedBlobs.ts +91 -0
- package/src/config.ts +87 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +10 -0
- package/src/main.ts +16 -0
- package/src/wallet.ts +47 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +13 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@meddleware/walrus-ui` are documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/). Before 0.2.0, breaking changes ship as patch releases.
|
|
6
|
+
|
|
7
|
+
## [0.1.62] - 2026-10-24
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Renamed from `walrus-ui` to `walrus-storage-ui` (workspace naming convention, `docs/architecture/ADR-0003-naming.md`). No code change beyond names, paths, hostnames and dependency names; the history below keeps the old names.
|
|
12
|
+
|
|
13
|
+
## [0.1.61] - 2026-10-22
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Dependencies: walrus-client ^0.0.30 (flow passes the real challenge expiry; nft-gate-client 0.0.18 rules), walrus-relay ^0.1.33, access-gate-client ^0.0.10 (stale indexer fallback); image base static-server 0.1.11 (Go 1.26.10 standard library, CVE-2026-111943)
|
|
18
|
+
|
|
19
|
+
## [0.1.60] - 2026-10-19
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Image rebuilt without the `VITE_UPLOAD_RELAY_MAX_TIP_MIST` repository variable (deleted; it still held the old 0.05 SUI ceiling and overrode the 0.005 SUI default introduced in the previous release)
|
|
24
|
+
|
|
25
|
+
## [0.1.59] - 2026-10-19
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- The default relay tip ceiling (`VITE_UPLOAD_RELAY_MAX_TIP_MIST` unset) is now `5_000_000` MIST (0.005 SUI), down
|
|
30
|
+
from 0.05 SUI: about 8× the worst-case tip of the re-priced operator relay (`100_000` MIST + 1 MIST per encoded
|
|
31
|
+
KiB; workspace cost model, decision D57). The repository variable `VITE_UPLOAD_RELAY_MAX_TIP_MIST`, when set,
|
|
32
|
+
still wins.
|
|
33
|
+
|
|
34
|
+
## [0.1.58] - 2026-10-18
|
|
35
|
+
|
|
36
|
+
### Changed (breaking, pre-v0.2)
|
|
37
|
+
|
|
38
|
+
- **Adopts `@meddleware/walrus-relay` 0.1.31.** `useAccessGate` is given the network and a client getter
|
|
39
|
+
that serves the active network only; the access gate is still built by `relayGateConfig`, so it names the
|
|
40
|
+
published `access_gate` deployment. The purchase prompt (`AccessGateCta`) now shows the price, payees,
|
|
41
|
+
network and pass kind read from the chain, with a "Read the price again" button when they cannot be read.
|
|
42
|
+
- **`VITE_ACCESS_GATE_PRICE_MIST_TESTNET` is removed** (code, `.env.example`, Dockerfile build argument,
|
|
43
|
+
release workflow and README). The price is read from the gate on the chain; a build that still sets the
|
|
44
|
+
variable ignores it.
|
|
45
|
+
- `WalrusUpload` gets `sessionKey` (`<network>:<address>`), so a change of account or network discards the
|
|
46
|
+
chosen file and any held retry; the upload approvals hint follows the held pass (`singleUse`).
|
|
47
|
+
- Dependencies: `@meddleware/walrus-client` ^0.0.28, `@meddleware/walrus-relay` ^0.1.31,
|
|
48
|
+
`@meddleware/design-tokens` ^0.1.10, `@meddleware/access-gate-client` ^0.0.9. The unused
|
|
49
|
+
`@mysten/wallet-standard` dependency is dropped.
|
|
50
|
+
- Image base: `static-server` 0.1.8 (`sha256:e32cf42b…4000`).
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **A slower owned-blob load no longer overwrites a newer one** after a wallet-account or network switch,
|
|
55
|
+
a disconnect, or a switch back to a list that is already held (a stale result, error or `loading` flag is
|
|
56
|
+
dropped); a cleared list no longer reads as loaded for the previous account.
|
|
57
|
+
- **Extend shows its cost before the wallet opens.** The seeded +10 epochs is priced on the first press
|
|
58
|
+
(the button reads "Show cost"), the amount and `≈ WAL` cost are shown, and the wallet opens on the next
|
|
59
|
+
press (`Extend`). An amount that cannot be priced says "price unavailable".
|
|
60
|
+
- **My Blobs text meets AA contrast in both themes.** It read `--mw-color-*` custom properties that
|
|
61
|
+
design-tokens does not define, so literal fallbacks (`#888`, `#c00`, `#ddd`, `#f90`) ignored theme and
|
|
62
|
+
season; it now uses `--muted`, `--danger`, `--border`, `--warning` (tint only) and `--accent`.
|
|
63
|
+
A test fails if a component reads a custom property design-tokens does not define.
|
|
64
|
+
|
|
65
|
+
### Security
|
|
66
|
+
|
|
67
|
+
- `.env*` is git-ignored (except `.env.example`).
|
|
68
|
+
- `SECURITY.md` invariant 1 names the real wiring of the tip ceiling (`maxTipMist` to `runBlobUpload`).
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- `CHANGELOG.md` (it was listed in the package `files` without existing).
|
|
73
|
+
- Tests: stale-load guard (`useOwnedBlobs.stale`), extend quotes, the custom-property check, and the
|
|
74
|
+
gate-config/`useAccessGate` pairing.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
BSD Zero Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MeddleWare
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @meddleware/walrus-storage-ui
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
|
|
5
|
+
A Vue 3 SPA for uploading and managing blobs on [Walrus](https://walrus.xyz) decentralised
|
|
6
|
+
storage on Sui. Supports NFT-gated operator relays (commission-enforced on-chain via
|
|
7
|
+
`access_gate`) with automatic fallback to the public Mysten relay.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Connect any Sui wallet (wallet-standard)
|
|
12
|
+
- Select between operator relay (supports the app) and public relay (free)
|
|
13
|
+
- Upload blobs through an NFT-gated or open relay
|
|
14
|
+
- View and extend your owned Walrus blobs
|
|
15
|
+
- Two-approval upload flow: register tx → upload → certify tx
|
|
16
|
+
|
|
17
|
+
## Local development
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
npm run dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> `@meddleware/walrus-storage-kit` is resolved from the npm registry. For local development against an
|
|
25
|
+
> unpublished library, use `npm link @meddleware/walrus-storage-kit` or an `overrides` entry pointing at
|
|
26
|
+
> a local checkout.
|
|
27
|
+
|
|
28
|
+
## Environment variables
|
|
29
|
+
|
|
30
|
+
All `VITE_*` vars are baked into the static bundle at build time.
|
|
31
|
+
|
|
32
|
+
| Variable | Default | Description |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `VITE_NETWORK` | `testnet` | Network the standalone build selects (`testnet` or `mainnet`); embedded, the host's selector rules |
|
|
35
|
+
| `VITE_WALRUS_RELAY_TESTNET` | Public Mysten relay | Operator relay URL for testnet |
|
|
36
|
+
| `VITE_WALRUS_RELAY_MAINNET` | Public Mysten relay | Operator relay URL for mainnet |
|
|
37
|
+
| `VITE_ACCESS_GATE_ID_TESTNET` | — | Gate object ID (testnet; unset = no gate) |
|
|
38
|
+
| `VITE_ACCESS_GATE_SOULBOUND_TESTNET` | `false` | `true` if NFTs are soulbound |
|
|
39
|
+
| `VITE_ACCESS_GATE_ID_MAINNET` | — | Gate object ID (mainnet) |
|
|
40
|
+
| `VITE_UPLOAD_RELAY_MAX_TIP_MIST` | `5000000` | Max relay tip cap in MIST (0.005 SUI) |
|
|
41
|
+
|
|
42
|
+
The `access_gate` package and `PlatformConfig` come from the published deployment
|
|
43
|
+
(`relayGateConfig` in `@meddleware/walrus-storage-kit`) — operators only configure the values listed
|
|
44
|
+
above.
|
|
45
|
+
|
|
46
|
+
## Docker build
|
|
47
|
+
|
|
48
|
+
The Docker context is this repo root. `@meddleware/walrus-storage-kit` resolves from the npm registry,
|
|
49
|
+
so the library must be published before building the image.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
docker build \
|
|
53
|
+
--build-arg VITE_NETWORK=testnet \
|
|
54
|
+
--build-arg VITE_WALRUS_RELAY_TESTNET=https://sui-walrus-storage-relay.example.com \
|
|
55
|
+
--build-arg VITE_ACCESS_GATE_ID_TESTNET=0x... \
|
|
56
|
+
-t walrus-storage-ui:latest .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Architecture
|
|
60
|
+
|
|
61
|
+
Thin SPA — no accounting logic, no chain state derivation. The app:
|
|
62
|
+
1. Reads config from env vars
|
|
63
|
+
2. Wires `@meddleware/walrus-storage-kit` composables (`useAccessGate`, `useWalrusRelay`) to the
|
|
64
|
+
connected wallet
|
|
65
|
+
3. Passes a `performUpload` callback to `WalrusUpload`, which holds the `@mysten/walrus` client
|
|
66
|
+
and the upload flow logic
|
|
67
|
+
4. Displays results
|
|
68
|
+
|
|
69
|
+
All financial truth is on-chain. This UI is an orchestration shell.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
0BSD
|
package/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
7
|
+
<title>Walrus Uploader · MeddleWare</title>
|
|
8
|
+
<meta name="description" content="Upload blobs to Walrus decentralised storage on Sui through the MeddleWare relay. Client-side; your wallet signs and pays." />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app"></div>
|
|
12
|
+
<script type="module" src="/src/main.ts"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1 +1,95 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@meddleware/walrus-storage-ui",
|
|
3
|
+
"version": "0.1.62",
|
|
4
|
+
"homepage": "https://docs.meddleware.co.uk/blockchain/sui/walrus-storage/",
|
|
5
|
+
"description": "Standalone Vue 3 SPA for uploading blobs to Walrus decentralised storage and managing owned blobs — wallet-connected, NFT-gated relay support.",
|
|
6
|
+
"author": "Meddleware <dev@meddleware.co.uk>",
|
|
7
|
+
"license": "0BSD",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/meddleware-org/walrus-storage-ui.git"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"sideEffects": [
|
|
14
|
+
"*.css",
|
|
15
|
+
"*.vue"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./src/index.ts",
|
|
20
|
+
"default": "./src/index.ts"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"index.html",
|
|
27
|
+
"vite.config.ts",
|
|
28
|
+
"tsconfig.json",
|
|
29
|
+
"CHANGELOG.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"dev": "vite",
|
|
33
|
+
"build": "vue-tsc --noEmit && vite build",
|
|
34
|
+
"preview": "vite preview",
|
|
35
|
+
"type-check": "vue-tsc --noEmit",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"test:e2e": "playwright test",
|
|
39
|
+
"lint:css": "stylelint \"src/**/*.{css,vue}\"",
|
|
40
|
+
"lint:js": "eslint .",
|
|
41
|
+
"lint": "npm run lint:css && npm run lint:js && npm run lint:html",
|
|
42
|
+
"lint:html": "html-validate \"src/**/*.vue\"",
|
|
43
|
+
"licenses": "node scripts/third-party-licenses.mjs",
|
|
44
|
+
"check:licenses": "node scripts/third-party-licenses.mjs --check"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@meddleware/design-tokens": "^0.1.10",
|
|
51
|
+
"@meddleware/ui": "^0.1.31",
|
|
52
|
+
"@meddleware/walrus-storage-client": "^0.0.31",
|
|
53
|
+
"@meddleware/walrus-storage-kit": "^0.1.34",
|
|
54
|
+
"@mysten/sui": "^2.33.2",
|
|
55
|
+
"@mysten/walrus": "~1.2.32",
|
|
56
|
+
"@mysten/walrus-wasm": "~0.3.1",
|
|
57
|
+
"vue": "^3.5.43"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@meddleware/wallet-adapter": ">=0.0.12 <0.2.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@meddleware/access-gate-client": "^0.0.10",
|
|
64
|
+
"@meddleware/eslint-config": "^0.0.3",
|
|
65
|
+
"@meddleware/wallet-adapter": "^0.0.17",
|
|
66
|
+
"@playwright/test": "~1.64.1",
|
|
67
|
+
"@types/node": "~26.6.5",
|
|
68
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
69
|
+
"@vue/eslint-config-typescript": "^14.9.0",
|
|
70
|
+
"autoprefixer": "^10.4.20",
|
|
71
|
+
"eslint": "^10.11.0",
|
|
72
|
+
"eslint-plugin-vue": "^10.11.1",
|
|
73
|
+
"eslint-plugin-vuejs-accessibility": "^2.6.0",
|
|
74
|
+
"html-validate": "^11.16.1",
|
|
75
|
+
"html-validate-vue": "^9.3.1",
|
|
76
|
+
"jiti": "^2.7.0",
|
|
77
|
+
"postcss": "^8.4.49",
|
|
78
|
+
"postcss-html": "^2.0.0",
|
|
79
|
+
"stylelint": "^17.15.0",
|
|
80
|
+
"stylelint-config-standard": "^40.0.0",
|
|
81
|
+
"typescript": "~6.0.0",
|
|
82
|
+
"vite": "^8.3.0",
|
|
83
|
+
"vitest": "~5.0.2",
|
|
84
|
+
"vue-eslint-parser": "^10.4.1",
|
|
85
|
+
"vue-tsc": "~3.3.0"
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": "^22.18.0 || >=24.12.0"
|
|
89
|
+
},
|
|
90
|
+
"browserslist": [
|
|
91
|
+
"> 0.5%",
|
|
92
|
+
"last 2 versions",
|
|
93
|
+
"not dead"
|
|
94
|
+
]
|
|
95
|
+
}
|
package/src/App.vue
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Standalone shell for the Walrus SPA: app header + footer wrapping the core tool view.
|
|
3
|
+
// The core UI lives in WalrusStorageView.vue (also exported for inline embedding in the dashboard).
|
|
4
|
+
import { AppHeader, AppFooter, ColorModeControl, useColorMode } from '@meddleware/ui'
|
|
5
|
+
import { TipConfigBadge } from '@meddleware/walrus-storage-kit'
|
|
6
|
+
import { walrusNetwork, OPERATOR_RELAY_HOSTS } from './config.js'
|
|
7
|
+
import WalrusStorageView from './components/WalrusStorageView.vue'
|
|
8
|
+
|
|
9
|
+
const { mode, set } = useColorMode('dark')
|
|
10
|
+
const DOCS_URL = import.meta.env.VITE_DOCS_URL || 'https://docs.meddleware.co.uk/blockchain/sui/walrus-storage/'
|
|
11
|
+
const DEV_URL = import.meta.env.VITE_DEV_URL || 'https://dev.meddleware.co.uk/sui/walrus-storage/'
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="app">
|
|
16
|
+
<AppHeader variant="dark">
|
|
17
|
+
<template #brand>
|
|
18
|
+
<h1 class="brand-title">Walrus Assets</h1>
|
|
19
|
+
</template>
|
|
20
|
+
<template #actions>
|
|
21
|
+
<TipConfigBadge v-if="walrusNetwork" :host="OPERATOR_RELAY_HOSTS[walrusNetwork]" />
|
|
22
|
+
<ColorModeControl :model-value="mode" @update:model-value="set" />
|
|
23
|
+
</template>
|
|
24
|
+
</AppHeader>
|
|
25
|
+
|
|
26
|
+
<main class="app__content">
|
|
27
|
+
<WalrusStorageView />
|
|
28
|
+
</main>
|
|
29
|
+
|
|
30
|
+
<AppFooter :docs-url="DOCS_URL" :dev-url="DEV_URL" />
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.app {
|
|
36
|
+
min-height: 100vh;
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Centre the tool at the shared tool-content width when running standalone. The dashboard
|
|
42
|
+
supplies its own width container, so this lives in the shell, not WalrusStorageView. */
|
|
43
|
+
.app__content {
|
|
44
|
+
flex: 1;
|
|
45
|
+
width: 100%;
|
|
46
|
+
max-width: var(--mw-tool-content-max);
|
|
47
|
+
margin: 0 auto;
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
padding: 1.5rem 1.25rem 4rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.brand-title {
|
|
53
|
+
font: inherit;
|
|
54
|
+
margin: 0;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Group a wallet's owned Walrus blobs by `blobId` for display. Re-uploading the same content creates
|
|
2
|
+
// multiple independent Blob objects (each its own storage reservation); grouping collapses those into
|
|
3
|
+
// one row so My Blobs matches how Walruscan thinks per-blobId, while still exposing each copy.
|
|
4
|
+
import type { OwnedBlob } from '@meddleware/walrus-storage-client'
|
|
5
|
+
|
|
6
|
+
export interface BlobGroup {
|
|
7
|
+
/** Shared Walrus blob id. */
|
|
8
|
+
blobId: string
|
|
9
|
+
/** Size in bytes (identical across copies of the same content). */
|
|
10
|
+
size: number
|
|
11
|
+
/** Furthest storage end epoch across copies — the effective availability of this content. */
|
|
12
|
+
maxEndEpoch: number
|
|
13
|
+
/** True if any copy is certified (i.e. the content is currently available on-chain). */
|
|
14
|
+
anyCertified: boolean
|
|
15
|
+
/** The copy that defines availability (max end epoch); the default target for Extend. */
|
|
16
|
+
representative: OwnedBlob
|
|
17
|
+
/** All underlying Blob objects for this blobId, furthest-expiry first. */
|
|
18
|
+
copies: OwnedBlob[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Collapse owned blobs into per-blobId groups, each sorted by end epoch (furthest first). */
|
|
22
|
+
export function groupBlobs(blobs: OwnedBlob[]): BlobGroup[] {
|
|
23
|
+
const byId = new Map<string, OwnedBlob[]>()
|
|
24
|
+
for (const b of blobs) {
|
|
25
|
+
const list = byId.get(b.blobId)
|
|
26
|
+
if (list) list.push(b)
|
|
27
|
+
else byId.set(b.blobId, [b])
|
|
28
|
+
}
|
|
29
|
+
const groups: BlobGroup[] = []
|
|
30
|
+
for (const [blobId, copies] of byId) {
|
|
31
|
+
const sorted = [...copies].sort((a, b) => b.endEpoch - a.endEpoch)
|
|
32
|
+
const representative = sorted[0]
|
|
33
|
+
groups.push({
|
|
34
|
+
blobId,
|
|
35
|
+
size: representative.size,
|
|
36
|
+
maxEndEpoch: representative.endEpoch,
|
|
37
|
+
anyCertified: sorted.some((b) => b.certified),
|
|
38
|
+
representative,
|
|
39
|
+
copies: sorted,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
// Most-expiring-soonest groups first (matches the pre-grouping sort), so attention lands on them.
|
|
43
|
+
return groups.sort((a, b) => a.maxEndEpoch - b.maxEndEpoch)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Epochs of storage left before expiry (clamped at 0). */
|
|
47
|
+
export function epochsLeft(endEpoch: number, currentEpoch: number): number {
|
|
48
|
+
return Math.max(0, endEpoch - currentEpoch)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Human label for a blob's remaining lifetime, transport-independent (no fabricated day estimate). */
|
|
52
|
+
export function expiryLabel(endEpoch: number, currentEpoch: number): string {
|
|
53
|
+
const left = epochsLeft(endEpoch, currentEpoch)
|
|
54
|
+
if (left <= 0) return `epoch ${endEpoch} · expired`
|
|
55
|
+
return `epoch ${endEpoch} · ${left} epoch${left === 1 ? '' : 's'} left`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Epochs that can still be added to a blob before hitting `max_epochs_ahead` (a blob's end epoch
|
|
60
|
+
* cannot exceed `currentEpoch + maxReservation`). Zero means it's already at max lifetime.
|
|
61
|
+
*/
|
|
62
|
+
export function maxExtendableEpochs(
|
|
63
|
+
endEpoch: number,
|
|
64
|
+
currentEpoch: number,
|
|
65
|
+
maxReservation: number,
|
|
66
|
+
): number {
|
|
67
|
+
return Math.max(0, currentEpoch + maxReservation - endEpoch)
|
|
68
|
+
}
|