@meddleware/token-deployer-ui 0.0.9 → 0.0.11
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/LICENSE +11 -118
- package/README.md +11 -10
- package/package.json +22 -10
- package/src/App.vue +1 -1
- package/src/component-styles.css +39 -57
- package/src/components/GithubPush.vue +2 -2
- package/src/components/IconPicker.vue +44 -75
- package/src/components/LicensePicker.vue +4 -3
- package/src/components/ResultPanel.vue +2 -2
- package/src/components/ReviewPanel.vue +11 -3
- package/src/components/TokenDeployerView.vue +52 -48
- package/src/components/WalletBar.vue +12 -1
- package/src/components/WalrusBlobBrowser.vue +4 -4
- package/src/composables/useWalrusRelay.ts +1 -2
- package/src/config.ts +29 -30
- package/src/lib/buildPublishTx.ts +0 -10
- package/src/lib/deploy.ts +45 -3
- package/src/lib/deployExecutor.ts +19 -44
- package/src/lib/generatePackage.ts +12 -12
- package/src/lib/licenses.ts +4 -3
- package/src/lib/listMyTokens.ts +7 -11
- package/src/lib/readClient.ts +15 -7
- package/src/lib/template.ts +3 -0
- package/src/lib/types.ts +1 -1
- package/src/lib/walrus-constants.ts +2 -2
- package/src/main.ts +42 -61
- package/src/styles.css +22 -0
- package/src/template-src/files.json +7 -7
- package/.claude/move-language-reference/SKILL.md +0 -445
- package/.claude/skills/move-language-reference/SKILL.md +0 -445
- package/.claude/skills/sui-move-bootcamp/SKILL.md +0 -128
- package/.claude/sui-move-bootcamp/SKILL.md +0 -128
- package/.editorconfig +0 -8
- package/.env.example +0 -75
- package/.gitattributes +0 -1
- package/.github/workflows/docker-publish.yml +0 -348
- package/.github/workflows/e2e-realchain.yml +0 -64
- package/.github/workflows/node-ci.yml +0 -43
- package/.github/workflows/npm-publish.yml +0 -83
- package/.htmlvalidate.mjs +0 -30
- package/.oxfmtrc.json +0 -5
- package/.oxlintrc.json +0 -10
- package/.vscode/extensions.json +0 -9
- package/AGENTS.md +0 -39
- package/CLAUDE.md +0 -159
- package/Dockerfile +0 -96
- package/eslint.config.ts +0 -63
- package/index.html +0 -13
- package/playwright.config.ts +0 -30
- package/postcss.config.js +0 -8
- package/public/CNAME +0 -1
- package/public/_headers +0 -12
- package/public/_redirects +0 -1
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +0 -5
- package/public/og-image.png +0 -0
- package/public/robots.txt +0 -3
- package/public/sitemap.xml +0 -8
- package/scripts/e2e-browser.mjs +0 -15
- package/scripts/e2e-deploy.mjs +0 -222
- package/scripts/e2e-localnet.mjs +0 -65
- package/scripts/e2e-walrus-browser.mjs +0 -115
- package/scripts/gen-og-image.mjs +0 -31
- package/scripts/publish-localnet.mjs +0 -55
- package/scripts/regen-template.sh +0 -45
- package/scripts/sync-template-src.sh +0 -36
- package/scripts/verify-bytecode.mjs +0 -49
- package/src/__tests__/App.spec.js +0 -11
- package/src/composables/useAccessGate.ts +0 -127
- package/src/lib/accessGate.ts +0 -141
- package/src/lib/walrus.ts +0 -144
- package/stylelint.config.js +0 -33
- package/test-results/.last-run.json +0 -4
- package/tests/accessGate.test.ts +0 -108
- package/tests/buildPublishTx.test.ts +0 -243
- package/tests/deploy.test.ts +0 -81
- package/tests/errors.test.ts +0 -32
- package/tests/generatePackage.test.ts +0 -182
- package/tests/github.test.ts +0 -61
- package/tests/licenses.test.ts +0 -78
- package/tests/template.test.ts +0 -93
- package/tests/templateArtifact.test.ts +0 -61
- package/tests/templateParity.test.ts +0 -55
- package/tests/useAccessGate.test.ts +0 -101
- package/tests/useWallet.test.ts +0 -74
- package/tests/useWalrusRelayGating.test.ts +0 -30
- package/tests/validation.test.ts +0 -117
- package/tests/walrusUpload.test.ts +0 -93
- package/tests/walrus_url_patch.test.ts +0 -26
- package/tsconfig.json +0 -19
- package/typedoc.json +0 -34
- package/vite.config.ts +0 -65
- package/vitest.config.ts +0 -10
|
@@ -4,15 +4,12 @@ import { useWallet } from '@meddleware/wallet-adapter'
|
|
|
4
4
|
import { buildDeployExecutor } from '../lib/deployExecutor.js'
|
|
5
5
|
import { getReadClient } from '../lib/readClient.js'
|
|
6
6
|
import { useWalrusRelay } from '../composables/useWalrusRelay.js'
|
|
7
|
-
import { useAccessGate } from '
|
|
7
|
+
import { useAccessGate } from '@meddleware/walrus-relay'
|
|
8
|
+
// The flow subpath carries no wasm; it loads the Walrus client lazily when an upload starts.
|
|
9
|
+
import { consumeStorageKey, createGatedAccess, runBlobUpload } from '@meddleware/walrus-client/flow'
|
|
8
10
|
import { ICON_EPOCHS } from '../lib/walrus-constants.js'
|
|
9
|
-
import { ICON_MAX_BYTES, validateIconFile } from '../config.js'
|
|
10
|
-
import {
|
|
11
|
-
fetchRelayChallenge,
|
|
12
|
-
personalMessageForNonce,
|
|
13
|
-
buildAccessProofToken,
|
|
14
|
-
} from '../lib/accessGate.js'
|
|
15
|
-
import type { WalrusNetwork } from '../lib/walrus.js'
|
|
11
|
+
import { ACCESS_GATE, ICON_MAX_BYTES, WALRUS_MAX_TIP_MIST, WALRUS_RPC_URLS, validateIconFile } from '../config.js'
|
|
12
|
+
import type { WalrusNetwork } from '../config.js'
|
|
16
13
|
import type { Network } from '../lib/types.js'
|
|
17
14
|
import { UiNotice, UiFieldHint, UiSegmentedControl } from '@meddleware/ui'
|
|
18
15
|
import WalrusBlobBrowser from './WalrusBlobBrowser.vue'
|
|
@@ -36,7 +33,7 @@ const isWalrusNetwork = computed(() => props.network !== 'localnet')
|
|
|
36
33
|
const walrusNet: WalrusNetwork = props.network === 'mainnet' ? 'mainnet' : 'testnet'
|
|
37
34
|
|
|
38
35
|
// NFT-gated relay access (inert when no gate is configured for this network).
|
|
39
|
-
const accessGate = useAccessGate(walrusNet,
|
|
36
|
+
const accessGate = useAccessGate({ gate: ACCESS_GATE[walrusNet], getClient: () => getReadClient(walrusNet) })
|
|
40
37
|
|
|
41
38
|
const {
|
|
42
39
|
selectedRelayHost,
|
|
@@ -142,82 +139,53 @@ async function upload(): Promise<void> {
|
|
|
142
139
|
uploadError.value = 'Connect your wallet first.'
|
|
143
140
|
return
|
|
144
141
|
}
|
|
142
|
+
const address = account.value.address
|
|
145
143
|
uploading.value = true
|
|
146
144
|
uploadError.value = null
|
|
147
145
|
try {
|
|
148
|
-
const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } =
|
|
149
|
-
await import('../lib/walrus.js')
|
|
150
146
|
const executor = await buildDeployExecutor(props.network as Network)
|
|
151
|
-
const suiClient = getReadClient(props.network as Network)
|
|
152
147
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
148
|
+
// The gated operator relay needs proof of an access pass. For a single-use pass the on-chain
|
|
149
|
+
// consume is persisted before the upload and reused on a retry (the gateway redeems a consume
|
|
150
|
+
// digest once, on a successful upload), so an interrupted upload never burns a use; a fresh use
|
|
151
|
+
// is spent only after the gateway reports the stored one redeemed.
|
|
157
152
|
const publicHost = availableRelays.value.find((r) => r.isPublic)?.host
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
153
|
+
const gate = accessGate.gate
|
|
154
|
+
const nftId = accessGate.nftId.value
|
|
155
|
+
const gated = !!gate && accessGate.hasAccess.value === true && !!nftId && selectedRelayHost.value !== publicHost
|
|
156
|
+
const access =
|
|
157
|
+
gated && gate && nftId
|
|
158
|
+
? createGatedAccess({
|
|
159
|
+
storage: window.localStorage,
|
|
160
|
+
key: consumeStorageKey(walrusNet, gate.gateId, address),
|
|
161
|
+
relayHost: selectedRelayHost.value,
|
|
162
|
+
address,
|
|
163
|
+
nftId,
|
|
164
|
+
singleUse: accessGate.usesRemaining.value !== null,
|
|
165
|
+
buildConsume: (id, nonce) => accessGate.buildConsume(id, nonce),
|
|
166
|
+
signAndExecute: (tx) => executor.signAndExecute(tx),
|
|
167
|
+
waitForTransaction: (digest) => executor.waitForTransaction(digest),
|
|
168
|
+
sign: signPersonalMessage,
|
|
169
|
+
onStatus: (p) => (status.value = p.detail ?? ''),
|
|
170
|
+
})
|
|
171
|
+
: undefined
|
|
172
|
+
|
|
173
|
+
const result = await runBlobUpload({
|
|
174
|
+
bytes,
|
|
175
|
+
network: walrusNet,
|
|
176
|
+
relayHost: selectedRelayHost.value,
|
|
177
|
+
address,
|
|
178
|
+
rpcUrl: WALRUS_RPC_URLS[walrusNet],
|
|
180
179
|
wasmUrl: walrusWasmUrl,
|
|
181
|
-
|
|
182
|
-
uploadRelayAuthToken,
|
|
183
|
-
})
|
|
184
|
-
const flow = createBlobUploadFlow(client, bytes)
|
|
185
|
-
|
|
186
|
-
status.value = 'Encoding…'
|
|
187
|
-
await flow.encode()
|
|
188
|
-
|
|
189
|
-
let feeNote = ''
|
|
190
|
-
try {
|
|
191
|
-
const tipMist = await client.walrus.calculateUploadRelayTip({ size: bytes.length })
|
|
192
|
-
const tipSui = Number(tipMist) / 1e9
|
|
193
|
-
feeNote = tipMist > 0n ? ` Relay fee ≈ ${tipSui.toFixed(4)} SUI + storage (WAL).` : ''
|
|
194
|
-
} catch {
|
|
195
|
-
// Non-fatal — the register step surfaces any real tip error.
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
status.value = `Registering blob (approve in wallet)…${feeNote}`
|
|
199
|
-
const regTx = flow.register({
|
|
200
|
-
owner: account.value.address,
|
|
180
|
+
maxTipMist: Number(WALRUS_MAX_TIP_MIST),
|
|
201
181
|
epochs: ICON_EPOCHS,
|
|
202
182
|
deletable: !permanent.value,
|
|
183
|
+
executor,
|
|
184
|
+
suiClient: getReadClient(props.network as Network),
|
|
185
|
+
access,
|
|
186
|
+
onStatus: (p) => (status.value = p.detail ?? ''),
|
|
203
187
|
})
|
|
204
|
-
|
|
205
|
-
await regTx.build({ client: suiClient })
|
|
206
|
-
const reg = await executor.signAndExecute(regTx)
|
|
207
|
-
await executor.waitForTransaction(reg.digest)
|
|
208
|
-
|
|
209
|
-
status.value = 'Uploading to the relay…'
|
|
210
|
-
await flow.upload({ digest: reg.digest })
|
|
211
|
-
|
|
212
|
-
status.value = 'Certifying (approve in wallet)…'
|
|
213
|
-
const certTx = flow.certify()
|
|
214
|
-
certTx.setSenderIfNotSet(account.value.address)
|
|
215
|
-
await certTx.build({ client: suiClient })
|
|
216
|
-
const cert = await executor.signAndExecute(certTx)
|
|
217
|
-
await executor.waitForTransaction(cert.digest)
|
|
218
|
-
|
|
219
|
-
const blob = await flow.getBlob()
|
|
220
|
-
emit('update:modelValue', walrusBlobUrl(walrusNet, blob.blobId))
|
|
188
|
+
emit('update:modelValue', result.url)
|
|
221
189
|
tab.value = 'url'
|
|
222
190
|
status.value = 'Uploaded ✓ — URL filled in above.'
|
|
223
191
|
} catch (e) {
|
|
@@ -225,6 +193,7 @@ async function upload(): Promise<void> {
|
|
|
225
193
|
status.value = ''
|
|
226
194
|
} finally {
|
|
227
195
|
uploading.value = false
|
|
196
|
+
if (account.value) void refreshAccess() // uses remaining may have changed
|
|
228
197
|
}
|
|
229
198
|
}
|
|
230
199
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { computed, onMounted, ref } from 'vue'
|
|
3
3
|
import { fetchLicenseList, fetchLicenseText, NO_LICENSE } from '../lib/licenses.js'
|
|
4
4
|
import type { SpdxLicense } from '../lib/licenses.js'
|
|
5
|
-
import { UiDialog, UiFieldHint, UiFormField, UiNotice, UiSelect } from '@meddleware/ui'
|
|
5
|
+
import { UiDialog, UiFieldHint, UiFormField, UiNotice, UiSelect, safeHref } from '@meddleware/ui'
|
|
6
6
|
|
|
7
7
|
const props = defineProps<{ modelValue: string }>()
|
|
8
8
|
const emit = defineEmits<{
|
|
@@ -73,7 +73,8 @@ async function openLicenseModal(): Promise<void> {
|
|
|
73
73
|
<UiFieldHint field-id="license-select">
|
|
74
74
|
Applies only to the downloadable Move source package — embedded in the LICENSE file and
|
|
75
75
|
source headers. Has <b>no effect on the on-chain token</b> or its transferability. Choose
|
|
76
|
-
<b>
|
|
76
|
+
<b>0BSD</b> (default) for a minimal permissive licence, or <b>CC0-1.0</b> for public domain.
|
|
77
|
+
Choose <b>None</b> to retain all rights.
|
|
77
78
|
</UiFieldHint>
|
|
78
79
|
</template>
|
|
79
80
|
<template #default="{ attrs }">
|
|
@@ -130,7 +131,7 @@ async function openLicenseModal(): Promise<void> {
|
|
|
130
131
|
<template v-else>
|
|
131
132
|
<p class="license-spdx">
|
|
132
133
|
<a
|
|
133
|
-
:href="`https://spdx.org/licenses/${modelValue}.html`"
|
|
134
|
+
:href="safeHref(`https://spdx.org/licenses/${encodeURIComponent(modelValue)}.html`)"
|
|
134
135
|
target="_blank"
|
|
135
136
|
rel="noopener noreferrer"
|
|
136
137
|
>View on SPDX ↗</a
|
|
@@ -5,7 +5,7 @@ import { explorerObjectUrl } from '../config.js'
|
|
|
5
5
|
import { generatePackageZip } from '../lib/generatePackage.js'
|
|
6
6
|
import { fetchLicenseText } from '../lib/licenses.js'
|
|
7
7
|
import GithubPush from './GithubPush.vue'
|
|
8
|
-
import { UiNotice } from '@meddleware/ui'
|
|
8
|
+
import { UiNotice, safeHref } from '@meddleware/ui'
|
|
9
9
|
|
|
10
10
|
const props = defineProps<{ result: PublishResult; config: TokenConfig }>()
|
|
11
11
|
defineEmits<{ (e: 'restart'): void }>()
|
|
@@ -51,7 +51,7 @@ async function downloadPackage(): Promise<void> {
|
|
|
51
51
|
<dd class="mono">{{ result.coinType }}</dd>
|
|
52
52
|
<dt>Package</dt>
|
|
53
53
|
<dd class="mono">
|
|
54
|
-
<a :href="packageUrl" target="_blank" rel="noopener noreferrer">
|
|
54
|
+
<a :href="safeHref(packageUrl)" target="_blank" rel="noopener noreferrer">
|
|
55
55
|
{{ result.packageId }}
|
|
56
56
|
</a>
|
|
57
57
|
</dd>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { TokenConfig, Network } from '../lib/types.js'
|
|
3
|
-
import { FEE_MIST } from '../config.js'
|
|
3
|
+
import { FEE_MIST, FEE_TREASURY, isFeeConfigured } from '../config.js'
|
|
4
4
|
import { UiNotice } from '@meddleware/ui'
|
|
5
5
|
|
|
6
6
|
const props = defineProps<{ config: TokenConfig; network: Network }>()
|
|
7
7
|
defineEmits<{ (e: 'back'): void; (e: 'confirm'): void }>()
|
|
8
8
|
|
|
9
|
+
// Mirrors TokenDeployerView: the fee is only charged when a real treasury is configured.
|
|
10
|
+
const feeCharged = isFeeConfigured(props.network)
|
|
9
11
|
const feeSui = Number(FEE_MIST) / 1e9
|
|
12
|
+
const treasury = FEE_TREASURY[props.network]
|
|
10
13
|
</script>
|
|
11
14
|
|
|
12
15
|
<template>
|
|
@@ -51,8 +54,13 @@ const feeSui = Number(FEE_MIST) / 1e9
|
|
|
51
54
|
</dl>
|
|
52
55
|
|
|
53
56
|
<p v-once class="hint">
|
|
54
|
-
Your wallet signs and pays gas.
|
|
55
|
-
|
|
57
|
+
Your wallet signs and pays gas.
|
|
58
|
+
<template v-if="feeCharged">
|
|
59
|
+
A one-off fee of <strong>{{ feeSui }} SUI</strong> is included in the publish transaction,
|
|
60
|
+
paid to the operator treasury <span class="mono">{{ treasury }}</span>.
|
|
61
|
+
</template>
|
|
62
|
+
<template v-else>No operator fee is charged on this network.</template>
|
|
63
|
+
Nothing is compiled or signed on a server.
|
|
56
64
|
</p>
|
|
57
65
|
|
|
58
66
|
<p v-once class="hint">
|
|
@@ -191,57 +191,61 @@ function restart(): void {
|
|
|
191
191
|
</script>
|
|
192
192
|
|
|
193
193
|
<template>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
<div class="mw-token-deployer">
|
|
195
|
+
<UiToolIntro
|
|
196
|
+
>Deploy your own Sui coin — your wallet signs and pays; no intermediaries ({{
|
|
197
|
+
network
|
|
198
|
+
}}).</UiToolIntro
|
|
199
|
+
>
|
|
200
|
+
|
|
201
|
+
<AppTabNav
|
|
202
|
+
v-model="activeTab"
|
|
203
|
+
:tabs="TABS"
|
|
204
|
+
id-prefix="token-deployer"
|
|
205
|
+
aria-label="Sections"
|
|
206
|
+
class="deployer-tabs"
|
|
207
|
+
/>
|
|
208
|
+
|
|
209
|
+
<!-- The tab list and its panel always render (the selected tab controls a live panel); the wallet
|
|
210
|
+
prompt replaces only the panel's content until a wallet is connected. -->
|
|
209
211
|
<UiTabPanel id-prefix="token-deployer" :tab="activeTab">
|
|
210
|
-
<
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
212
|
+
<WalletGuard message="Connect a Sui wallet to deploy your token.">
|
|
213
|
+
<template v-if="activeTab === 'deploy'">
|
|
214
|
+
<UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
|
|
215
|
+
<ConfigForm
|
|
216
|
+
v-if="step === 'identity' || step === 'token' || step === 'settings'"
|
|
217
|
+
:form="form"
|
|
218
|
+
:errors="errors"
|
|
219
|
+
:can-proceed="canProceed"
|
|
220
|
+
:network="network"
|
|
221
|
+
:connected="connected"
|
|
222
|
+
:form-step="formStep"
|
|
223
|
+
@next="goNext"
|
|
224
|
+
@back="goBack"
|
|
225
|
+
/>
|
|
226
|
+
<ReviewPanel
|
|
227
|
+
v-if="step === 'review'"
|
|
228
|
+
:config="config"
|
|
229
|
+
:network="network"
|
|
230
|
+
@back="goBack"
|
|
231
|
+
@confirm="confirmDeploy"
|
|
232
|
+
/>
|
|
233
|
+
<DeployProgress v-if="step === 'deploying'" :step="deployStep" />
|
|
234
|
+
<ResultPanel
|
|
235
|
+
v-if="step === 'done' && result"
|
|
236
|
+
:result="result"
|
|
237
|
+
:config="config"
|
|
238
|
+
@restart="restart"
|
|
239
|
+
/>
|
|
240
|
+
<UiNotice v-if="deployError" type="error">{{ deployError }}</UiNotice>
|
|
241
|
+
</template>
|
|
242
|
+
|
|
243
|
+
<MyTokens v-else :owner="account?.address ?? null" :network="network" />
|
|
244
|
+
</WalletGuard>
|
|
241
245
|
</UiTabPanel>
|
|
242
|
-
</WalletGuard>
|
|
243
246
|
|
|
244
|
-
|
|
247
|
+
<TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
|
|
248
|
+
</div>
|
|
245
249
|
</template>
|
|
246
250
|
|
|
247
251
|
<style scoped>
|
|
@@ -3,6 +3,17 @@ 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
|
+
|
|
7
|
+
/** Wallet-supplied icons are untrusted: render only inline images or https URLs. */
|
|
8
|
+
function safeIcon(icon: string | undefined): string | undefined {
|
|
9
|
+
if (!icon) return undefined
|
|
10
|
+
if (/^data:image\/(png|svg\+xml|jpeg|webp|gif);/i.test(icon)) return icon
|
|
11
|
+
try {
|
|
12
|
+
return new URL(icon).protocol === 'https:' ? icon : undefined
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
}
|
|
6
17
|
import { UiDialog, UiFormField, UiNotice, UiSelect } from '@meddleware/ui'
|
|
7
18
|
|
|
8
19
|
const { wallets, currentWallet, account, connecting, error, connect, disconnect } = useWallet()
|
|
@@ -66,7 +77,7 @@ watch(
|
|
|
66
77
|
<menu class="wallet-list">
|
|
67
78
|
<li v-for="w in wallets" :key="w.name">
|
|
68
79
|
<button type="button" class="primary" :disabled="connecting" @click="connect(w)">
|
|
69
|
-
<img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
|
|
80
|
+
<img v-if="safeIcon(w.icon)" :src="safeIcon(w.icon)" alt="" width="20" height="20" />
|
|
70
81
|
{{ w.name }}
|
|
71
82
|
</button>
|
|
72
83
|
</li>
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { ref, watch } from 'vue'
|
|
3
3
|
import { useWallet } from '@meddleware/wallet-adapter'
|
|
4
4
|
import { getReadClient } from '../lib/readClient.js'
|
|
5
|
-
import type { OwnedBlob
|
|
5
|
+
import type { OwnedBlob } from '@meddleware/walrus-client'
|
|
6
|
+
import { WALRUS_RPC_URLS, type WalrusNetwork } from '../config.js'
|
|
6
7
|
import type { Network } from '../lib/types.js'
|
|
7
8
|
import { UiNotice } from '@meddleware/ui'
|
|
8
9
|
|
|
@@ -46,10 +47,9 @@ async function load(): Promise<void> {
|
|
|
46
47
|
blobs.value = []
|
|
47
48
|
hasLoaded.value = false
|
|
48
49
|
try {
|
|
49
|
-
const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } =
|
|
50
|
-
await import('../lib/walrus.js')
|
|
50
|
+
const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } = await import('@meddleware/walrus-client')
|
|
51
51
|
const suiClient = getReadClient(props.network as Network)
|
|
52
|
-
const client = createWalrusClient(props.network)
|
|
52
|
+
const client = createWalrusClient({ network: props.network, rpcUrl: WALRUS_RPC_URLS[props.network] })
|
|
53
53
|
const raw = await fetchOwnedWalrusBlobs(suiClient, client, addr)
|
|
54
54
|
blobs.value = raw.map((b) => ({ ...b, url: walrusBlobUrl(props.network, b.blobId) }))
|
|
55
55
|
hasLoaded.value = true
|
|
@@ -4,8 +4,7 @@
|
|
|
4
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
|
-
import { WALRUS_RELAY_HOSTS, PUBLIC_WALRUS_RELAY_HOSTS } from '../config.js'
|
|
8
|
-
import type { WalrusNetwork } from '../lib/walrus.js'
|
|
7
|
+
import { WALRUS_RELAY_HOSTS, PUBLIC_WALRUS_RELAY_HOSTS, type WalrusNetwork } from '../config.js'
|
|
9
8
|
|
|
10
9
|
export type { RelayAccessOptions, RelayOption }
|
|
11
10
|
|
package/src/config.ts
CHANGED
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
// Vite env vars so they can be set at build/deploy time without code changes.
|
|
3
3
|
// See .env.example.
|
|
4
4
|
import type { Network } from './lib/types.js'
|
|
5
|
-
import type
|
|
5
|
+
import { relayGateConfig, type RelayGateConfig } from '@meddleware/walrus-relay'
|
|
6
6
|
|
|
7
7
|
const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Public
|
|
10
|
+
* Public gRPC fullnode URLs per network (overridable via `VITE_RPC_*`).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* These are **gRPC(-web)** endpoints for `SuiGrpcClient` — public fullnodes have
|
|
13
|
+
* deprecated JSON-RPC, so a JSON-RPC-only URL will not work here. Operators should
|
|
14
|
+
* set `VITE_RPC_*` to their preferred (often paid) gRPC endpoint for production to
|
|
15
|
+
* avoid public rate limits.
|
|
16
16
|
*/
|
|
17
17
|
export const RPC_URLS: Record<Network, string> = {
|
|
18
18
|
mainnet: env.VITE_RPC_MAINNET || 'https://fullnode.mainnet.sui.io:443',
|
|
19
|
-
testnet: env.VITE_RPC_TESTNET || 'https://
|
|
19
|
+
testnet: env.VITE_RPC_TESTNET || 'https://fullnode.testnet.sui.io:443',
|
|
20
20
|
localnet: env.VITE_RPC_LOCALNET || 'http://127.0.0.1:9000',
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -56,7 +56,7 @@ export const SELECTABLE_NETWORKS: Network[] = ['testnet', 'mainnet']
|
|
|
56
56
|
// ---------------------------------------------------------------------------
|
|
57
57
|
// Walrus is a separate protocol from Sui; only testnet/mainnet exist (no
|
|
58
58
|
// localnet). Icon uploads go through an upload relay (direct-to-storage-node
|
|
59
|
-
// writes fail from browsers).
|
|
59
|
+
// writes fail from browsers). The upload is `@meddleware/walrus-client/flow`; see the relay runbook in
|
|
60
60
|
// README.md / CLAUDE.md for the operator side.
|
|
61
61
|
|
|
62
62
|
/** A Walrus network — a strict subset of {@link Network} (no localnet). */
|
|
@@ -89,15 +89,12 @@ export function isOperatorRelayConfigured(network: WalrusNetwork): boolean {
|
|
|
89
89
|
/**
|
|
90
90
|
* gRPC fullnode URL for the Walrus icon-upload client (a `SuiGrpcClient`).
|
|
91
91
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* are not interchangeable — pointing the Walrus gRPC client at a JSON-RPC URL breaks it —
|
|
95
|
-
* so this has its own `VITE_WALRUS_RPC_*` override rather than reusing `VITE_RPC_*`.
|
|
96
|
-
* Defaults to the public gRPC fullnodes (rate-limited); set your own for production.
|
|
92
|
+
* Defaults to {@link RPC_URLS} (also gRPC); `VITE_WALRUS_RPC_*` overrides it only when
|
|
93
|
+
* Walrus traffic should use a different fullnode from the deploy path.
|
|
97
94
|
*/
|
|
98
95
|
export const WALRUS_RPC_URLS: Record<WalrusNetwork, string> = {
|
|
99
|
-
testnet: env.VITE_WALRUS_RPC_TESTNET ||
|
|
100
|
-
mainnet: env.VITE_WALRUS_RPC_MAINNET ||
|
|
96
|
+
testnet: env.VITE_WALRUS_RPC_TESTNET || RPC_URLS.testnet,
|
|
97
|
+
mainnet: env.VITE_WALRUS_RPC_MAINNET || RPC_URLS.mainnet,
|
|
101
98
|
}
|
|
102
99
|
|
|
103
100
|
/**
|
|
@@ -116,27 +113,29 @@ export const WALRUS_MAX_TIP_MIST: bigint = BigInt(env.VITE_WALRUS_MAX_TIP_MIST |
|
|
|
116
113
|
// NFT-gated relay access (optional)
|
|
117
114
|
// ---------------------------------------------------------------------------
|
|
118
115
|
// When the operator relay runs behind an `nft-gate` gateway, using it requires
|
|
119
|
-
// holding the gate's access NFT.
|
|
120
|
-
//
|
|
121
|
-
//
|
|
116
|
+
// holding the gate's access NFT. The operator sets the gate id, soulbound flag (default true) and
|
|
117
|
+
// price per network via env; `relayGateConfig` (walrus-relay) fixes the package and PlatformConfig
|
|
118
|
+
// to the published access_gate deployment. When no gate id is set the relay is ungated.
|
|
122
119
|
|
|
123
|
-
|
|
120
|
+
/** The gate config for a network, or `null` (ungated) without a gate id or a recorded deployment. */
|
|
121
|
+
export function parseAccessGate(network: WalrusNetwork, envSource: Record<string, string | undefined> = env): RelayGateConfig | null {
|
|
124
122
|
const NET = network.toUpperCase()
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
123
|
+
const gateId = envSource[`VITE_ACCESS_GATE_ID_${NET}`]
|
|
124
|
+
if (!gateId) return null
|
|
125
|
+
try {
|
|
126
|
+
return relayGateConfig(network, {
|
|
127
|
+
gateId,
|
|
128
|
+
soulbound: (envSource[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] ?? 'true') !== 'false',
|
|
129
|
+
priceMist: BigInt(envSource[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
|
|
130
|
+
})
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.warn(`[token-deployer-ui] access gate for ${network} disabled: ${e instanceof Error ? e.message : String(e)}`)
|
|
133
|
+
return null
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
/** The access gate per Walrus network, or `null` when gating is not configured. */
|
|
139
|
-
export const ACCESS_GATE: Record<WalrusNetwork,
|
|
138
|
+
export const ACCESS_GATE: Record<WalrusNetwork, RelayGateConfig | null> = {
|
|
140
139
|
testnet: parseAccessGate('testnet'),
|
|
141
140
|
mainnet: parseAccessGate('mainnet'),
|
|
142
141
|
}
|
|
@@ -73,16 +73,6 @@ export interface BuildFinalizeArgs {
|
|
|
73
73
|
gasBudget: bigint
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/** Whether a finalize transaction is needed at all for this config. */
|
|
77
|
-
export function needsFinalize(config: TokenConfig, sender: string): boolean {
|
|
78
|
-
return (
|
|
79
|
-
config.initialSupply > 0n ||
|
|
80
|
-
config.supplyPolicy === 'fixed' ||
|
|
81
|
-
config.metadataPolicy === 'frozen' ||
|
|
82
|
-
config.recipient.toLowerCase() !== sender.toLowerCase()
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
76
|
/**
|
|
87
77
|
* Finalize PTB: register the coin in the registry, mint initial supply, apply
|
|
88
78
|
* supply/metadata policy, and route caps to the recipient.
|
package/src/lib/deploy.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Decoupled from the wallet via an `Executor` so it is unit-testable.
|
|
3
3
|
|
|
4
4
|
import type { Transaction } from '@mysten/sui/transactions'
|
|
5
|
+
import type { SuiClientTypes } from '@mysten/sui/client'
|
|
5
6
|
import { patchTemplateModule } from './template.js'
|
|
6
7
|
import {
|
|
7
8
|
buildPublishTransaction,
|
|
@@ -25,6 +26,41 @@ export interface SuiTxResult {
|
|
|
25
26
|
effects?: { status?: { status?: string; error?: string } }
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/** The core-API execution result {@link toSuiTxResult} consumes (effects + object types). */
|
|
30
|
+
export type CoreExecutionResult = SuiClientTypes.TransactionResult<{
|
|
31
|
+
effects: true
|
|
32
|
+
objectTypes: true
|
|
33
|
+
}>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Map a core-API (gRPC) execution result onto {@link SuiTxResult}: created/mutated objects
|
|
37
|
+
* come from `effects.changedObjects` (types from the `objectTypes` map), and a newly written
|
|
38
|
+
* package becomes the `published` entry. Failed transactions are returned with a failure
|
|
39
|
+
* status (not thrown) so {@link deployToken}'s `assertSuccess` reports them uniformly.
|
|
40
|
+
*/
|
|
41
|
+
export function toSuiTxResult(res: CoreExecutionResult): SuiTxResult {
|
|
42
|
+
const tx = res.Transaction ?? res.FailedTransaction
|
|
43
|
+
const types = tx.objectTypes ?? {}
|
|
44
|
+
const objectChanges: NonNullable<SuiTxResult['objectChanges']> = []
|
|
45
|
+
for (const c of tx.effects?.changedObjects ?? []) {
|
|
46
|
+
if (c.outputState === 'PackageWrite') {
|
|
47
|
+
if (c.idOperation === 'Created') objectChanges.push({ type: 'published', packageId: c.objectId })
|
|
48
|
+
} else if (c.outputState === 'ObjectWrite') {
|
|
49
|
+
objectChanges.push({
|
|
50
|
+
type: c.idOperation === 'Created' ? 'created' : 'mutated',
|
|
51
|
+
objectId: c.objectId,
|
|
52
|
+
objectType: types[c.objectId],
|
|
53
|
+
version: c.outputVersion ?? undefined,
|
|
54
|
+
digest: c.outputDigest ?? undefined,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const status = tx.status.success
|
|
59
|
+
? { status: 'success' }
|
|
60
|
+
: { status: 'failure', error: tx.status.error.message }
|
|
61
|
+
return { digest: tx.digest, objectChanges, effects: { status } }
|
|
62
|
+
}
|
|
63
|
+
|
|
28
64
|
export interface Executor {
|
|
29
65
|
/** Sign with the wallet and execute; must request object changes + effects. */
|
|
30
66
|
signAndExecute(tx: Transaction): Promise<SuiTxResult>
|
|
@@ -51,10 +87,16 @@ export interface DeployArgs {
|
|
|
51
87
|
onStep?: (step: DeployStep) => void
|
|
52
88
|
}
|
|
53
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Only an explicit `success` effects status counts: a missing status (an executor that did not
|
|
92
|
+
* return effects) is treated as failure, never as success.
|
|
93
|
+
*/
|
|
54
94
|
function assertSuccess(res: SuiTxResult, what: string): void {
|
|
55
95
|
const status = res.effects?.status?.status
|
|
56
|
-
if (status
|
|
57
|
-
throw new Error(
|
|
96
|
+
if (status !== 'success') {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`${what} failed: ${res.effects?.status?.error ?? status ?? 'no effects status returned'}`,
|
|
99
|
+
)
|
|
58
100
|
}
|
|
59
101
|
}
|
|
60
102
|
|
|
@@ -67,7 +109,7 @@ export async function deployToken(args: DeployArgs): Promise<PublishResult> {
|
|
|
67
109
|
const structName = deriveStructName(config.moduleName)
|
|
68
110
|
const normalizedConfig: TokenConfig = { ...config, structName }
|
|
69
111
|
const recipient = normalizedConfig.recipient || args.sender
|
|
70
|
-
const isE2e = import.meta.env.VITE_E2E === '1'
|
|
112
|
+
const isE2e = import.meta.env.MODE === 'e2e' && import.meta.env.VITE_E2E === '1'
|
|
71
113
|
|
|
72
114
|
onStep?.('patching')
|
|
73
115
|
const moduleBytes = await patchTemplateModule({
|