@meddleware/dao-ui 0.1.3 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meddleware/dao-ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "Meddleware DAO console — treasury overview, proposals, and governance on Sui.",
5
5
  "author": "Meddleware <dev@meddleware.co.uk>",
6
6
  "license": "0BSD",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@meddleware/design-tokens": "^0.1.2",
43
- "@meddleware/ui": "^0.1.10",
43
+ "@meddleware/ui": "^0.1.11",
44
44
  "@meddleware/wallet-adapter": "^0.0.5",
45
45
  "@mysten/sui": "^2.30.0",
46
46
  "vue": "^3.5.40"
@@ -53,8 +53,14 @@ export function useDaoEvents(limit = 20) {
53
53
  }
54
54
  }
55
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.
56
+ // Sort globally by checkpoint descending so the merged list is newest-first across all types.
57
+ all.sort((a, b) => {
58
+ const ca = BigInt(a.checkpoint ?? '0')
59
+ const cb = BigInt(b.checkpoint ?? '0')
60
+ if (cb > ca) return 1
61
+ if (cb < ca) return -1
62
+ return 0
63
+ })
58
64
  events.value = all.slice(0, limit)
59
65
  lastRefresh.value = new Date()
60
66
  } catch (e) {
@@ -1,6 +1,6 @@
1
1
  import { ref, onMounted } from 'vue'
2
2
  import { getSuiClient } from '../wallet.js'
3
- import { PACKAGE_ID } from '../config.js'
3
+ import { PACKAGE_ID, CONFIG_ID } from '../config.js'
4
4
 
5
5
  export interface Gate {
6
6
  id: string
@@ -8,8 +8,7 @@ export interface Gate {
8
8
  price: bigint
9
9
  paused: boolean
10
10
  frozen: boolean
11
- txDigest: string
12
- timestampMs: number
11
+ checkpoint: string | null
13
12
  }
14
13
 
15
14
  export function useGates() {
@@ -18,28 +17,54 @@ export function useGates() {
18
17
  const error = ref<string | null>(null)
19
18
 
20
19
  async function load() {
21
- if (!PACKAGE_ID) return
20
+ if (!PACKAGE_ID || !CONFIG_ID) return
22
21
  loading.value = true
23
22
  error.value = null
24
23
  try {
25
24
  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
- }
25
+
26
+ // Get treasury address from PlatformConfig so we know whose AdminCaps to look for.
27
+ // This is resilient to event pruning (the GateCreatedEvent is pruned on testnet
28
+ // after ~3 months, making event-based discovery unreliable).
29
+ const configRes = await client.getObject({ objectId: CONFIG_ID, include: { json: true } })
30
+ const configFields = configRes.object?.json as Record<string, unknown> | null
31
+ const treasury = configFields?.treasury as string | undefined
32
+ if (!treasury) {
33
+ gates.value = []
34
+ return
35
+ }
36
+
37
+ // List AdminCap objects owned by treasury — one per gate the treasury controls.
38
+ const { objects } = await client.listOwnedObjects({
39
+ owner: treasury,
40
+ type: `${PACKAGE_ID}::access_gate::AdminCap`,
41
+ include: { json: true },
42
42
  })
43
+
44
+ // Fetch each Gate shared object referenced by the AdminCap's gate_id.
45
+ const gateList = await Promise.all(
46
+ (objects ?? []).map(async (o: any): Promise<Gate | null> => {
47
+ const capFields = o?.json ?? o?.object?.json
48
+ const gateId = String(capFields?.gate_id ?? '')
49
+ if (!gateId || gateId === 'undefined') return null
50
+ try {
51
+ const gateRes = await client.getObject({ objectId: gateId, include: { json: true } })
52
+ const f = gateRes.object?.json as Record<string, unknown> | null
53
+ if (!f) return null
54
+ return {
55
+ id: gateId,
56
+ name: String(f.nft_name ?? 'Unnamed Gate'),
57
+ price: BigInt(String(f.price_mist ?? '0')),
58
+ paused: Boolean(f.paused),
59
+ frozen: Boolean(f.frozen),
60
+ checkpoint: null,
61
+ }
62
+ } catch {
63
+ return null
64
+ }
65
+ }),
66
+ )
67
+ gates.value = gateList.filter((g): g is Gate => g !== null)
43
68
  } catch (e) {
44
69
  error.value = e instanceof Error ? e.message : String(e)
45
70
  } finally {
package/src/styles/qt.css CHANGED
@@ -322,6 +322,36 @@
322
322
  font-size: 0.72rem;
323
323
  }
324
324
 
325
+ /* ── Info tooltip marker ─────────────────────────────────────────────────── */
326
+
327
+ .dao-info {
328
+ display: inline-flex;
329
+ align-items: center;
330
+ justify-content: center;
331
+ width: 13px;
332
+ height: 13px;
333
+ border-radius: 50%;
334
+ border: 1px solid var(--muted);
335
+ color: var(--muted);
336
+ font-size: 0.58rem;
337
+ font-weight: 700;
338
+ font-style: normal;
339
+ cursor: help;
340
+ vertical-align: middle;
341
+ margin-left: 3px;
342
+ text-decoration: none;
343
+ }
344
+
345
+ /* ── Pagination ──────────────────────────────────────────────────────────── */
346
+
347
+ .dao-pagination {
348
+ display: flex;
349
+ align-items: center;
350
+ gap: 10px;
351
+ justify-content: center;
352
+ margin-top: 10px;
353
+ }
354
+
325
355
  /* ── Misc ────────────────────────────────────────────────────────────────── */
326
356
 
327
357
  .dao-mono {
@@ -1,19 +1,52 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
2
+ import { ref, watch, computed } from 'vue'
3
3
  import Panel from '../components/Panel.vue'
4
4
  import { usePlatformConfig } from '../composables/usePlatformConfig.js'
5
+ import { useWallet, getSuiClient } from '../wallet.js'
6
+ import { PACKAGE_ID } from '../config.js'
7
+ import { CopyableAddress, ExplorerLink, suiExplorerUrl } from '@meddleware/ui'
8
+ import { NETWORK } from '../config.js'
5
9
 
6
10
  const { config } = usePlatformConfig()
11
+ const { account } = useWallet()
7
12
 
8
13
  const commissionPct = computed(() =>
9
14
  config.value ? (config.value.commissionBps / 100).toFixed(2) + '%' : '—',
10
15
  )
16
+
17
+ type CapState = 'idle' | 'checking' | 'found' | 'not-found' | 'error'
18
+ const capState = ref<CapState>('idle')
19
+
20
+ async function checkCaps(address: string) {
21
+ if (!PACKAGE_ID) { capState.value = 'not-found'; return }
22
+ capState.value = 'checking'
23
+ try {
24
+ const client = getSuiClient()
25
+ const res = await client.listOwnedObjects({
26
+ owner: address,
27
+ type: `${PACKAGE_ID}::access_gate::PlatformAdminCap`,
28
+ limit: 1,
29
+ })
30
+ capState.value = res.objects.length > 0 ? 'found' : 'not-found'
31
+ } catch {
32
+ capState.value = 'error'
33
+ }
34
+ }
35
+
36
+ watch(
37
+ () => account.value?.address,
38
+ (addr) => {
39
+ if (addr) void checkCaps(addr)
40
+ else capState.value = 'idle'
41
+ },
42
+ { immediate: true },
43
+ )
11
44
  </script>
12
45
 
13
46
  <template>
14
47
  <div style="display: flex; flex-direction: column; gap: 10px">
15
- <Panel title="On-Chain Parameters (access_gate)">
16
- <div class="dao-stat-grid" style="max-width: 420px">
48
+ <Panel title="Platform Parameters">
49
+ <div class="dao-stat-grid" style="max-width: 480px">
17
50
  <span class="dao-stat-grid__label">Commission rate</span>
18
51
  <span class="dao-stat-grid__value dao-mono">{{ commissionPct }}</span>
19
52
 
@@ -21,34 +54,61 @@ const commissionPct = computed(() =>
21
54
  <span class="dao-stat-grid__value dao-mono">10.00% (1000 bps — on-chain hard cap)</span>
22
55
 
23
56
  <span class="dao-stat-grid__label">Treasury address</span>
24
- <span
25
- class="dao-stat-grid__value dao-mono"
26
- style="font-size: 0.72rem; text-align: left; word-break: break-all"
27
- >
28
- {{ config?.treasury ?? '—' }}
57
+ <span class="dao-stat-grid__value" style="text-align: left">
58
+ <CopyableAddress v-if="config?.treasury" :address="config.treasury">
59
+ <ExplorerLink :href="suiExplorerUrl('account', config.treasury, NETWORK)" :value="config.treasury" />
60
+ </CopyableAddress>
61
+ <span v-else class="dao-mono" style="font-size: 0.7rem">—</span>
29
62
  </span>
30
63
  </div>
31
64
 
32
65
  <p class="dao-muted" style="margin: 10px 0 0; font-size: 0.72rem">
33
66
  Parameters are governed by <code>PlatformAdminCap</code> on-chain.
34
- Commission is split on every NFT purchase: <code>commission_bps / 10000 × price</code>
35
- routes to treasury; the remainder goes to the gate operator.
36
- </p>
37
- </Panel>
38
-
39
- <Panel title="Vault DAO Parameters">
40
- <p class="dao-muted" style="margin: 0; font-size: 0.78rem">
41
- DAO-governed vault parameters (skim fee, buyback ratio, allocation caps, fee recipients)
42
- will appear here when <code>vault_config</code> and <code>vault_dao</code> are deployed.
67
+ Commission is charged on every NFT access purchase:
68
+ <code>commission_bps / 10000 × price</code> routes to treasury;
69
+ the remainder goes to the gate operator.
43
70
  </p>
44
71
  </Panel>
45
72
 
46
73
  <Panel title="Admin Actions">
47
- <p class="dao-muted" style="margin: 0; font-size: 0.78rem">
48
- Privileged actions (update commission, change treasury, pause gates) require the
49
- <code>PlatformAdminCap</code> or <code>DaoAdminCap</code>.
50
- Connect a wallet holding the relevant capability to enable these controls.
51
- </p>
74
+ <!-- Not connected -->
75
+ <template v-if="!account">
76
+ <p class="dao-muted" style="margin: 0; font-size: 0.78rem">
77
+ Connect a wallet via the header to manage platform settings.
78
+ </p>
79
+ </template>
80
+
81
+ <!-- Connected, checking -->
82
+ <template v-else-if="capState === 'checking'">
83
+ <p class="dao-muted" style="margin: 0; font-size: 0.78rem">
84
+ Checking capabilities for {{ account.address.slice(0, 10) }}…
85
+ </p>
86
+ </template>
87
+
88
+ <!-- Connected, has PlatformAdminCap -->
89
+ <template v-else-if="capState === 'found'">
90
+ <p class="dao-muted" style="margin: 0 0 8px; font-size: 0.78rem">
91
+ <span style="color: var(--ok)">●</span>
92
+ <code>PlatformAdminCap</code> detected. Admin controls will appear here when implemented.
93
+ </p>
94
+ </template>
95
+
96
+ <!-- Connected, no cap -->
97
+ <template v-else-if="capState === 'not-found'">
98
+ <p class="dao-muted" style="margin: 0; font-size: 0.78rem">
99
+ <span>●</span>
100
+ Connected: {{ account.address.slice(0, 10) }}…{{ account.address.slice(-6) }}<br>
101
+ This wallet does not hold <code>PlatformAdminCap</code> or <code>DaoAdminCap</code>.
102
+ Privileged actions are not available.
103
+ </p>
104
+ </template>
105
+
106
+ <!-- Error -->
107
+ <template v-else-if="capState === 'error'">
108
+ <p class="dao-muted" style="margin: 0; font-size: 0.78rem">
109
+ Unable to verify capabilities. Check your connection and try again.
110
+ </p>
111
+ </template>
52
112
  </Panel>
53
113
  </div>
54
114
  </template>
@@ -1,9 +1,23 @@
1
1
  <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
2
3
  import DataTable from '../components/DataTable.vue'
3
4
  import { useDaoEvents } from '../composables/useDaoEvents.js'
5
+ import { CopyableAddress, ExplorerLink, suiExplorerUrl } from '@meddleware/ui'
6
+ import { NETWORK } from '../config.js'
4
7
  import type { DaoEvent } from '../composables/useDaoEvents.js'
5
8
 
6
- const { events, loading, error, reload } = useDaoEvents(50)
9
+ const { events, loading, error, reload } = useDaoEvents(200)
10
+
11
+ const PAGE_SIZE = 20
12
+ const page = ref(1)
13
+
14
+ const totalPages = computed(() => Math.max(1, Math.ceil(events.value.length / PAGE_SIZE)))
15
+ const pageEvents = computed(() =>
16
+ events.value.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE),
17
+ )
18
+
19
+ function prevPage() { if (page.value > 1) page.value-- }
20
+ function nextPage() { if (page.value < totalPages.value) page.value++ }
7
21
 
8
22
  const eventLabel: Record<DaoEvent['type'], string> = {
9
23
  AccessMinted: 'Access Sold',
@@ -12,18 +26,6 @@ const eventLabel: Record<DaoEvent['type'], string> = {
12
26
  AccessBurned: 'Access Burned',
13
27
  }
14
28
 
15
- function shortCkpt(ckpt: string | null): string {
16
- return ckpt ? `ckpt ${ckpt}` : '—'
17
- }
18
-
19
- function shortAddr(addr: string | undefined): string {
20
- if (!addr || addr === 'undefined') return '—'
21
- return addr.slice(0, 10) + '…' + addr.slice(-6)
22
- }
23
-
24
- function explorerUrl(digest: string): string {
25
- return `https://suiscan.xyz/testnet/tx/${digest}`
26
- }
27
29
  </script>
28
30
 
29
31
  <template>
@@ -37,14 +39,23 @@ function explorerUrl(digest: string): string {
37
39
 
38
40
  <p v-if="error" class="dao-muted">{{ error }}</p>
39
41
 
40
- <DataTable v-if="events.length || loading" :empty="'No events'">
42
+ <DataTable v-if="pageEvents.length || loading" :empty="'No events'">
41
43
  <template #head>
42
44
  <th>Type</th>
43
- <th>Address</th>
44
- <th>Checkpoint</th>
45
- <th>Tx</th>
45
+ <th>
46
+ Address
47
+ <abbr class="dao-info" title="The wallet address associated with this event (recipient, creator, or initiator depending on event type). Click to view in explorer; click the copy icon to copy the full address.">?</abbr>
48
+ </th>
49
+ <th>
50
+ Block
51
+ <abbr class="dao-info" title="Sui checkpoint sequence number — equivalent to a block height in other blockchains. Each checkpoint finalizes a batch of transactions; higher numbers are more recent.">?</abbr>
52
+ </th>
53
+ <th>
54
+ Transaction
55
+ <abbr class="dao-info" title="Transaction digest — a unique identifier for the transaction that emitted this event. Click to view in explorer; click the copy icon to copy the full digest.">?</abbr>
56
+ </th>
46
57
  </template>
47
- <tr v-for="ev in events" :key="ev.txDigest">
58
+ <tr v-for="ev in pageEvents" :key="ev.txDigest">
48
59
  <td>
49
60
  <span
50
61
  class="dao-badge"
@@ -55,15 +66,28 @@ function explorerUrl(digest: string): string {
55
66
  }"
56
67
  >{{ eventLabel[ev.type] }}</span>
57
68
  </td>
58
- <td class="dao-mono" style="font-size: 0.72rem">{{ shortAddr(ev.address) }}</td>
59
- <td class="dao-mono" style="font-size: 0.72rem">{{ shortCkpt(ev.checkpoint) }}</td>
69
+ <td class="dao-mono" style="font-size: 0.72rem; white-space: nowrap">
70
+ <CopyableAddress v-if="ev.address && ev.address !== 'undefined'" :address="ev.address">
71
+ <ExplorerLink :href="suiExplorerUrl('account', ev.address, NETWORK)" :value="ev.address" />
72
+ </CopyableAddress>
73
+ <span v-else>—</span>
74
+ </td>
60
75
  <td class="dao-mono" style="font-size: 0.72rem">
61
- <a :href="explorerUrl(ev.txDigest)" target="_blank" rel="noopener" style="color: var(--accent)">
62
- {{ ev.txDigest.slice(0, 8) }}…
63
- </a>
76
+ {{ ev.checkpoint ? '#' + ev.checkpoint : '—' }}
77
+ </td>
78
+ <td class="dao-mono" style="font-size: 0.72rem; white-space: nowrap">
79
+ <CopyableAddress :address="ev.txDigest" label="Copy transaction">
80
+ <ExplorerLink :href="suiExplorerUrl('txblock', ev.txDigest, NETWORK)" :value="ev.txDigest" />
81
+ </CopyableAddress>
64
82
  </td>
65
83
  </tr>
66
84
  </DataTable>
67
85
  <p v-else-if="!loading" class="dao-placeholder">No events found.</p>
86
+
87
+ <div v-if="totalPages > 1" class="dao-pagination">
88
+ <button class="dao-toolbar__btn" :disabled="page === 1" @click="prevPage">← Prev</button>
89
+ <span class="dao-muted" style="font-size: 0.78rem">Page {{ page }} / {{ totalPages }}</span>
90
+ <button class="dao-toolbar__btn" :disabled="page === totalPages" @click="nextPage">Next →</button>
91
+ </div>
68
92
  </div>
69
93
  </template>
@@ -6,12 +6,14 @@ import { usePlatformConfig } from '../composables/usePlatformConfig.js'
6
6
  import { useTreasury } from '../composables/useTreasury.js'
7
7
  import { useDaoEvents } from '../composables/useDaoEvents.js'
8
8
  import { useGates } from '../composables/useGates.js'
9
+ import { CopyableAddress, ExplorerLink, suiExplorerUrl } from '@meddleware/ui'
10
+ import { NETWORK } from '../config.js'
9
11
  import type { DaoEvent } from '../composables/useDaoEvents.js'
10
12
 
11
13
  const { config } = usePlatformConfig()
12
14
  const { balance } = useTreasury(() => config.value?.treasury ?? null)
13
15
  const { events, loading: eventsLoading } = useDaoEvents(12)
14
- const { gates } = useGates()
16
+ const { gates, loading: gatesLoading } = useGates()
15
17
 
16
18
  const eventLabel: Record<DaoEvent['type'], string> = {
17
19
  AccessMinted: 'Access sold',
@@ -41,12 +43,11 @@ const accessesConsumed = computed(() => events.value.filter((e) => e.type === 'A
41
43
  <span class="dao-stat-grid__value dao-mono">{{ commissionPct }}</span>
42
44
 
43
45
  <span class="dao-stat-grid__label">Treasury address</span>
44
- <span
45
- class="dao-stat-grid__value dao-mono"
46
- style="font-size: 0.7rem; word-break: break-all; text-align: left"
47
- :title="config?.treasury"
48
- >
49
- {{ config?.treasury ? config.treasury.slice(0, 10) + '…' + config.treasury.slice(-6) : '—' }}
46
+ <span class="dao-stat-grid__value" style="text-align: left">
47
+ <CopyableAddress v-if="config?.treasury" :address="config.treasury">
48
+ <ExplorerLink :href="suiExplorerUrl('account', config.treasury, NETWORK)" :value="config.treasury" />
49
+ </CopyableAddress>
50
+ <span v-else class="dao-mono" style="font-size: 0.7rem">—</span>
50
51
  </span>
51
52
  </div>
52
53
  </Panel>
@@ -54,7 +55,7 @@ const accessesConsumed = computed(() => events.value.filter((e) => e.type === 'A
54
55
  <Panel title="Platform Activity">
55
56
  <div class="dao-stat-grid">
56
57
  <span class="dao-stat-grid__label">Active gates</span>
57
- <span class="dao-stat-grid__value dao-mono">{{ gates.length || '—' }}</span>
58
+ <span class="dao-stat-grid__value dao-mono">{{ gatesLoading ? '…' : gates.length }}</span>
58
59
 
59
60
  <span class="dao-stat-grid__label">Accesses minted</span>
60
61
  <span class="dao-stat-grid__value dao-mono">{{ accessesMinted || '—' }}</span>
@@ -6,6 +6,8 @@ import DataTable from '../components/DataTable.vue'
6
6
  import { usePlatformConfig } from '../composables/usePlatformConfig.js'
7
7
  import { useTreasury } from '../composables/useTreasury.js'
8
8
  import { useGates } from '../composables/useGates.js'
9
+ import { CopyableAddress, ExplorerLink, suiExplorerUrl } from '@meddleware/ui'
10
+ import { NETWORK } from '../config.js'
9
11
 
10
12
  const { config, loading: cfgLoading, error: cfgErr } = usePlatformConfig()
11
13
  const { balance } = useTreasury(() => config.value?.treasury ?? null)
@@ -19,15 +21,11 @@ function formatPrice(mist: bigint): string {
19
21
  const n = Number(mist) / 1e9
20
22
  return n >= 0.0001 ? n.toFixed(4) + ' SUI' : n.toFixed(9).replace(/0+$/, '') + ' SUI'
21
23
  }
22
-
23
- function shortDate(ms: number): string {
24
- return ms ? new Date(ms).toISOString().slice(0, 10) : '—'
25
- }
26
24
  </script>
27
25
 
28
26
  <template>
29
27
  <div style="display: flex; flex-direction: column; gap: 10px">
30
- <Panel title="Commission Configuration">
28
+ <Panel title="Treasury">
31
29
  <p v-if="cfgLoading" class="dao-muted">Loading…</p>
32
30
  <p v-else-if="cfgErr" class="dao-muted">{{ cfgErr }}</p>
33
31
  <template v-else-if="config">
@@ -35,40 +33,42 @@ function shortDate(ms: number): string {
35
33
  <span class="dao-stat-grid__label">Commission rate</span>
36
34
  <span class="dao-stat-grid__value dao-mono">{{ commissionPct }} ({{ config.commissionBps }} bps)</span>
37
35
 
38
- <span class="dao-stat-grid__label">Treasury address</span>
39
- <span class="dao-stat-grid__value dao-mono" style="font-size: 0.72rem; text-align: left; word-break: break-all">
40
- {{ config.treasury }}
41
- </span>
42
-
43
36
  <span class="dao-stat-grid__label">Current balance</span>
44
37
  <AmountCell class="dao-stat-grid__value" :mist="balance" />
38
+
39
+ <span class="dao-stat-grid__label">Treasury address</span>
40
+ <span class="dao-stat-grid__value" style="text-align: left">
41
+ <CopyableAddress :address="config.treasury">
42
+ <ExplorerLink :href="suiExplorerUrl('account', config.treasury, NETWORK)" :value="config.treasury" />
43
+ </CopyableAddress>
44
+ </span>
45
45
  </div>
46
46
  </template>
47
47
  <p v-else class="dao-muted">PlatformConfig not loaded.</p>
48
48
  </Panel>
49
49
 
50
- <Panel title="Fee Distribution">
51
- <p class="dao-muted" style="margin: 0; font-size: 0.78rem">
52
- Fee distributor data (buyback/burn ratios, distribution history) will appear here when
53
- <code>vault_fee_distributor</code> is deployed.
54
- </p>
55
- </Panel>
56
-
57
50
  <Panel title="Community Gates">
58
51
  <p v-if="gatesLoading" class="dao-muted">Loading gates…</p>
59
52
  <DataTable v-else-if="gates.length" :empty="'No gates found'">
60
53
  <template #head>
61
54
  <th>Gate name</th>
62
55
  <th>Price</th>
63
- <th>Created</th>
56
+ <th>
57
+ Block
58
+ <abbr class="dao-info" title="Sui checkpoint sequence number at which this gate was created. Equivalent to a block height in other blockchains.">?</abbr>
59
+ </th>
64
60
  <th>Object ID</th>
65
61
  </template>
66
62
  <tr v-for="gate in gates" :key="gate.id">
67
63
  <td>{{ gate.name }}</td>
68
64
  <td class="dao-amount">{{ formatPrice(gate.price) }}</td>
69
- <td class="dao-mono" style="font-size: 0.72rem">{{ shortDate(gate.timestampMs) }}</td>
70
- <td class="dao-mono" style="font-size: 0.7rem">
71
- {{ gate.id.slice(0, 10) }}…{{ gate.id.slice(-6) }}
65
+ <td class="dao-mono" style="font-size: 0.72rem">
66
+ {{ gate.checkpoint ? '#' + gate.checkpoint : '—' }}
67
+ </td>
68
+ <td class="dao-mono" style="font-size: 0.7rem; white-space: nowrap">
69
+ <CopyableAddress :address="gate.id" label="Copy object ID">
70
+ <ExplorerLink :href="suiExplorerUrl('object', gate.id, NETWORK)" :value="gate.id" />
71
+ </CopyableAddress>
72
72
  </td>
73
73
  </tr>
74
74
  </DataTable>