@meddleware/token-deployer-ui 0.0.1
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/.claude/move-language-reference/SKILL.md +445 -0
- package/.claude/skills/move-language-reference/SKILL.md +445 -0
- package/.claude/skills/sui-move-bootcamp/SKILL.md +128 -0
- package/.claude/sui-move-bootcamp/SKILL.md +128 -0
- package/.editorconfig +8 -0
- package/.env.example +75 -0
- package/.env.production +7 -0
- package/.gitattributes +1 -0
- package/.github/workflows/ci.yml +34 -0
- package/.github/workflows/token-deployer-e2e.yml +64 -0
- package/.oxfmtrc.json +5 -0
- package/.oxlintrc.json +10 -0
- package/.vscode/extensions.json +9 -0
- package/AGENTS.md +39 -0
- package/CLAUDE.md +159 -0
- package/LICENSE +121 -0
- package/README.md +234 -0
- package/env.d.ts +13 -0
- package/index.html +13 -0
- package/package.json +67 -0
- package/playwright.config.ts +30 -0
- package/public/CNAME +1 -0
- package/public/_headers +12 -0
- package/public/_redirects +1 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +5 -0
- package/public/og-image.png +0 -0
- package/public/robots.txt +3 -0
- package/public/sitemap.xml +8 -0
- package/scripts/e2e-browser.mjs +15 -0
- package/scripts/e2e-deploy.mjs +222 -0
- package/scripts/e2e-localnet.mjs +65 -0
- package/scripts/e2e-walrus-browser.mjs +115 -0
- package/scripts/gen-og-image.mjs +31 -0
- package/scripts/publish-localnet.mjs +55 -0
- package/scripts/regen-template.sh +45 -0
- package/scripts/sync-template-src.sh +36 -0
- package/scripts/verify-bytecode.mjs +49 -0
- package/src/App.vue +41 -0
- package/src/__tests__/App.spec.js +11 -0
- package/src/components/AppNotice.vue +15 -0
- package/src/components/ColorModeControl.vue +81 -0
- package/src/components/ConfigForm.vue +192 -0
- package/src/components/DeployProgress.vue +46 -0
- package/src/components/FieldHint.vue +79 -0
- package/src/components/FormField.vue +28 -0
- package/src/components/FormSelect.vue +43 -0
- package/src/components/GithubPush.vue +100 -0
- package/src/components/IconPicker.vue +410 -0
- package/src/components/LicensePicker.vue +182 -0
- package/src/components/ResultPanel.vue +88 -0
- package/src/components/ReviewPanel.vue +69 -0
- package/src/components/TermsDialog.vue +81 -0
- package/src/components/TokenDeployerView.vue +115 -0
- package/src/components/WalletBar.vue +108 -0
- package/src/components/WalrusBlobBrowser.vue +188 -0
- package/src/components/WalrusIconUpload.vue +205 -0
- package/src/composables/useAccessGate.ts +122 -0
- package/src/composables/useColorMode.ts +76 -0
- package/src/composables/useWalrusRelay.ts +21 -0
- package/src/config.ts +185 -0
- package/src/index.ts +1 -0
- package/src/lib/accessGate.ts +141 -0
- package/src/lib/buildPublishTx.ts +192 -0
- package/src/lib/deploy.ts +168 -0
- package/src/lib/deployExecutor.ts +61 -0
- package/src/lib/errors.ts +21 -0
- package/src/lib/form.ts +75 -0
- package/src/lib/generatePackage.ts +312 -0
- package/src/lib/github.ts +146 -0
- package/src/lib/licenses.ts +122 -0
- package/src/lib/readClient.ts +17 -0
- package/src/lib/template.ts +131 -0
- package/src/lib/types.ts +69 -0
- package/src/lib/validation.ts +172 -0
- package/src/lib/walrus-constants.ts +15 -0
- package/src/lib/walrus.ts +144 -0
- package/src/main.ts +154 -0
- package/src/move-template/sui_token_template.mv +0 -0
- package/src/move-template/template.ts +31 -0
- package/src/styles.css +384 -0
- package/src/template-src/files.json +10 -0
- package/test-results/.last-run.json +4 -0
- package/tests/accessGate.test.ts +108 -0
- package/tests/buildPublishTx.test.ts +243 -0
- package/tests/deploy.test.ts +81 -0
- package/tests/errors.test.ts +32 -0
- package/tests/generatePackage.test.ts +182 -0
- package/tests/github.test.ts +61 -0
- package/tests/licenses.test.ts +78 -0
- package/tests/template.test.ts +93 -0
- package/tests/templateArtifact.test.ts +61 -0
- package/tests/templateParity.test.ts +55 -0
- package/tests/useAccessGate.test.ts +101 -0
- package/tests/useWallet.test.ts +74 -0
- package/tests/useWalrusRelayGating.test.ts +30 -0
- package/tests/validation.test.ts +117 -0
- package/tests/walrusUpload.test.ts +93 -0
- package/tests/walrus_url_patch.test.ts +26 -0
- package/tsconfig.json +19 -0
- package/typedoc.json +34 -0
- package/vite.config.ts +65 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TokenConfig, Network } from '../lib/types.js'
|
|
3
|
+
import { FEE_MIST } from '../config.js'
|
|
4
|
+
import AppNotice from './AppNotice.vue'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{ config: TokenConfig; network: Network }>()
|
|
7
|
+
defineEmits<{ (e: 'back'): void; (e: 'confirm'): void }>()
|
|
8
|
+
|
|
9
|
+
const feeSui = Number(FEE_MIST) / 1e9
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<section class="card" aria-labelledby="review-h">
|
|
14
|
+
<h2 id="review-h">Review — deploying to {{ props.network }}</h2>
|
|
15
|
+
|
|
16
|
+
<AppNotice>
|
|
17
|
+
These values are <strong>permanent</strong> once published: symbol
|
|
18
|
+
<span class="mono">{{ config.symbol }}</span
|
|
19
|
+
>, name <span class="mono">{{ config.name }}</span
|
|
20
|
+
>, decimals <span class="mono">{{ config.decimals }}</span
|
|
21
|
+
>, and the coin type
|
|
22
|
+
<span class="mono"><package>::{{ config.moduleName }}::{{ config.structName }}</span
|
|
23
|
+
>.
|
|
24
|
+
</AppNotice>
|
|
25
|
+
|
|
26
|
+
<dl class="grid2" v-once>
|
|
27
|
+
<div>
|
|
28
|
+
<dt>Initial supply</dt>
|
|
29
|
+
<dd>{{ config.initialSupply.toString() }}</dd>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<dt>Supply policy</dt>
|
|
33
|
+
<dd>{{ config.supplyPolicy }}</dd>
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<dt>Metadata</dt>
|
|
37
|
+
<dd>{{ config.metadataPolicy }}</dd>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<dt>Package code</dt>
|
|
41
|
+
<dd>{{ config.packagePolicy }}</dd>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<dt>License (source only)</dt>
|
|
45
|
+
<dd>{{ config.license }}</dd>
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<dt>Recipient</dt>
|
|
49
|
+
<dd class="mono">{{ config.recipient || 'your wallet' }}</dd>
|
|
50
|
+
</div>
|
|
51
|
+
</dl>
|
|
52
|
+
|
|
53
|
+
<p v-once class="hint">
|
|
54
|
+
Your wallet signs and pays gas. A one-off fee of <strong>{{ feeSui }} SUI</strong> is included
|
|
55
|
+
in the publish transaction. Nothing is compiled or signed on a server.
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<p v-once class="hint">
|
|
59
|
+
You are the sole author of this token and fully responsible for its details and use. This tool
|
|
60
|
+
does not review, verify, or endorse tokens or their creators, and takes no custody of your
|
|
61
|
+
token or caps.
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<div class="row" style="margin-top: 1rem">
|
|
65
|
+
<button type="button" @click="$emit('back')">Back</button>
|
|
66
|
+
<button type="button" class="primary" @click="$emit('confirm')">Confirm & deploy</button>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
</template>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ open: boolean }>()
|
|
5
|
+
const emit = defineEmits<{
|
|
6
|
+
(e: 'accept'): void
|
|
7
|
+
(e: 'cancel'): void
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const el = ref<HTMLDialogElement>()
|
|
11
|
+
|
|
12
|
+
watch(
|
|
13
|
+
() => props.open,
|
|
14
|
+
(v) => (v ? el.value?.showModal() : el.value?.close()),
|
|
15
|
+
)
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<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>
|
|
24
|
+
<li>
|
|
25
|
+
All operations — bytecode compilation, transaction construction, and signing — take place
|
|
26
|
+
entirely <strong>client-side</strong>, meaning on your computer. No code runs on our
|
|
27
|
+
servers, and no server has any visibility into or control over what happens on your machine.
|
|
28
|
+
</li>
|
|
29
|
+
<li>
|
|
30
|
+
This is a neutral, self-service tool. The operator does not review, approve, or endorse any
|
|
31
|
+
token deployed with it.
|
|
32
|
+
</li>
|
|
33
|
+
<li>
|
|
34
|
+
You are solely responsible for your token, its content, and its use — including compliance
|
|
35
|
+
with all applicable laws in your jurisdiction.
|
|
36
|
+
</li>
|
|
37
|
+
<li>
|
|
38
|
+
Please don't create tokens that misrepresent projects, impersonate other assets, or
|
|
39
|
+
facilitate fraud. We can't actually see what you're doing from here, but we thought we'd
|
|
40
|
+
mention it. Bad vibes.
|
|
41
|
+
</li>
|
|
42
|
+
<li>
|
|
43
|
+
On-chain transactions are irreversible. Review your configuration carefully before
|
|
44
|
+
confirming.
|
|
45
|
+
</li>
|
|
46
|
+
<li>
|
|
47
|
+
The operator accepts no liability for financial loss, legal consequences, or any harm
|
|
48
|
+
arising from your use of this tool.
|
|
49
|
+
</li>
|
|
50
|
+
</ul>
|
|
51
|
+
<p style="margin-bottom: 1.25rem; font-size: 0.9rem; color: var(--muted)">
|
|
52
|
+
By continuing, you confirm that you have read and accept these terms.
|
|
53
|
+
</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>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<style scoped>
|
|
62
|
+
ul {
|
|
63
|
+
padding-left: 1.25rem;
|
|
64
|
+
margin: 0 0 1rem;
|
|
65
|
+
line-height: 1.6;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ul li {
|
|
69
|
+
margin-bottom: 0.6rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ul li:last-child {
|
|
73
|
+
margin-bottom: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.terms-actions {
|
|
77
|
+
display: flex;
|
|
78
|
+
gap: 0.75rem;
|
|
79
|
+
justify-content: flex-end;
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, reactive, ref } from 'vue'
|
|
3
|
+
import { useWallet, useNetwork, WalletGuard } from '@meddleware/wallet-adapter'
|
|
4
|
+
import ConfigForm from './ConfigForm.vue'
|
|
5
|
+
import ReviewPanel from './ReviewPanel.vue'
|
|
6
|
+
import DeployProgress from './DeployProgress.vue'
|
|
7
|
+
import ResultPanel from './ResultPanel.vue'
|
|
8
|
+
import TermsDialog from './TermsDialog.vue'
|
|
9
|
+
import AppNotice from './AppNotice.vue'
|
|
10
|
+
import { emptyForm, toTokenConfig } from '../lib/form.js'
|
|
11
|
+
import { validateForm, isValid } from '../lib/validation.js'
|
|
12
|
+
import { deployToken } from '../lib/deploy.js'
|
|
13
|
+
import type { DeployStep } from '../lib/deploy.js'
|
|
14
|
+
import { buildDeployExecutor } from '../lib/deployExecutor.js'
|
|
15
|
+
import { FEE_MIST, FEE_TREASURY, PUBLISH_GAS_BUDGET, isFeeConfigured } from '../config.js'
|
|
16
|
+
import { extractErrorMessage } from '../lib/errors.js'
|
|
17
|
+
import type { Network, PublishResult } from '../lib/types.js'
|
|
18
|
+
|
|
19
|
+
type Step = 'form' | 'review' | 'deploying' | 'done'
|
|
20
|
+
|
|
21
|
+
const { account } = useWallet()
|
|
22
|
+
const { network } = useNetwork()
|
|
23
|
+
|
|
24
|
+
const form = reactive(emptyForm())
|
|
25
|
+
const step = ref<Step>('form')
|
|
26
|
+
const deployStep = ref<DeployStep | null>(null)
|
|
27
|
+
const deployError = ref<string | null>(null)
|
|
28
|
+
const result = ref<PublishResult | null>(null)
|
|
29
|
+
const termsAccepted = ref(localStorage.getItem('sui-deployer:terms-accepted') === '1')
|
|
30
|
+
const showTerms = ref(false)
|
|
31
|
+
|
|
32
|
+
const errors = computed(() => validateForm(form))
|
|
33
|
+
const config = computed(() => toTokenConfig(form))
|
|
34
|
+
const connected = computed(() => Boolean(account.value))
|
|
35
|
+
const canProceed = computed(() => isValid(errors.value) && connected.value)
|
|
36
|
+
|
|
37
|
+
function goReview(): void {
|
|
38
|
+
if (!canProceed.value) return
|
|
39
|
+
if (!termsAccepted.value) { showTerms.value = true; return }
|
|
40
|
+
step.value = 'review'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function onTermsAccept(): void {
|
|
44
|
+
localStorage.setItem('sui-deployer:terms-accepted', '1')
|
|
45
|
+
termsAccepted.value = true
|
|
46
|
+
showTerms.value = false
|
|
47
|
+
step.value = 'review'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onTermsCancel(): void {
|
|
51
|
+
showTerms.value = false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function confirmDeploy(): Promise<void> {
|
|
55
|
+
const sender = account.value?.address
|
|
56
|
+
if (!sender) { deployError.value = 'Connect a wallet first.'; return }
|
|
57
|
+
deployError.value = null
|
|
58
|
+
step.value = 'deploying'
|
|
59
|
+
deployStep.value = null
|
|
60
|
+
try {
|
|
61
|
+
const net = network.value as Network
|
|
62
|
+
const feeConfigured = isFeeConfigured(net)
|
|
63
|
+
result.value = await deployToken({
|
|
64
|
+
config: config.value,
|
|
65
|
+
network: net,
|
|
66
|
+
sender,
|
|
67
|
+
feeMist: feeConfigured ? FEE_MIST : 0n,
|
|
68
|
+
feeTreasury: FEE_TREASURY[net],
|
|
69
|
+
gasBudget: PUBLISH_GAS_BUDGET,
|
|
70
|
+
executor: await buildDeployExecutor(net),
|
|
71
|
+
onStep: (s) => (deployStep.value = s),
|
|
72
|
+
})
|
|
73
|
+
step.value = 'done'
|
|
74
|
+
} catch (e) {
|
|
75
|
+
deployError.value = extractErrorMessage(e)
|
|
76
|
+
step.value = 'review'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function restart(): void {
|
|
81
|
+
result.value = null
|
|
82
|
+
deployError.value = null
|
|
83
|
+
step.value = 'form'
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<WalletGuard message="Connect a Sui wallet to deploy your token.">
|
|
89
|
+
<ConfigForm
|
|
90
|
+
v-if="step === 'form'"
|
|
91
|
+
:form="form"
|
|
92
|
+
:errors="errors"
|
|
93
|
+
:can-proceed="canProceed"
|
|
94
|
+
:network="network"
|
|
95
|
+
:connected="connected"
|
|
96
|
+
@submit="goReview"
|
|
97
|
+
/>
|
|
98
|
+
<ReviewPanel
|
|
99
|
+
v-if="step === 'review'"
|
|
100
|
+
:config="config"
|
|
101
|
+
:network="network"
|
|
102
|
+
@back="step = 'form'"
|
|
103
|
+
@confirm="confirmDeploy"
|
|
104
|
+
/>
|
|
105
|
+
<DeployProgress v-if="step === 'deploying'" :step="deployStep" />
|
|
106
|
+
<ResultPanel
|
|
107
|
+
v-if="step === 'done' && result"
|
|
108
|
+
:result="result"
|
|
109
|
+
:config="config"
|
|
110
|
+
@restart="restart"
|
|
111
|
+
/>
|
|
112
|
+
<AppNotice v-if="deployError" type="error">{{ deployError }}</AppNotice>
|
|
113
|
+
<TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
|
|
114
|
+
</WalletGuard>
|
|
115
|
+
</template>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { useWallet, useNetwork } from '@meddleware/wallet-adapter'
|
|
4
|
+
import { SELECTABLE_NETWORKS } from '../config.js'
|
|
5
|
+
import type { Network } from '../lib/types.js'
|
|
6
|
+
import FormSelect from './FormSelect.vue'
|
|
7
|
+
import AppNotice from './AppNotice.vue'
|
|
8
|
+
|
|
9
|
+
const { wallets, currentWallet, account, connecting, error, connect, disconnect } = useWallet()
|
|
10
|
+
const { network, setNetwork } = useNetwork()
|
|
11
|
+
|
|
12
|
+
const dialogEl = ref<HTMLDialogElement>()
|
|
13
|
+
|
|
14
|
+
const shortAddress = computed(() => {
|
|
15
|
+
const a = account.value?.address
|
|
16
|
+
return a ? `${a.slice(0, 6)}…${a.slice(-4)}` : ''
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const networkModel = computed({
|
|
20
|
+
get: () => network.value,
|
|
21
|
+
set: (v: string) => setNetwork(v as Network),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
function openDialog(): void {
|
|
25
|
+
dialogEl.value?.showModal()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function closeDialog(): void {
|
|
29
|
+
dialogEl.value?.close()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
watch(() => account.value, (acc) => {
|
|
33
|
+
if (acc) {
|
|
34
|
+
closeDialog()
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="card" role="region" aria-label="Wallet and network">
|
|
41
|
+
<template v-if="account">
|
|
42
|
+
<div class="row" style="justify-content: space-between">
|
|
43
|
+
<div class="row">
|
|
44
|
+
<label for="network-select" style="margin: 0">Network</label>
|
|
45
|
+
<FormSelect id="network-select" v-model="networkModel">
|
|
46
|
+
<option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
|
|
47
|
+
</FormSelect>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="row">
|
|
51
|
+
<span class="badge" aria-label="Connected wallet">
|
|
52
|
+
{{ currentWallet?.name }} · <span class="mono">{{ shortAddress }}</span>
|
|
53
|
+
</span>
|
|
54
|
+
<button type="button" @click="disconnect">Disconnect</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
<template v-else>
|
|
59
|
+
<div class="row">
|
|
60
|
+
<div v-if="!wallets.length" style="width: 100%">
|
|
61
|
+
<p class="hint" style="margin: 0">
|
|
62
|
+
No Sui wallet detected. Install a Sui wallet extension to continue.
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
<button v-else type="button" class="primary" @click="openDialog">
|
|
66
|
+
Connect Wallet
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<AppNotice v-if="error" type="error" style="margin-top: 0.75rem">{{ error }}</AppNotice>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<dialog ref="dialogEl" aria-labelledby="wallet-dialog-title" @click.self="closeDialog">
|
|
75
|
+
<div class="dialog-header">
|
|
76
|
+
<h2 id="wallet-dialog-title">Connect Wallet</h2>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
aria-label="Close"
|
|
80
|
+
style="background: none; border: none; font-size: 1.5rem; padding: 0; cursor: pointer; color: var(--text)"
|
|
81
|
+
@click="closeDialog"
|
|
82
|
+
>
|
|
83
|
+
×
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div>
|
|
88
|
+
<label for="dialog-network-select">Network</label>
|
|
89
|
+
<FormSelect id="dialog-network-select" v-model="networkModel">
|
|
90
|
+
<option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
|
|
91
|
+
</FormSelect>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="wallet-list">
|
|
95
|
+
<button
|
|
96
|
+
v-for="w in wallets"
|
|
97
|
+
:key="w.name"
|
|
98
|
+
type="button"
|
|
99
|
+
class="primary"
|
|
100
|
+
:disabled="connecting"
|
|
101
|
+
@click="connect(w)"
|
|
102
|
+
>
|
|
103
|
+
<img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
|
|
104
|
+
{{ w.name }}
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
</dialog>
|
|
108
|
+
</template>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
import { useWallet } from '@meddleware/wallet-adapter'
|
|
4
|
+
import { getReadClient } from '../lib/readClient.js'
|
|
5
|
+
import type { OwnedBlob, WalrusNetwork } from '../lib/walrus.js'
|
|
6
|
+
import type { Network } from '../lib/types.js'
|
|
7
|
+
import AppNotice from './AppNotice.vue'
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{ network: WalrusNetwork; connected: boolean }>()
|
|
10
|
+
const emit = defineEmits<{ (e: 'select', url: string): void }>()
|
|
11
|
+
|
|
12
|
+
const { account } = useWallet()
|
|
13
|
+
|
|
14
|
+
const addressInput = ref(account.value?.address ?? '')
|
|
15
|
+
watch(
|
|
16
|
+
() => account.value?.address,
|
|
17
|
+
(addr) => {
|
|
18
|
+
if (addr && !addressInput.value) addressInput.value = addr
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
interface BlobItem extends OwnedBlob {
|
|
23
|
+
url: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const blobs = ref<BlobItem[]>([])
|
|
27
|
+
const loading = ref(false)
|
|
28
|
+
const loadError = ref<string | null>(null)
|
|
29
|
+
const selected = ref('')
|
|
30
|
+
const hasLoaded = ref(false)
|
|
31
|
+
|
|
32
|
+
function formatSize(bytes: number): string {
|
|
33
|
+
if (bytes < 1024) return `${bytes} B`
|
|
34
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
35
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function load(): Promise<void> {
|
|
39
|
+
const addr = addressInput.value.trim() || account.value?.address
|
|
40
|
+
if (!addr) {
|
|
41
|
+
loadError.value = 'Enter an address or connect your wallet.'
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
loading.value = true
|
|
45
|
+
loadError.value = null
|
|
46
|
+
blobs.value = []
|
|
47
|
+
hasLoaded.value = false
|
|
48
|
+
try {
|
|
49
|
+
const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } = await import(
|
|
50
|
+
'../lib/walrus.js'
|
|
51
|
+
)
|
|
52
|
+
const suiClient = getReadClient(props.network as Network)
|
|
53
|
+
const client = createWalrusClient(props.network)
|
|
54
|
+
const raw = await fetchOwnedWalrusBlobs(suiClient, client, addr)
|
|
55
|
+
blobs.value = raw.map((b) => ({ ...b, url: walrusBlobUrl(props.network, b.blobId) }))
|
|
56
|
+
hasLoaded.value = true
|
|
57
|
+
} catch (e) {
|
|
58
|
+
loadError.value = e instanceof Error ? e.message : String(e)
|
|
59
|
+
} finally {
|
|
60
|
+
loading.value = false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function selectBlob(item: BlobItem): void {
|
|
65
|
+
selected.value = item.blobId
|
|
66
|
+
emit('select', item.url)
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div class="blob-browser">
|
|
72
|
+
<div class="row">
|
|
73
|
+
<input
|
|
74
|
+
v-model="addressInput"
|
|
75
|
+
type="text"
|
|
76
|
+
placeholder="0x… or leave blank for your wallet"
|
|
77
|
+
aria-label="Wallet address to browse"
|
|
78
|
+
style="flex: 1"
|
|
79
|
+
/>
|
|
80
|
+
<button type="button" :disabled="loading || (!connected && !addressInput.trim())" @click="load">
|
|
81
|
+
<span v-if="loading" class="spinner" aria-hidden="true"></span>
|
|
82
|
+
Load
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
<p class="hint">Browse Walrus blobs owned by this address. Only image blobs show a preview.</p>
|
|
86
|
+
|
|
87
|
+
<AppNotice v-if="loadError" type="error">{{ loadError }}</AppNotice>
|
|
88
|
+
|
|
89
|
+
<p v-if="hasLoaded && blobs.length === 0 && !loading" class="hint">
|
|
90
|
+
No Walrus blobs found at this address.
|
|
91
|
+
</p>
|
|
92
|
+
|
|
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>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<style scoped>
|
|
117
|
+
.blob-browser {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
gap: 0.5rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.blob-grid {
|
|
124
|
+
display: grid;
|
|
125
|
+
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
|
126
|
+
gap: 0.5rem;
|
|
127
|
+
margin-top: 0.25rem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.blob-thumb {
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: 0.25rem;
|
|
135
|
+
padding: 0.25rem;
|
|
136
|
+
border: 2px solid var(--border);
|
|
137
|
+
border-radius: 6px;
|
|
138
|
+
background: transparent;
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
transition: border-color 0.15s;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.blob-thumb:hover {
|
|
145
|
+
border-color: var(--accent);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.blob-thumb.selected {
|
|
149
|
+
border-color: var(--accent);
|
|
150
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.blob-thumb img {
|
|
154
|
+
width: 100%;
|
|
155
|
+
aspect-ratio: 1;
|
|
156
|
+
object-fit: cover;
|
|
157
|
+
border-radius: 3px;
|
|
158
|
+
display: block;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.blob-thumb.no-img img {
|
|
162
|
+
height: 0;
|
|
163
|
+
width: 0;
|
|
164
|
+
visibility: hidden;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.blob-thumb.no-img::before {
|
|
168
|
+
content: '?';
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
width: 100%;
|
|
173
|
+
aspect-ratio: 1;
|
|
174
|
+
font-size: 1.25rem;
|
|
175
|
+
color: var(--muted);
|
|
176
|
+
background: var(--surface);
|
|
177
|
+
border-radius: 3px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.blob-meta {
|
|
181
|
+
font-size: 0.65rem;
|
|
182
|
+
color: var(--muted);
|
|
183
|
+
white-space: nowrap;
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
text-overflow: ellipsis;
|
|
186
|
+
max-width: 100%;
|
|
187
|
+
}
|
|
188
|
+
</style>
|