@meddleware/token-deployer-ui 0.0.7 → 0.0.9

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.
@@ -1,26 +1,25 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch } from 'vue'
2
+ // Pre-deploy terms, in the shared UiDialog. Closing by any route (Cancel, Escape, backdrop,
3
+ // close button) counts as cancel; only the "continue" action accepts — UiDialog's return value
4
+ // tells them apart.
5
+ import { UiDialog } from '@meddleware/ui'
3
6
 
4
- const props = defineProps<{ open: boolean }>()
7
+ defineProps<{ open: boolean }>()
5
8
  const emit = defineEmits<{
6
9
  (e: 'accept'): void
7
10
  (e: 'cancel'): void
8
11
  }>()
9
12
 
10
- const el = ref<HTMLDialogElement>()
11
-
12
- watch(
13
- () => props.open,
14
- (v) => (v ? el.value?.showModal() : el.value?.close()),
15
- )
13
+ function onClose(returnValue: string): void {
14
+ if (returnValue === 'accept') emit('accept')
15
+ else emit('cancel')
16
+ }
16
17
  </script>
17
18
 
18
19
  <template>
19
- <dialog ref="el" @click.self="$emit('cancel')" @cancel.prevent="$emit('cancel')">
20
- <div class="dialog-header">
21
- <h2>Before you deploy</h2>
22
- </div>
23
- <ul>
20
+ <UiDialog :open="open" title="Before you deploy" @close="onClose">
21
+ <!-- A list: each item is a discrete, parallel term (announced with an item count). -->
22
+ <ul class="terms">
24
23
  <li>
25
24
  All operations — bytecode compilation, transaction construction, and signing — take place
26
25
  entirely <strong>client-side</strong>, meaning on your computer. No code runs on our
