@meddleware/dao-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +13 -0
- package/package.json +60 -0
- package/src/App.vue +51 -0
- package/src/DaoView.vue +64 -0
- package/src/components/AmountCell.vue +21 -0
- package/src/components/DataTable.vue +24 -0
- package/src/components/Panel.vue +12 -0
- package/src/components/ProposalRow.vue +68 -0
- package/src/components/StatusBar.vue +34 -0
- package/src/components/TabBar.vue +30 -0
- package/src/composables/useDaoEvents.ts +70 -0
- package/src/composables/useEpoch.ts +20 -0
- package/src/composables/useGates.ts +53 -0
- package/src/composables/usePlatformConfig.ts +38 -0
- package/src/composables/useProposals.ts +57 -0
- package/src/composables/useTreasury.ts +30 -0
- package/src/config.ts +28 -0
- package/src/env.d.ts +15 -0
- package/src/index.ts +1 -0
- package/src/main.ts +7 -0
- package/src/styles/qt.css +356 -0
- package/src/tabs/GovernanceTab.vue +54 -0
- package/src/tabs/HistoryTab.vue +70 -0
- package/src/tabs/OverviewTab.vue +88 -0
- package/src/tabs/ProposalsTab.vue +25 -0
- package/src/tabs/TreasuryTab.vue +78 -0
- package/src/wallet.ts +25 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +6 -0
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
<title>DAO · Meddleware</title>
|
|
7
|
+
<meta name="description" content="Meddleware DAO console — treasury overview, proposals, and on-chain governance." />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meddleware/dao-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Meddleware DAO console — treasury overview, proposals, and governance on Sui.",
|
|
5
|
+
"author": "Meddleware <dev@meddleware.co.uk>",
|
|
6
|
+
"license": "0BSD",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/meddleware-org/dao-ui.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"index.html",
|
|
15
|
+
"vite.config.ts",
|
|
16
|
+
"tsconfig.json",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "vite",
|
|
21
|
+
"build": "vue-tsc --noEmit && vite build",
|
|
22
|
+
"preview": "vite preview",
|
|
23
|
+
"type-check": "vue-tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./src/index.ts",
|
|
30
|
+
"default": "./src/index.ts"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": [
|
|
35
|
+
"*.css",
|
|
36
|
+
"*.vue"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@meddleware/design-tokens": "^0.1.2",
|
|
43
|
+
"@meddleware/ui": "^0.1.10",
|
|
44
|
+
"@meddleware/wallet-adapter": "^0.0.5",
|
|
45
|
+
"@mysten/sui": "^2.30.0",
|
|
46
|
+
"vue": "^3.5.40"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "~24.12.2",
|
|
50
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
51
|
+
"@vue/test-utils": "^2.4.6",
|
|
52
|
+
"typescript": "~6.0.0",
|
|
53
|
+
"vite": "^8.1.5",
|
|
54
|
+
"vitest": "~2.1.9",
|
|
55
|
+
"vue-tsc": "~3.3.0"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": "^22.18.0 || >=24.12.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/App.vue
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Standalone shell for the DAO SPA. The core UI lives in DaoView.vue (also exported
|
|
3
|
+
// for inline embedding in the dashboard).
|
|
4
|
+
import { AppHeader, AppFooter, ColorModeControl, useColorMode } from '@meddleware/ui'
|
|
5
|
+
import { NETWORK } from './config.js'
|
|
6
|
+
import DaoView from './DaoView.vue'
|
|
7
|
+
|
|
8
|
+
const { mode, set } = useColorMode('dark')
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div class="app">
|
|
13
|
+
<AppHeader variant="dark">
|
|
14
|
+
<template #brand>
|
|
15
|
+
<h1 class="brand-title">Meddleware DAO</h1>
|
|
16
|
+
</template>
|
|
17
|
+
<template #actions>
|
|
18
|
+
<span class="network-badge">{{ NETWORK }}</span>
|
|
19
|
+
<ColorModeControl :model-value="mode" @update:model-value="set" />
|
|
20
|
+
</template>
|
|
21
|
+
</AppHeader>
|
|
22
|
+
|
|
23
|
+
<DaoView style="flex: 1; min-height: 0" />
|
|
24
|
+
|
|
25
|
+
<AppFooter />
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<style scoped>
|
|
30
|
+
.app {
|
|
31
|
+
min-height: 100vh;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.brand-title {
|
|
37
|
+
font: inherit;
|
|
38
|
+
margin: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.network-badge {
|
|
42
|
+
font-size: 0.72rem;
|
|
43
|
+
padding: 2px 8px;
|
|
44
|
+
border-radius: 2px;
|
|
45
|
+
border: 1px solid var(--border);
|
|
46
|
+
color: var(--muted);
|
|
47
|
+
font-family: var(--font-mono);
|
|
48
|
+
text-transform: uppercase;
|
|
49
|
+
letter-spacing: 0.05em;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
package/src/DaoView.vue
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Core DAO tool UI — tab-based console with desktop-application aesthetics.
|
|
3
|
+
// Rendered standalone by App.vue and embedded in the dashboard at the default route.
|
|
4
|
+
// All chain reads are read-only; no wallet connection is required to view data.
|
|
5
|
+
import { ref, shallowRef, computed } from 'vue'
|
|
6
|
+
import TabBar from './components/TabBar.vue'
|
|
7
|
+
import StatusBar from './components/StatusBar.vue'
|
|
8
|
+
import OverviewTab from './tabs/OverviewTab.vue'
|
|
9
|
+
import TreasuryTab from './tabs/TreasuryTab.vue'
|
|
10
|
+
import ProposalsTab from './tabs/ProposalsTab.vue'
|
|
11
|
+
import GovernanceTab from './tabs/GovernanceTab.vue'
|
|
12
|
+
import HistoryTab from './tabs/HistoryTab.vue'
|
|
13
|
+
import { useDaoEvents } from './composables/useDaoEvents.js'
|
|
14
|
+
import { useEpoch } from './composables/useEpoch.js'
|
|
15
|
+
|
|
16
|
+
import type { Tab } from './components/TabBar.vue'
|
|
17
|
+
|
|
18
|
+
const TABS: Tab[] = [
|
|
19
|
+
{ id: 'overview', label: 'Overview' },
|
|
20
|
+
{ id: 'treasury', label: 'Treasury' },
|
|
21
|
+
{ id: 'proposals', label: 'Proposals' },
|
|
22
|
+
{ id: 'governance', label: 'Governance' },
|
|
23
|
+
{ id: 'history', label: 'History' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const activeTab = ref('overview')
|
|
27
|
+
|
|
28
|
+
const TAB_COMPONENTS = {
|
|
29
|
+
overview: OverviewTab,
|
|
30
|
+
treasury: TreasuryTab,
|
|
31
|
+
proposals: ProposalsTab,
|
|
32
|
+
governance: GovernanceTab,
|
|
33
|
+
history: HistoryTab,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const activeComponent = computed(() => TAB_COMPONENTS[activeTab.value as keyof typeof TAB_COMPONENTS])
|
|
37
|
+
|
|
38
|
+
const { lastRefresh, error: eventsError } = useDaoEvents(1)
|
|
39
|
+
const { epoch } = useEpoch()
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div class="dao-view">
|
|
44
|
+
<div class="dao-toolbar">
|
|
45
|
+
<button class="dao-toolbar__btn" @click="activeTab = 'overview'">🏛 Meddleware DAO</button>
|
|
46
|
+
<span class="dao-toolbar__sep" />
|
|
47
|
+
<span class="dao-muted" style="font-size: 0.75rem; padding: 0 4px">
|
|
48
|
+
access_gate · testnet
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<TabBar :tabs="TABS" v-model="activeTab" />
|
|
53
|
+
|
|
54
|
+
<div class="dao-content">
|
|
55
|
+
<component :is="activeComponent" />
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<StatusBar
|
|
59
|
+
:epoch="epoch"
|
|
60
|
+
:last-refresh="lastRefresh"
|
|
61
|
+
:error="!!eventsError"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
mist: bigint | null
|
|
4
|
+
symbol?: string
|
|
5
|
+
muted?: boolean
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
function format(mist: bigint, symbol: string): string {
|
|
9
|
+
if (mist === 0n) return `0 ${symbol}`
|
|
10
|
+
const n = Number(mist) / 1e9
|
|
11
|
+
const s = n >= 0.0001 ? n.toFixed(4) : n.toFixed(9).replace(/0+$/, '').replace(/\.$/, '')
|
|
12
|
+
return `${s} ${symbol}`
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<span class="dao-amount" :class="{ 'dao-amount--muted': muted }">
|
|
18
|
+
<template v-if="mist !== null">{{ format(mist, symbol ?? 'SUI') }}</template>
|
|
19
|
+
<template v-else>—</template>
|
|
20
|
+
</span>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{ empty?: string }>()
|
|
3
|
+
|
|
4
|
+
const slots = defineSlots<{
|
|
5
|
+
head(): unknown
|
|
6
|
+
default(): unknown
|
|
7
|
+
}>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div style="overflow-x: auto">
|
|
12
|
+
<table class="dao-table">
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<slot name="head" />
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<slot />
|
|
20
|
+
</tbody>
|
|
21
|
+
</table>
|
|
22
|
+
<p v-if="empty && !slots.default" class="dao-placeholder">{{ empty }}</p>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { Proposal } from '../composables/useProposals.js'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
proposal: Proposal
|
|
7
|
+
currentEpoch: number | null
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const pct = computed(() => {
|
|
11
|
+
if (props.proposal.targetMist === 0n) return 0
|
|
12
|
+
return Math.min(100, Number((props.proposal.contributedMist * 100n) / props.proposal.targetMist))
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const epochsLeft = computed(() => {
|
|
16
|
+
if (props.currentEpoch === null) return null
|
|
17
|
+
return props.proposal.endEpoch - props.currentEpoch
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const statusBadge = computed(() => {
|
|
21
|
+
if (props.proposal.status === 'active') return 'dao-badge--active'
|
|
22
|
+
if (props.proposal.status === 'pending') return 'dao-badge--pending'
|
|
23
|
+
return 'dao-badge--closed'
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function formatSui(mist: bigint): string {
|
|
27
|
+
const n = Number(mist) / 1e9
|
|
28
|
+
return n.toFixed(2)
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<div class="dao-panel" style="margin-bottom: 8px">
|
|
34
|
+
<div class="dao-panel__body">
|
|
35
|
+
<div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-bottom: 6px">
|
|
36
|
+
<div>
|
|
37
|
+
<span :class="['dao-badge', statusBadge]" style="margin-right: 6px">{{ proposal.status }}</span>
|
|
38
|
+
<strong style="font-size: 0.85rem">{{ proposal.title }}</strong>
|
|
39
|
+
</div>
|
|
40
|
+
<span class="dao-muted" style="white-space: nowrap; font-family: var(--font-mono); font-size: 0.75rem">
|
|
41
|
+
<template v-if="epochsLeft !== null && epochsLeft > 0">{{ epochsLeft }} epochs left</template>
|
|
42
|
+
<template v-else-if="epochsLeft !== null && epochsLeft <= 0">Expired</template>
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<p class="dao-muted" style="margin: 0 0 8px; font-size: 0.78rem">{{ proposal.description }}</p>
|
|
47
|
+
|
|
48
|
+
<div style="margin-bottom: 4px">
|
|
49
|
+
<div class="dao-progress">
|
|
50
|
+
<div class="dao-progress__fill" :style="{ width: `${pct}%` }" />
|
|
51
|
+
</div>
|
|
52
|
+
<div style="display: flex; justify-content: space-between; font-size: 0.72rem; color: var(--muted); font-family: var(--font-mono); margin-top: 2px">
|
|
53
|
+
<span>{{ formatSui(proposal.contributedMist) }} SUI raised</span>
|
|
54
|
+
<span>{{ pct }}% of {{ formatSui(proposal.targetMist) }} SUI target</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<button
|
|
59
|
+
class="dao-toolbar__btn"
|
|
60
|
+
disabled
|
|
61
|
+
title="Vault DAO governance module launching soon"
|
|
62
|
+
style="margin-top: 6px; opacity: 0.5; cursor: not-allowed"
|
|
63
|
+
>
|
|
64
|
+
Contribute
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { NETWORK } from '../config.js'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
epoch: number | null
|
|
7
|
+
lastRefresh: Date | null
|
|
8
|
+
error?: boolean
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const dotClass = computed(() => (props.error ? 'dao-statusbar__dot--error' : 'dao-statusbar__dot--ok'))
|
|
12
|
+
|
|
13
|
+
function timeAgo(d: Date): string {
|
|
14
|
+
const s = Math.floor((Date.now() - d.getTime()) / 1000)
|
|
15
|
+
if (s < 5) return 'just now'
|
|
16
|
+
if (s < 60) return `${s}s ago`
|
|
17
|
+
return `${Math.floor(s / 60)}m ago`
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<footer class="dao-statusbar">
|
|
23
|
+
<span class="dao-statusbar__item">
|
|
24
|
+
<span class="dao-statusbar__dot" :class="dotClass" />
|
|
25
|
+
{{ NETWORK.charAt(0).toUpperCase() + NETWORK.slice(1) }}
|
|
26
|
+
</span>
|
|
27
|
+
<span class="dao-statusbar__sep">│</span>
|
|
28
|
+
<span v-if="epoch !== null" class="dao-statusbar__item">Epoch {{ epoch }}</span>
|
|
29
|
+
<span v-else class="dao-statusbar__item dao-muted">Epoch —</span>
|
|
30
|
+
<span class="dao-statusbar__sep">│</span>
|
|
31
|
+
<span v-if="lastRefresh" class="dao-statusbar__item">Refreshed {{ timeAgo(lastRefresh) }}</span>
|
|
32
|
+
<span v-else class="dao-statusbar__item dao-muted">Loading…</span>
|
|
33
|
+
</footer>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
export interface Tab {
|
|
3
|
+
id: string
|
|
4
|
+
label: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
defineProps<{
|
|
8
|
+
tabs: Tab[]
|
|
9
|
+
modelValue: string
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
'update:modelValue': [id: string]
|
|
14
|
+
}>()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<nav class="dao-tabs" aria-label="DAO sections">
|
|
19
|
+
<button
|
|
20
|
+
v-for="tab in tabs"
|
|
21
|
+
:key="tab.id"
|
|
22
|
+
class="dao-tab"
|
|
23
|
+
:class="{ 'dao-tab--active': modelValue === tab.id }"
|
|
24
|
+
:aria-current="modelValue === tab.id ? 'page' : undefined"
|
|
25
|
+
@click="emit('update:modelValue', tab.id)"
|
|
26
|
+
>
|
|
27
|
+
{{ tab.label }}
|
|
28
|
+
</button>
|
|
29
|
+
</nav>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ref, onMounted } from 'vue'
|
|
2
|
+
import { getSuiClient } from '../wallet.js'
|
|
3
|
+
import { PACKAGE_ID } from '../config.js'
|
|
4
|
+
|
|
5
|
+
export interface DaoEvent {
|
|
6
|
+
type: 'AccessMinted' | 'AccessConsumed' | 'GateCreated' | 'AccessBurned'
|
|
7
|
+
txDigest: string
|
|
8
|
+
timestampMs: number
|
|
9
|
+
address?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useDaoEvents(limit = 20) {
|
|
13
|
+
const events = ref<DaoEvent[]>([])
|
|
14
|
+
const loading = ref(false)
|
|
15
|
+
const error = ref<string | null>(null)
|
|
16
|
+
const lastRefresh = ref<Date | null>(null)
|
|
17
|
+
|
|
18
|
+
async function load() {
|
|
19
|
+
if (!PACKAGE_ID) return
|
|
20
|
+
loading.value = true
|
|
21
|
+
error.value = null
|
|
22
|
+
try {
|
|
23
|
+
const client = getSuiClient()
|
|
24
|
+
const eventTypes = [
|
|
25
|
+
`${PACKAGE_ID}::access_gate::AccessMintedEvent`,
|
|
26
|
+
`${PACKAGE_ID}::access_gate::AccessConsumedEvent`,
|
|
27
|
+
`${PACKAGE_ID}::access_gate::GateCreatedEvent`,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const results = await Promise.allSettled(
|
|
31
|
+
eventTypes.map((t) =>
|
|
32
|
+
client.listEvents({ filter: { eventType: t }, limit, order: 'descending' }),
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const all: DaoEvent[] = []
|
|
37
|
+
for (const r of results) {
|
|
38
|
+
if (r.status !== 'fulfilled') continue
|
|
39
|
+
for (const e of r.value.events) {
|
|
40
|
+
const typeName = e.eventType.split('::').pop() ?? ''
|
|
41
|
+
const label =
|
|
42
|
+
typeName === 'AccessMintedEvent' ? 'AccessMinted'
|
|
43
|
+
: typeName === 'AccessConsumedEvent' ? 'AccessConsumed'
|
|
44
|
+
: typeName === 'GateCreatedEvent' ? 'GateCreated'
|
|
45
|
+
: 'AccessBurned'
|
|
46
|
+
const f = (e.json ?? {}) as Record<string, unknown>
|
|
47
|
+
all.push({
|
|
48
|
+
type: label as DaoEvent['type'],
|
|
49
|
+
txDigest: e.transactionDigest,
|
|
50
|
+
timestampMs: 0,
|
|
51
|
+
address: String(f.recipient ?? f.creator ?? f.sender ?? ''),
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Events come back in descending order per-type; stable sort across types by txDigest.
|
|
57
|
+
// timestampMs is not available from gRPC EventEntry; order is already newest-first per batch.
|
|
58
|
+
events.value = all.slice(0, limit)
|
|
59
|
+
lastRefresh.value = new Date()
|
|
60
|
+
} catch (e) {
|
|
61
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
62
|
+
} finally {
|
|
63
|
+
loading.value = false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onMounted(load)
|
|
68
|
+
|
|
69
|
+
return { events, loading, error, lastRefresh, reload: load }
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ref, onMounted } from 'vue'
|
|
2
|
+
import { getSuiClient } from '../wallet.js'
|
|
3
|
+
|
|
4
|
+
export function useEpoch() {
|
|
5
|
+
const epoch = ref<number | null>(null)
|
|
6
|
+
|
|
7
|
+
async function load() {
|
|
8
|
+
try {
|
|
9
|
+
const client = getSuiClient()
|
|
10
|
+
const res = await client.getCurrentSystemState()
|
|
11
|
+
epoch.value = Number(res.systemState.epoch)
|
|
12
|
+
} catch {
|
|
13
|
+
// non-fatal — status bar shows "Epoch —"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onMounted(load)
|
|
18
|
+
|
|
19
|
+
return { epoch, reload: load }
|
|
20
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ref, onMounted } from 'vue'
|
|
2
|
+
import { getSuiClient } from '../wallet.js'
|
|
3
|
+
import { PACKAGE_ID } from '../config.js'
|
|
4
|
+
|
|
5
|
+
export interface Gate {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
price: bigint
|
|
9
|
+
paused: boolean
|
|
10
|
+
frozen: boolean
|
|
11
|
+
txDigest: string
|
|
12
|
+
timestampMs: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useGates() {
|
|
16
|
+
const gates = ref<Gate[]>([])
|
|
17
|
+
const loading = ref(false)
|
|
18
|
+
const error = ref<string | null>(null)
|
|
19
|
+
|
|
20
|
+
async function load() {
|
|
21
|
+
if (!PACKAGE_ID) return
|
|
22
|
+
loading.value = true
|
|
23
|
+
error.value = null
|
|
24
|
+
try {
|
|
25
|
+
const client = getSuiClient()
|
|
26
|
+
const events = await client.listEvents({
|
|
27
|
+
filter: { eventType: `${PACKAGE_ID}::access_gate::GateCreatedEvent` },
|
|
28
|
+
limit: 50,
|
|
29
|
+
order: 'descending',
|
|
30
|
+
})
|
|
31
|
+
gates.value = events.events.map((e) => {
|
|
32
|
+
const f = (e.json ?? {}) as Record<string, unknown>
|
|
33
|
+
return {
|
|
34
|
+
id: String(f.gate_id ?? ''),
|
|
35
|
+
name: String(f.nft_name ?? 'Unnamed Gate'),
|
|
36
|
+
price: BigInt(String(f.price ?? '0')),
|
|
37
|
+
paused: false,
|
|
38
|
+
frozen: false,
|
|
39
|
+
txDigest: e.transactionDigest,
|
|
40
|
+
timestampMs: 0,
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
} catch (e) {
|
|
44
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
45
|
+
} finally {
|
|
46
|
+
loading.value = false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onMounted(load)
|
|
51
|
+
|
|
52
|
+
return { gates, loading, error, reload: load }
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ref, onMounted } from 'vue'
|
|
2
|
+
import { getSuiClient } from '../wallet.js'
|
|
3
|
+
import { CONFIG_ID } from '../config.js'
|
|
4
|
+
|
|
5
|
+
export interface PlatformConfig {
|
|
6
|
+
treasury: string
|
|
7
|
+
commissionBps: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function usePlatformConfig() {
|
|
11
|
+
const config = ref<PlatformConfig | null>(null)
|
|
12
|
+
const loading = ref(false)
|
|
13
|
+
const error = ref<string | null>(null)
|
|
14
|
+
|
|
15
|
+
async function load() {
|
|
16
|
+
if (!CONFIG_ID) { error.value = 'PlatformConfig ID not configured'; return }
|
|
17
|
+
loading.value = true
|
|
18
|
+
error.value = null
|
|
19
|
+
try {
|
|
20
|
+
const client = getSuiClient()
|
|
21
|
+
const res = await client.getObject({ objectId: CONFIG_ID, include: { json: true } })
|
|
22
|
+
const fields = res.object.json as Record<string, unknown> | null
|
|
23
|
+
if (!fields) throw new Error('unexpected object structure')
|
|
24
|
+
config.value = {
|
|
25
|
+
treasury: String(fields.treasury),
|
|
26
|
+
commissionBps: Number(fields.commission_bps),
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
30
|
+
} finally {
|
|
31
|
+
loading.value = false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onMounted(load)
|
|
36
|
+
|
|
37
|
+
return { config, loading, error, reload: load }
|
|
38
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Proposals are backed by mock data until vault_dao is deployed on-chain.
|
|
2
|
+
// When vault_dao ships, replace this composable with one that queries the on-chain
|
|
3
|
+
// proposal registry. The Proposal type and return shape are kept stable.
|
|
4
|
+
|
|
5
|
+
import { ref, readonly } from 'vue'
|
|
6
|
+
|
|
7
|
+
export type ProposalStatus = 'active' | 'pending' | 'closed'
|
|
8
|
+
|
|
9
|
+
export interface Proposal {
|
|
10
|
+
id: string
|
|
11
|
+
title: string
|
|
12
|
+
description: string
|
|
13
|
+
targetMist: bigint
|
|
14
|
+
contributedMist: bigint
|
|
15
|
+
endEpoch: number
|
|
16
|
+
status: ProposalStatus
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MOCK_PROPOSALS: Proposal[] = [
|
|
20
|
+
{
|
|
21
|
+
id: 'prop-001',
|
|
22
|
+
title: 'mwSUI Vault Strategy — Haedal LST Allocation',
|
|
23
|
+
description:
|
|
24
|
+
'Allocate 10% of vault AUM to haSUI (Haedal liquid-staked SUI) to diversify yield sources beyond Spring Finance sSUI. Requires at least 50 SUI in community endorsement to enact.',
|
|
25
|
+
targetMist: 50_000_000_000n,
|
|
26
|
+
contributedMist: 0n,
|
|
27
|
+
endEpoch: 620,
|
|
28
|
+
status: 'active',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'prop-002',
|
|
32
|
+
title: 'Commission Rate Reduction — 0.20% → 0.15%',
|
|
33
|
+
description:
|
|
34
|
+
'Reduce the access-gate platform commission from 20 bps to 15 bps to stay competitive with comparable NFT-gated relay services.',
|
|
35
|
+
targetMist: 25_000_000_000n,
|
|
36
|
+
contributedMist: 0n,
|
|
37
|
+
endEpoch: 640,
|
|
38
|
+
status: 'active',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'prop-003',
|
|
42
|
+
title: 'Sealed Storage Relay Integration',
|
|
43
|
+
description:
|
|
44
|
+
'Route Seal uploads through the Meddleware Walrus relay so relay tips are captured and included in the fee distribution cycle.',
|
|
45
|
+
targetMist: 100_000_000_000n,
|
|
46
|
+
contributedMist: 0n,
|
|
47
|
+
endEpoch: 680,
|
|
48
|
+
status: 'pending',
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
export function useProposals() {
|
|
53
|
+
const proposals = ref<Proposal[]>(MOCK_PROPOSALS)
|
|
54
|
+
const loading = ref(false)
|
|
55
|
+
|
|
56
|
+
return { proposals: readonly(proposals), loading: readonly(loading) }
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ref, watch } from 'vue'
|
|
2
|
+
import { getSuiClient } from '../wallet.js'
|
|
3
|
+
|
|
4
|
+
export function useTreasury(treasuryAddress: () => string | null) {
|
|
5
|
+
const balance = ref<bigint | null>(null)
|
|
6
|
+
const loading = ref(false)
|
|
7
|
+
const error = ref<string | null>(null)
|
|
8
|
+
|
|
9
|
+
async function load(address: string) {
|
|
10
|
+
loading.value = true
|
|
11
|
+
error.value = null
|
|
12
|
+
try {
|
|
13
|
+
const client = getSuiClient()
|
|
14
|
+
const res = await client.getBalance({ owner: address, coinType: '0x2::sui::SUI' })
|
|
15
|
+
balance.value = BigInt(res.balance.coinBalance)
|
|
16
|
+
} catch (e) {
|
|
17
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
18
|
+
} finally {
|
|
19
|
+
loading.value = false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
watch(
|
|
24
|
+
treasuryAddress,
|
|
25
|
+
(addr) => { if (addr) { void load(addr) } else { balance.value = null } },
|
|
26
|
+
{ immediate: true },
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return { balance, loading, error }
|
|
30
|
+
}
|