@@ -48,34 +47,36 @@ watch(
48
47
  arising from your use of this tool.
49
48
  </li>
50
49
  </ul>
51
- <p style="margin-bottom: 1.25rem; font-size: 0.9rem; color: var(--muted)">
50
+ <p class="terms__confirm">
52
51
  By continuing, you confirm that you have read and accept these terms.
53
52
  </p>
54
- <div class="terms-actions">
55
- <button type="button" class="primary" @click="$emit('accept')">I understand — continue</button>
56
- <button type="button" @click="$emit('cancel')">Cancel</button>
57
- </div>
58
- </dialog>
53
+ <template #actions="{ close }">
54
+ <button type="button" class="primary" @click="close('accept')">
55
+ I understand — continue
56
+ </button>
57
+ <button type="button" @click="close('cancel')">Cancel</button>
58
+ </template>
59
+ </UiDialog>
59
60
  </template>
60
61
 
61
62
  <style scoped>
62
- ul {
63
+ .terms {
63
64
  padding-left: 1.25rem;
64
- margin: 0 0 1rem;
65
+ margin: 0;
65
66
  line-height: 1.6;
66
67
  }
67
68
 
68
- ul li {
69
+ .terms li {
69
70
  margin-bottom: 0.6rem;
70
71
  }
71
72
 
72
- ul li:last-child {
73
+ .terms li:last-child {
73
74
  margin-bottom: 0;
74
75
  }
75
76
 
76
- .terms-actions {
77
- display: flex;
78
- gap: 0.75rem;
79
- justify-content: flex-end;
77
+ .terms__confirm {
78
+ margin: 0;
79
+ font-size: 0.9rem;
80
+ color: var(--muted);
80
81
  }
81
82
  </style>
@@ -6,7 +6,16 @@ import ReviewPanel from './ReviewPanel.vue'
6
6
  import DeployProgress from './DeployProgress.vue'
7
7
  import ResultPanel from './ResultPanel.vue'
8
8
  import TermsDialog from './TermsDialog.vue'
9
- import { UiNotice, UiStepper, type StepperStep } from '@meddleware/ui'
9
+ import MyTokens from './MyTokens.vue'
10
+ import {
11
+ AppTabNav,
12
+ UiNotice,
13
+ UiStepper,
14
+ UiTabPanel,
15
+ UiToolIntro,
16
+ type AppTab,
17
+ type StepperStep,
18
+ } from '@meddleware/ui'
10
19
  import { emptyForm, toTokenConfig } from '../lib/form.js'
11
20
  import { validateForm } from '../lib/validation.js'
12
21
  import type { FormErrors } from '../lib/validation.js'
@@ -32,6 +41,14 @@ const CONFIG_STEPS: ConfigFormStep[] = ['identity', 'token', 'settings']
32
41
  const { account } = useWallet()
33
42
  const { network } = useNetwork()
34
43
 
44
+ // Top-level tabs — Deploy (the wizard) and My Tokens (coins this wallet has deployed), so the
45
+ // layout matches the other Sui tools (intro → tabs → content).
46
+ const TABS: AppTab[] = [
47
+ { id: 'deploy', label: 'Deploy' },
48
+ { id: 'my-tokens', label: 'My Tokens' },
49
+ ]
50
+ const activeTab = ref('deploy')
51
+
35
52
  const form = reactive(emptyForm())
36
53
  const step = ref<Step>('identity')
37
54
  const deployStep = ref<DeployStep | null>(null)
@@ -55,10 +72,14 @@ function stepHasErrors(keys: (keyof FormErrors)[]): boolean {
55
72
  const canProceed = computed((): boolean => {
56
73
  if (!connected.value) return false
57
74
  switch (step.value) {
58
- case 'identity': return !stepHasErrors(IDENTITY_KEYS)
59
- case 'token': return !stepHasErrors(TOKEN_KEYS)
60
- case 'settings': return !stepHasErrors(SETTINGS_KEYS)
61
- default: return true
75
+ case 'identity':
76
+ return !stepHasErrors(IDENTITY_KEYS)
77
+ case 'token':
78
+ return !stepHasErrors(TOKEN_KEYS)
79
+ case 'settings':
80
+ return !stepHasErrors(SETTINGS_KEYS)
81
+ default:
82
+ return true
62
83
  }
63
84
  })
64
85
 
@@ -90,18 +111,36 @@ function onStepperBack(i: number): void {
90
111
 
91
112
  function goNext(): void {
92
113
  if (!canProceed.value) return
93
- if (step.value === 'identity') { step.value = 'token'; return }
94
- if (step.value === 'token') { step.value = 'settings'; return }
114
+ if (step.value === 'identity') {
115
+ step.value = 'token'
116
+ return
117
+ }
118
+ if (step.value === 'token') {
119
+ step.value = 'settings'
120
+ return
121
+ }
95
122
  if (step.value === 'settings') {
96
- if (!termsAccepted.value) { showTerms.value = true; return }
123
+ if (!termsAccepted.value) {
124
+ showTerms.value = true
125
+ return
126
+ }
97
127
  step.value = 'review'
98
128
  }
99
129
  }
100
130
 
101
131
  function goBack(): void {
102
- if (step.value === 'token') { step.value = 'identity'; return }
103
- if (step.value === 'settings') { step.value = 'token'; return }
104
- if (step.value === 'review') { step.value = 'settings'; return }
132
+ if (step.value === 'token') {
133
+ step.value = 'identity'
134
+ return
135
+ }
136
+ if (step.value === 'settings') {
137
+ step.value = 'token'
138
+ return
139
+ }
140
+ if (step.value === 'review') {
141
+ step.value = 'settings'
142
+ return
143
+ }
105
144
  }
106
145
 
107
146
  function onTermsAccept(): void {
@@ -117,7 +156,10 @@ function onTermsCancel(): void {
117
156
 
118
157
  async function confirmDeploy(): Promise<void> {
119
158
  const sender = account.value?.address
120
- if (!sender) { deployError.value = 'Connect a wallet first.'; return }
159
+ if (!sender) {
160
+ deployError.value = 'Connect a wallet first.'
161
+ return
162
+ }
121
163
  deployError.value = null
122
164
  step.value = 'deploying'
123
165
  deployStep.value = null
@@ -149,34 +191,61 @@ function restart(): void {
149
191
  </script>
150
192
 
151
193
  <template>
194
+ <UiToolIntro
195
+ >Deploy your own Sui coin — your wallet signs and pays; no intermediaries ({{
196
+ network
197
+ }}).</UiToolIntro
198
+ >
199
+
200
+ <AppTabNav
201
+ v-model="activeTab"
202
+ :tabs="TABS"
203
+ id-prefix="token-deployer"
204
+ aria-label="Sections"
205
+ class="deployer-tabs"
206
+ />
207
+
152
208
  <WalletGuard message="Connect a Sui wallet to deploy your token.">
153
- <UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
154
- <ConfigForm
155
- v-if="step === 'identity' || step === 'token' || step === 'settings'"
156
- :form="form"
157
- :errors="errors"
158
- :can-proceed="canProceed"
159
- :network="network"
160
- :connected="connected"
161
- :form-step="formStep"
162
- @next="goNext"
163
- @back="goBack"
164
- />
165
- <ReviewPanel
166
- v-if="step === 'review'"
167
- :config="config"
168
- :network="network"
169
- @back="goBack"
170
- @confirm="confirmDeploy"
171
- />
172
- <DeployProgress v-if="step === 'deploying'" :step="deployStep" />
173
- <ResultPanel
174
- v-if="step === 'done' && result"
175
- :result="result"
176
- :config="config"
177
- @restart="restart"
178
- />
179
- <UiNotice v-if="deployError" type="error">{{ deployError }}</UiNotice>
180
- <TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
209
+ <UiTabPanel id-prefix="token-deployer" :tab="activeTab">
210
+ <template v-if="activeTab === 'deploy'">
211
+ <UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
212
+ <ConfigForm
213
+ v-if="step === 'identity' || step === 'token' || step === 'settings'"
214
+ :form="form"
215
+ :errors="errors"
216
+ :can-proceed="canProceed"
217
+ :network="network"
218
+ :connected="connected"
219
+ :form-step="formStep"
220
+ @next="goNext"
221
+ @back="goBack"
222
+ />
223
+ <ReviewPanel
224
+ v-if="step === 'review'"
225
+ :config="config"
226
+ :network="network"
227
+ @back="goBack"
228
+ @confirm="confirmDeploy"
229
+ />
230
+ <DeployProgress v-if="step === 'deploying'" :step="deployStep" />
231
+ <ResultPanel
232
+ v-if="step === 'done' && result"
233
+ :result="result"
234
+ :config="config"
235
+ @restart="restart"
236
+ />
237
+ <UiNotice v-if="deployError" type="error">{{ deployError }}</UiNotice>
238
+ </template>
239
+
240
+ <MyTokens v-else :owner="account?.address ?? null" :network="network" />
241
+ </UiTabPanel>
181
242
  </WalletGuard>
243
+
244
+ <TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
182
245
  </template>
246
+
247
+ <style scoped>
248
+ .deployer-tabs {
249
+ margin: 0 0 1rem;
250
+ }
251
+ </style>
@@ -3,12 +3,12 @@ import { computed, ref, watch } from 'vue'
3
3
  import { useWallet, useNetwork } from '@meddleware/wallet-adapter'
4
4
  import { SELECTABLE_NETWORKS } from '../config.js'
5
5
  import type { Network } from '../lib/types.js'
6
- import { UiSelect, UiNotice } from '@meddleware/ui'
6
+ import { UiDialog, UiFormField, UiNotice, UiSelect } from '@meddleware/ui'
7
7
 
8
8
  const { wallets, currentWallet, account, connecting, error, connect, disconnect } = useWallet()
9
9
  const { network, setNetwork } = useNetwork()
10
10
 
11
- const dialogEl = ref<HTMLDialogElement>()
11
+ const dialogOpen = ref(false)
12
12
 
13
13
  const shortAddress = computed(() => {
14
14
  const a = account.value?.address
@@ -20,88 +20,77 @@ const networkModel = computed({
20
20
  set: (v: string) => setNetwork(v as Network),
21
21
  })
22
22
 
23
- function openDialog(): void {
24
- dialogEl.value?.showModal()
25
- }
26
-
27
- function closeDialog(): void {
28
- dialogEl.value?.close()
29
- }
30
-
31
- watch(() => account.value, (acc) => {
32
- if (acc) {
33
- closeDialog()
34
- }
35
- })
23
+ // Close the connect dialog once a wallet connects.
24
+ watch(
25
+ () => account.value,
26
+ (acc) => {
27
+ if (acc) dialogOpen.value = false
28
+ },
29
+ )
36
30
  </script>
37
31
 
38
32
  <template>
39
- <div class="card" role="region" aria-label="Wallet and network">
33
+ <section class="card wallet-bar" aria-label="Wallet and network">
40
34
  <template v-if="account">
41
- <div class="row" style="justify-content: space-between">
42
- <div class="row">
43
- <label for="network-select" style="margin: 0">Network</label>
44
- <UiSelect id="network-select" v-model="networkModel">
45
- <option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
46
- </UiSelect>
47
- </div>
48
-
49
- <div class="row">
50
- <span class="badge" aria-label="Connected wallet">
51
- {{ currentWallet?.name }} · <span class="mono">{{ shortAddress }}</span>
52
- </span>
53
- <button type="button" @click="disconnect">Disconnect</button>
54
- </div>
55
- </div>
56
- </template>
57
- <template v-else>
58
- <div class="row">
59
- <div v-if="!wallets.length" style="width: 100%">
60
- <p class="hint" style="margin: 0">
61
- No Sui wallet detected. Install a Sui wallet extension to continue.
62
- </p>
63
- </div>
64
- <button v-else type="button" class="primary" @click="openDialog">
65
- Connect Wallet
66
- </button>
67
- </div>
35
+ <label class="row wallet-bar__network">
36
+ Network
37
+ <UiSelect v-model="networkModel">
38
+ <option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
39
+ </UiSelect>
40
+ </label>
41
+ <p class="row wallet-bar__account">
42
+ <span class="badge">
43
+ <span class="mw-visually-hidden">Connected wallet:</span>
44
+ {{ currentWallet?.name }} · <span class="mono">{{ shortAddress }}</span>
45
+ </span>
46
+ <button type="button" @click="disconnect">Disconnect</button>
47
+ </p>
68
48
  </template>
49
+ <p v-else-if="!wallets.length" class="hint wallet-bar__none">
50
+ No Sui wallet detected. Install a Sui wallet extension to continue.
51
+ </p>
52
+ <button v-else type="button" class="primary" @click="dialogOpen = true">Connect Wallet</button>
69
53
 
70
- <UiNotice v-if="error" type="error" style="margin-top: 0.75rem">{{ error }}</UiNotice>
71
- </div>
54
+ <UiNotice v-if="error" type="error" class="wallet-bar__error">{{ error }}</UiNotice>
55
+ </section>
72
56
 
73
- <dialog ref="dialogEl" aria-labelledby="wallet-dialog-title" @click.self="closeDialog">
74
- <div class="dialog-header">
75
- <h2 id="wallet-dialog-title">Connect Wallet</h2>
76
- <button
77
- type="button"
78
- aria-label="Close"
79
- style="background: none; border: none; font-size: 1.5rem; padding: 0; cursor: pointer; color: var(--text)"
80
- @click="closeDialog"
81
- >
82
- ×
83
- </button>
84
- </div>
57
+ <UiDialog v-model:open="dialogOpen" title="Connect Wallet">
58
+ <UiFormField id="dialog-network-select" label="Network">
59
+ <template #default="{ attrs }">
60
+ <UiSelect v-bind="attrs" v-model="networkModel">
61
+ <option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
62
+ </UiSelect>
63
+ </template>
64
+ </UiFormField>
85
65
 
86
- <div>
87
- <label for="dialog-network-select">Network</label>
88
- <UiSelect id="dialog-network-select" v-model="networkModel">
89
- <option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
90
- </UiSelect>
91
- </div>
92
-
93
- <div class="wallet-list">
94
- <button
95
- v-for="w in wallets"
96
- :key="w.name"
97
- type="button"
98
- class="primary"
99
- :disabled="connecting"
100
- @click="connect(w)"
101
- >
102
- <img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
103
- {{ w.name }}
104
- </button>
105
- </div>
106
- </dialog>
66
+ <menu class="wallet-list">
67
+ <li v-for="w in wallets" :key="w.name">
68
+ <button type="button" class="primary" :disabled="connecting" @click="connect(w)">
69
+ <img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
70
+ {{ w.name }}
71
+ </button>
72
+ </li>
73
+ </menu>
74
+ </UiDialog>
107
75
  </template>
76
+
77
+ <style scoped>
78
+ .wallet-bar {
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ align-items: center;
82
+ justify-content: space-between;
83
+ gap: 0.75rem;
84
+ }
85
+ .wallet-bar__network,
86
+ .wallet-bar__account,
87
+ .wallet-bar__none {
88
+ margin: 0;
89
+ }
90
+ .wallet-bar__none {
91
+ width: 100%;
92
+ }
93
+ .wallet-bar__error {
94
+ flex-basis: 100%;
95
+ }
96
+ </style>
@@ -46,9 +46,8 @@ async function load(): Promise<void> {
46
46
  blobs.value = []
47
47
  hasLoaded.value = false
48
48
  try {
49
- const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } = await import(
50
- '../lib/walrus.js'
51
- )
49
+ const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } =
50
+ await import('../lib/walrus.js')
52
51
  const suiClient = getReadClient(props.network as Network)
53
52
  const client = createWalrusClient(props.network)
54
53
  const raw = await fetchOwnedWalrusBlobs(suiClient, client, addr)
@@ -75,9 +74,13 @@ function selectBlob(item: BlobItem): void {
75
74
  type="text"
76
75
  placeholder="0x… or leave blank for your wallet"
77
76
  aria-label="Wallet address to browse"
78
- style="flex: 1"
77
+ class="blob-browser__address"
79
78
  />
80
- <button type="button" :disabled="loading || (!connected && !addressInput.trim())" @click="load">
79
+ <button
80
+ type="button"
81
+ :disabled="loading || (!connected && !addressInput.trim())"
82
+ @click="load"
83
+ >
81
84
  <span v-if="loading" class="spinner" aria-hidden="true"></span>
82
85
  Load
83
86
  </button>
@@ -90,26 +93,26 @@ function selectBlob(item: BlobItem): void {
90
93
  No Walrus blobs found at this address.
91
94
  </p>
92
95
 
93
- <div v-if="blobs.length > 0" class="blob-grid" role="listbox" aria-label="Walrus blobs">
94
- <button
95
- v-for="b in blobs"
96
- :key="b.objectId"
97
- type="button"
98
- class="blob-thumb"
99
- :class="{ selected: selected === b.blobId }"
100
- role="option"
101
- :aria-selected="selected === b.blobId"
102
- @click="selectBlob(b)"
103
- >
104
- <img
105
- :src="b.url"
106
- :alt="`Blob ${b.blobId.slice(0, 8)}…`"
107
- loading="lazy"
108
- @error="(e) => (e.target as HTMLElement).parentElement?.classList.add('no-img')"
109
- />
110
- <span class="blob-meta">{{ formatSize(b.size) }}</span>
111
- </button>
112
- </div>
96
+ <!-- A list of toggle buttons (native keyboard behaviour); aria-pressed marks the chosen blob. -->
97
+ <ul v-if="blobs.length > 0" role="list" class="blob-grid" aria-label="Walrus blobs">
98
+ <li v-for="b in blobs" :key="b.objectId">
99
+ <button
100
+ type="button"
101
+ class="blob-thumb"
102
+ :class="{ selected: selected === b.blobId }"
103
+ :aria-pressed="selected === b.blobId"
104
+ @click="selectBlob(b)"
105
+ >
106
+ <img
107
+ :src="b.url"
108
+ :alt="`Blob ${b.blobId.slice(0, 8)}…`"
109
+ loading="lazy"
110
+ @error="(e) => (e.target as HTMLElement).parentElement?.classList.add('no-img')"
111
+ />
112
+ <span class="blob-meta">{{ formatSize(b.size) }}</span>
113
+ </button>
114
+ </li>
115
+ </ul>
113
116
  </div>
114
117
  </template>
115
118
 
@@ -127,7 +130,12 @@ function selectBlob(item: BlobItem): void {
127
130
  margin-top: 0.25rem;
128
131
  }
129
132
 
133
+ .blob-browser__address {
134
+ flex: 1;
135
+ }
136
+
130
137
  .blob-thumb {
138
+ width: 100%;
131
139
  display: flex;
132
140
  flex-direction: column;
133
141
  align-items: center;
@@ -53,7 +53,12 @@ export function useAccessGate(
53
53
  checking.value = true
54
54
  error.value = null
55
55
  try {
56
- const nfts = await fetchAccessNfts(deps.getClient(network), address, gate.nftType, gate.gateId)
56
+ const nfts = await fetchAccessNfts(
57
+ deps.getClient(network),
58
+ address,
59
+ gate.nftType,
60
+ gate.gateId,
61
+ )
57
62
  hasAccess.value = nfts.length > 0
58
63
  usesRemaining.value = nfts.length ? nfts[0].usesRemaining : null
59
64
  nftId.value = nfts.length ? nfts[0].objectId : null
@@ -1,7 +1,7 @@
1
1
  // Thin wrapper around the shared `@meddleware/walrus-relay-ui` composable: injects
2
2
  // this app's per-network relay host pair so the relay-selection + tip-estimation logic
3
3
  // lives in exactly one place (DRY). The return shape is unchanged, so consumers
4
- // (`WalrusIconUpload.vue`, `IconPicker.vue`) and tests are unaffected.
4
+ // (`IconPicker.vue`) and tests are unaffected.
5
5
  import { useWalrusRelay as useSharedWalrusRelay } from '@meddleware/walrus-relay'
6
6
  import type { RelayAccessOptions, RelayOption } from '@meddleware/walrus-relay'
7
7
  import { WALRUS_RELAY_HOSTS, PUBLIC_WALRUS_RELAY_HOSTS } from '../config.js'
@@ -103,7 +103,7 @@ export function buildFinalizeTransaction(args: BuildFinalizeArgs): Transaction {
103
103
  target: `${SUI_FRAMEWORK}::coin_registry::finalize_registration`,
104
104
  typeArguments: [coinType],
105
105
  arguments: [
106
- tx.object('0xc'), // &mut CoinRegistry (shared object at well-known address)
106
+ tx.object('0xc'), // &mut CoinRegistry (shared object at well-known address)
107
107
  tx.receivingRef(args.currencyRef), // Receiving<Currency<T>> — pending object at @0xc
108
108
  ],
109
109
  })
@@ -11,7 +11,12 @@
11
11
  import { zipSync, strToU8 } from 'fflate'
12
12
  import templateFiles from '../template-src/files.json'
13
13
  import type { PublishResult, TokenConfig } from './types.js'
14
- import { SAFE_TEXT, deriveStructName, validateIdentifier, hasAllowedIconScheme } from './validation.js'
14
+ import {
15
+ SAFE_TEXT,
16
+ deriveStructName,
17
+ validateIdentifier,
18
+ hasAllowedIconScheme,
19
+ } from './validation.js'
15
20
 
16
21
  const files = templateFiles as Record<string, string>
17
22
 
@@ -0,0 +1,45 @@
1
+ // Discover coins the connected wallet has deployed, by listing the TreasuryCap<T> objects it
2
+ // owns (the deploy sends the TreasuryCap to the sender). Read-only; mirrors access-gate's
3
+ // AdminCap-based "My Gates" discovery.
4
+ import { getReadClient } from './readClient.js'
5
+ import type { Network } from './types.js'
6
+
7
+ /** One deployed coin the wallet controls (holds the TreasuryCap for). */
8
+ export interface DeployedToken {
9
+ /** Full coin type, e.g. `0x<pkg>::mytoken::MYTOKEN`. */
10
+ coinType: string
11
+ /** The published package id (first segment of the coin type). */
12
+ packageId: string
13
+ /** Object id of the owned `TreasuryCap<T>`. */
14
+ treasuryCapId: string
15
+ /** The one-time-witness struct name (last segment) — a compact display label. */
16
+ label: string
17
+ }
18
+
19
+ /** List the coins `owner` has deployed on `network` (via owned `TreasuryCap<T>` objects). */
20
+ export async function listMyTokens(owner: string, network: Network): Promise<DeployedToken[]> {
21
+ const client = getReadClient(network)
22
+ // The StructType filter without type params matches every TreasuryCap<T> instantiation.
23
+ const res = await client.getOwnedObjects({
24
+ owner,
25
+ filter: { StructType: '0x2::coin::TreasuryCap' },
26
+ options: { showType: true },
27
+ })
28
+ const seen = new Set<string>()
29
+ const out: DeployedToken[] = []
30
+ for (const item of res.data ?? []) {
31
+ const data = item.data
32
+ const m = String(data?.type ?? '').match(/^0x2::coin::TreasuryCap<(.+)>$/)
33
+ if (!m || !data) continue
34
+ const coinType = m[1]
35
+ if (seen.has(coinType)) continue
36
+ seen.add(coinType)
37
+ out.push({
38
+ coinType,
39
+ packageId: coinType.split('::')[0] ?? '',
40
+ treasuryCapId: data.objectId,
41
+ label: coinType.split('::').pop() ?? coinType,
42
+ })
43
+ }
44
+ return out
45
+ }