@meddleware/token-deployer-ui 0.0.8 → 0.0.10
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/.env.example +8 -11
- package/.github/workflows/e2e-realchain.yml +1 -1
- package/.github/workflows/node-ci.yml +9 -0
- package/.github/workflows/npm-publish.yml +16 -2
- package/.htmlvalidate.mjs +30 -0
- package/CLAUDE.md +14 -7
- package/Dockerfile +1 -1
- package/LICENSE +11 -118
- package/README.md +3 -5
- package/eslint.config.ts +63 -0
- package/index.html +11 -2
- package/package.json +32 -10
- package/postcss.config.js +8 -0
- package/public/og-image.png +0 -0
- package/public/robots.txt +1 -1
- package/public/sitemap.xml +1 -1
- package/scripts/e2e-deploy.mjs +22 -17
- package/scripts/e2e-localnet.mjs +14 -12
- package/scripts/e2e-walrus-browser.mjs +1 -4
- package/scripts/gen-og-image.mjs +2 -2
- package/scripts/publish-localnet.mjs +5 -4
- package/src/App.vue +8 -6
- package/src/component-styles.css +5 -38
- package/src/components/ConfigForm.vue +74 -28
- package/src/components/DeployProgress.vue +2 -4
- package/src/components/GithubPush.vue +22 -7
- package/src/components/IconPicker.vue +103 -42
- package/src/components/LicensePicker.vue +66 -66
- package/src/components/MyTokens.vue +125 -0
- package/src/components/ResultPanel.vue +2 -4
- package/src/components/ReviewPanel.vue +1 -1
- package/src/components/TermsDialog.vue +28 -27
- package/src/components/TokenDeployerView.vue +109 -40
- package/src/components/WalletBar.vue +67 -78
- package/src/components/WalrusBlobBrowser.vue +33 -25
- package/src/composables/useAccessGate.ts +7 -2
- package/src/composables/useWalrusRelay.ts +1 -1
- package/src/config.ts +10 -13
- package/src/lib/accessGate.ts +28 -14
- package/src/lib/buildPublishTx.ts +1 -1
- package/src/lib/deploy.ts +36 -0
- package/src/lib/deployExecutor.ts +14 -12
- package/src/lib/generatePackage.ts +18 -13
- package/src/lib/licenses.ts +4 -3
- package/src/lib/listMyTokens.ts +41 -0
- package/src/lib/readClient.ts +15 -7
- package/src/lib/types.ts +1 -1
- package/src/lib/validation.ts +8 -4
- package/src/lib/walrus-constants.ts +1 -1
- package/src/lib/walrus.ts +22 -11
- package/src/main.ts +46 -58
- package/src/styles.css +13 -5
- package/src/template-src/files.json +7 -7
- package/stylelint.config.js +33 -0
- package/test-results/.last-run.json +1 -1
- package/tests/accessGate.test.ts +26 -21
- package/tests/deploy.test.ts +70 -2
- package/tests/generatePackage.test.ts +11 -0
- package/tests/licenses.test.ts +6 -3
- package/tests/listMyTokens.test.ts +30 -0
- package/tests/useAccessGate.test.ts +15 -19
- package/typedoc.json +2 -2
- package/vite.config.ts +9 -1
- package/src/components/WalrusIconUpload.vue +0 -205
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>
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
// Builds a deploy Executor using the wallet-adapter's shared signing capability
|
|
2
|
-
// +
|
|
2
|
+
// + the app's gRPC client for execution (returns the object changes required by
|
|
3
|
+
// extractPublishResult).
|
|
3
4
|
//
|
|
4
5
|
// The wallet-adapter's own Executor only returns { digest } which is not sufficient
|
|
5
6
|
// for the token deploy flow; we sign via the shared wallet connection but execute
|
|
6
|
-
// via
|
|
7
|
+
// via our own client so we can request effects + object types.
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { fromBase64 } from '@mysten/sui/utils'
|
|
9
10
|
import { useWallet } from '@meddleware/wallet-adapter'
|
|
10
11
|
import type { Transaction } from '@mysten/sui/transactions'
|
|
12
|
+
import { toSuiTxResult } from './deploy.js'
|
|
11
13
|
import type { SuiTxResult, Executor } from './deploy.js'
|
|
12
14
|
import type { Network } from './types.js'
|
|
13
|
-
import {
|
|
15
|
+
import { getReadClient } from './readClient.js'
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Build a deploy Executor bound to the currently connected wallet (via the
|
|
17
|
-
* wallet-adapter singleton) and the given network's
|
|
19
|
+
* wallet-adapter singleton) and the given network's gRPC client.
|
|
18
20
|
*
|
|
19
21
|
* The returned executor signs via `sui:signTransaction` and executes via
|
|
20
|
-
* `
|
|
22
|
+
* `executeTransaction` with `effects + objectTypes`, mapped by {@link toSuiTxResult}
|
|
21
23
|
* so the caller can extract the published package ID, coin type, TreasuryCap, etc.
|
|
22
24
|
*/
|
|
23
25
|
export async function buildDeployExecutor(network: Network): Promise<Executor> {
|
|
@@ -26,7 +28,7 @@ export async function buildDeployExecutor(network: Network): Promise<Executor> {
|
|
|
26
28
|
const acct = account.value
|
|
27
29
|
if (!wallet || !acct) throw new Error('Connect a wallet first.')
|
|
28
30
|
|
|
29
|
-
const suiClient =
|
|
31
|
+
const suiClient = getReadClient(network)
|
|
30
32
|
const chain = `sui:${network}` as const
|
|
31
33
|
|
|
32
34
|
const signFeature = wallet.features['sui:signTransaction'] as
|
|
@@ -47,12 +49,12 @@ export async function buildDeployExecutor(network: Network): Promise<Executor> {
|
|
|
47
49
|
account: acct,
|
|
48
50
|
chain,
|
|
49
51
|
})
|
|
50
|
-
const res = await suiClient.
|
|
51
|
-
|
|
52
|
-
signature,
|
|
53
|
-
|
|
52
|
+
const res = await suiClient.executeTransaction({
|
|
53
|
+
transaction: fromBase64(bytes),
|
|
54
|
+
signatures: [signature],
|
|
55
|
+
include: { effects: true, objectTypes: true },
|
|
54
56
|
})
|
|
55
|
-
return res
|
|
57
|
+
return toSuiTxResult(res)
|
|
56
58
|
},
|
|
57
59
|
async waitForTransaction(digest: string): Promise<void> {
|
|
58
60
|
await suiClient.waitForTransaction({ digest })
|
|
@@ -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 {
|
|
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
|
|
|
@@ -117,12 +122,12 @@ function renderMoveToml(cfg: TokenConfig): string {
|
|
|
117
122
|
function renderSource(cfg: TokenConfig): string {
|
|
118
123
|
let out = files['source.move']
|
|
119
124
|
|
|
120
|
-
// license header (lines 1-2)
|
|
125
|
+
// license header (lines 1-2) — always rewritten, whatever licence the template itself carries
|
|
121
126
|
const lines = out.split('\n')
|
|
122
127
|
if (isProprietary(cfg.license)) {
|
|
123
128
|
lines[0] = '// SPDX-License-Identifier: UNLICENSED'
|
|
124
129
|
lines[1] = '// All rights reserved. Proprietary and not licensed for redistribution.'
|
|
125
|
-
} else
|
|
130
|
+
} else {
|
|
126
131
|
lines[0] = `// SPDX-License-Identifier: ${spdxId(cfg.license)}`
|
|
127
132
|
lines[1] = `// Licensed under the ${spdxId(cfg.license)} license; see the LICENSE file.`
|
|
128
133
|
}
|
|
@@ -152,18 +157,18 @@ function renderPublishScript(cfg: TokenConfig): string {
|
|
|
152
157
|
return out
|
|
153
158
|
}
|
|
154
159
|
|
|
160
|
+
/** The template README's licence line, whatever licence the template itself ships with. */
|
|
161
|
+
const TEMPLATE_LICENSE_LINE = /^(?:CC0 1\.0 Universal|BSD Zero Clause License).*$/m
|
|
162
|
+
|
|
155
163
|
function renderReadme(cfg: TokenConfig): string {
|
|
156
|
-
|
|
164
|
+
const out = applyDocPlaceholders(files['README.md'], cfg)
|
|
157
165
|
const licenseName = cfg.licenseName || spdxId(cfg.license)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
'All rights reserved. This package is proprietary and not licensed for redistribution.'
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
out = out.replace(/^CC0 1\.0 Universal.*$/m, `${licenseName} — see the LICENSE file.`)
|
|
165
|
-
}
|
|
166
|
-
return out
|
|
166
|
+
return out.replace(
|
|
167
|
+
TEMPLATE_LICENSE_LINE,
|
|
168
|
+
isProprietary(cfg.license)
|
|
169
|
+
? 'All rights reserved. This package is proprietary and not licensed for redistribution.'
|
|
170
|
+
: `${licenseName} — see the LICENSE file.`,
|
|
171
|
+
)
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
/**
|
package/src/lib/licenses.ts
CHANGED
|
@@ -23,9 +23,9 @@ export const NO_LICENSE: SpdxLicense = {
|
|
|
23
23
|
deprecated: false,
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** Ids surfaced first in the picker.
|
|
26
|
+
/** Ids surfaced first in the picker. 0BSD is the project default; CC0 stays available. */
|
|
27
27
|
export const POPULAR_LICENSE_IDS = [
|
|
28
|
-
'
|
|
28
|
+
'0BSD',
|
|
29
29
|
'MIT',
|
|
30
30
|
'Apache-2.0',
|
|
31
31
|
'BSD-3-Clause',
|
|
@@ -37,9 +37,10 @@ export const POPULAR_LICENSE_IDS = [
|
|
|
37
37
|
'MPL-2.0',
|
|
38
38
|
'ISC',
|
|
39
39
|
'Unlicense',
|
|
40
|
+
'CC0-1.0',
|
|
40
41
|
] as const
|
|
41
42
|
|
|
42
|
-
export const DEFAULT_LICENSE_ID = '
|
|
43
|
+
export const DEFAULT_LICENSE_ID = '0BSD'
|
|
43
44
|
|
|
44
45
|
type Fetcher = typeof fetch
|
|
45
46
|
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
// A type filter without type params matches every TreasuryCap<T> instantiation.
|
|
23
|
+
const { objects } = await client.listOwnedObjects({ owner, type: '0x2::coin::TreasuryCap' })
|
|
24
|
+
const seen = new Set<string>()
|
|
25
|
+
const out: DeployedToken[] = []
|
|
26
|
+
for (const obj of objects) {
|
|
27
|
+
// gRPC may render the framework address in long form (0x000…02).
|
|
28
|
+
const m = obj.type.match(/^0x0*2::coin::TreasuryCap<(.+)>$/)
|
|
29
|
+
if (!m) continue
|
|
30
|
+
const coinType = m[1]
|
|
31
|
+
if (seen.has(coinType)) continue
|
|
32
|
+
seen.add(coinType)
|
|
33
|
+
out.push({
|
|
34
|
+
coinType,
|
|
35
|
+
packageId: coinType.split('::')[0] ?? '',
|
|
36
|
+
treasuryCapId: obj.objectId,
|
|
37
|
+
label: coinType.split('::').pop() ?? coinType,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
package/src/lib/readClient.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
// Cached
|
|
2
|
-
//
|
|
3
|
-
import {
|
|
1
|
+
// Cached gRPC client per network, shared by read-only queries (listOwnedObjects, etc.) and
|
|
2
|
+
// the deploy executor. Public fullnodes have deprecated JSON-RPC, so this is a SuiGrpcClient.
|
|
3
|
+
import { SuiGrpcClient } from '@mysten/sui/grpc'
|
|
4
4
|
import type { Network } from './types.js'
|
|
5
5
|
import { RPC_URLS } from '../config.js'
|
|
6
6
|
|
|
7
|
-
const clients = new Map<Network,
|
|
7
|
+
const clients = new Map<Network, SuiGrpcClient>()
|
|
8
8
|
|
|
9
|
-
/** Return a memoised {@link
|
|
10
|
-
export function getReadClient(network: Network):
|
|
9
|
+
/** Return a memoised {@link SuiGrpcClient} for the given network. */
|
|
10
|
+
export function getReadClient(network: Network): SuiGrpcClient {
|
|
11
11
|
let c = clients.get(network)
|
|
12
12
|
if (!c) {
|
|
13
|
-
c = new
|
|
13
|
+
c = new SuiGrpcClient({ network, baseUrl: RPC_URLS[network] })
|
|
14
14
|
clients.set(network, c)
|
|
15
15
|
}
|
|
16
16
|
return c
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Replace the client for `network` (E2E builds only — lets `main.ts` inject a stub client in
|
|
21
|
+
* place of the network, since gRPC-web responses are binary and cannot be faked via `fetch`).
|
|
22
|
+
*/
|
|
23
|
+
export function setReadClient(network: Network, client: SuiGrpcClient): void {
|
|
24
|
+
clients.set(network, client)
|
|
25
|
+
}
|
package/src/lib/types.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface TokenConfig {
|
|
|
41
41
|
/** Address that receives the caps + initial supply. Defaults to the connected wallet. */
|
|
42
42
|
recipient: string
|
|
43
43
|
|
|
44
|
-
/** SPDX id (e.g. "
|
|
44
|
+
/** SPDX id (e.g. "0BSD", "MIT") or "NONE" for proprietary. Affects the downloadable package only. */
|
|
45
45
|
license: string
|
|
46
46
|
/** Human-readable license name for the README (defaults to the SPDX id). */
|
|
47
47
|
licenseName?: string
|
package/src/lib/validation.ts
CHANGED
|
@@ -123,18 +123,22 @@ export function validateForm(form: ValidatableForm): FormErrors {
|
|
|
123
123
|
|
|
124
124
|
if (!form.symbol.trim()) errors.symbol = 'Required'
|
|
125
125
|
else if (form.symbol.length > 32) errors.symbol = 'Maximum 32 characters'
|
|
126
|
-
else if (!SAFE_TEXT.test(form.symbol))
|
|
126
|
+
else if (!SAFE_TEXT.test(form.symbol))
|
|
127
|
+
errors.symbol = 'Double quotes and backslashes are not supported'
|
|
127
128
|
|
|
128
129
|
if (!form.name.trim()) errors.name = 'Required'
|
|
129
130
|
else if (form.name.length > 64) errors.name = 'Maximum 64 characters'
|
|
130
|
-
else if (!SAFE_TEXT.test(form.name))
|
|
131
|
+
else if (!SAFE_TEXT.test(form.name))
|
|
132
|
+
errors.name = 'Double quotes and backslashes are not supported'
|
|
131
133
|
|
|
132
134
|
if (form.description.length > 256) errors.description = 'Maximum 256 characters'
|
|
133
|
-
else if (!SAFE_TEXT.test(form.description))
|
|
135
|
+
else if (!SAFE_TEXT.test(form.description))
|
|
136
|
+
errors.description = 'Double quotes and backslashes are not supported'
|
|
134
137
|
|
|
135
138
|
if (form.iconUrl) {
|
|
136
139
|
if (form.iconUrl.length > 512) errors.iconUrl = 'Maximum 512 characters'
|
|
137
|
-
else if (!SAFE_TEXT.test(form.iconUrl))
|
|
140
|
+
else if (!SAFE_TEXT.test(form.iconUrl))
|
|
141
|
+
errors.iconUrl = 'Double quotes and backslashes are not supported'
|
|
138
142
|
else if (!hasAllowedIconScheme(form.iconUrl)) errors.iconUrl = 'Use an https:// or ipfs:// URL'
|
|
139
143
|
}
|
|
140
144
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Lightweight Walrus constants with NO heavy imports, so both the (lazily loaded)
|
|
2
|
-
// `lib/walrus.ts` client and the eagerly rendered `
|
|
2
|
+
// `lib/walrus.ts` client and the eagerly rendered `IconPicker.vue` component
|
|
3
3
|
// can share a single source of truth without pulling `@mysten/walrus` into the
|
|
4
4
|
// component's chunk.
|
|
5
5
|
|
package/src/lib/walrus.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { SuiGrpcClient } from '@mysten/sui/grpc'
|
|
14
14
|
import { walrus, blobIdFromInt } from '@mysten/walrus'
|
|
15
|
-
import type {
|
|
15
|
+
import type { ClientWithCoreApi } from '@mysten/sui/client'
|
|
16
16
|
import { WALRUS_RELAY_HOSTS, WALRUS_MAX_TIP_MIST, WALRUS_RPC_URLS } from '../config.js'
|
|
17
17
|
|
|
18
18
|
export { ICON_EPOCHS } from './walrus-constants.js'
|
|
@@ -107,33 +107,44 @@ export interface OwnedBlob {
|
|
|
107
107
|
certified: boolean
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Unwrap a Move-struct field bag from a core `json` value. The gRPC/core API returns struct
|
|
112
|
+
* fields flat; the old JSON-RPC shape nested them under `.fields`. Tolerate both, since the
|
|
113
|
+
* SDK documents that the `json` shape may vary between API implementations.
|
|
114
|
+
*/
|
|
115
|
+
function structFields(v: unknown): Record<string, unknown> | undefined {
|
|
116
|
+
if (!v || typeof v !== 'object') return undefined
|
|
117
|
+
const o = v as Record<string, unknown>
|
|
118
|
+
const nested = o.fields
|
|
119
|
+
return nested && typeof nested === 'object' ? (nested as Record<string, unknown>) : o
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
/**
|
|
111
123
|
* Return all Walrus blobs owned by `owner`. Uses `getBlobType()` for dynamic
|
|
112
124
|
* package resolution so no hardcoded addresses are needed.
|
|
113
125
|
*/
|
|
114
126
|
export async function fetchOwnedWalrusBlobs(
|
|
115
|
-
suiClient:
|
|
127
|
+
suiClient: ClientWithCoreApi,
|
|
116
128
|
walrusClient: ReturnType<typeof createWalrusClient>,
|
|
117
129
|
owner: string,
|
|
118
130
|
): Promise<OwnedBlob[]> {
|
|
119
131
|
const blobType = await walrusClient.walrus.getBlobType()
|
|
120
|
-
const {
|
|
132
|
+
const { objects } = await suiClient.core.listOwnedObjects({
|
|
121
133
|
owner,
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
type: blobType,
|
|
135
|
+
include: { json: true },
|
|
124
136
|
})
|
|
125
137
|
const blobs: OwnedBlob[] = []
|
|
126
|
-
for (const
|
|
127
|
-
|
|
128
|
-
const fields = (item.data.content as { fields?: Record<string, unknown> } | undefined)?.fields
|
|
138
|
+
for (const obj of objects) {
|
|
139
|
+
const fields = structFields(obj.json)
|
|
129
140
|
if (!fields) continue
|
|
130
|
-
const storage = fields.storage
|
|
141
|
+
const storage = structFields(fields.storage)
|
|
131
142
|
try {
|
|
132
143
|
blobs.push({
|
|
133
|
-
objectId:
|
|
144
|
+
objectId: obj.objectId,
|
|
134
145
|
blobId: blobIdFromInt(BigInt(fields.blob_id as string)),
|
|
135
146
|
size: Number(fields.size),
|
|
136
|
-
endEpoch: Number(storage?.
|
|
147
|
+
endEpoch: Number(storage?.end_epoch ?? 0),
|
|
137
148
|
certified: fields.certified_epoch !== null && fields.certified_epoch !== undefined,
|
|
138
149
|
})
|
|
139
150
|
} catch {
|
package/src/main.ts
CHANGED
|
@@ -6,12 +6,19 @@ import App from './App.vue'
|
|
|
6
6
|
import './styles.css'
|
|
7
7
|
import './component-styles.css'
|
|
8
8
|
import { configureWasm } from './lib/template.js'
|
|
9
|
-
|
|
9
|
+
// Static: readClient is already in the main chunk (IconPicker, WalrusBlobBrowser and listMyTokens
|
|
10
|
+
// import it), so a dynamic import in the E2E block below could not split it out anyway.
|
|
11
|
+
import { getReadClient, setReadClient } from './lib/readClient.js'
|
|
12
|
+
import type { SuiGrpcClient } from '@mysten/sui/grpc'
|
|
13
|
+
import { useColorMode, useSeason } from '@meddleware/ui'
|
|
10
14
|
|
|
11
15
|
// Apply the colour mode before mount so there is no theme flash. Defaults to
|
|
12
16
|
// `system` (follows the OS) while enabling the in-app light/dark/system control.
|
|
13
17
|
useColorMode('system')
|
|
14
18
|
|
|
19
|
+
// Enable seasonal theming (sets data-season on <html>; seasons.css is imported above).
|
|
20
|
+
useSeason()
|
|
21
|
+
|
|
15
22
|
// Vite serves the wasm as an asset URL; wasm-bindgen's init loads it on demand.
|
|
16
23
|
import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_bg.wasm?url'
|
|
17
24
|
|
|
@@ -19,73 +26,54 @@ import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_b
|
|
|
19
26
|
// doesn't block initial page load.
|
|
20
27
|
configureWasm(wasmUrl)
|
|
21
28
|
|
|
22
|
-
// E2E-only: register mock wallet and expose getSuiClient for headless tests.
|
|
29
|
+
// E2E-only: stub the Sui client, register a mock wallet and expose getSuiClient for headless tests.
|
|
23
30
|
// Wrapped in async IIFE to ensure wallet is registered BEFORE app mounts (timing critical).
|
|
24
31
|
// Tree-shaking removes this entire block from production when VITE_E2E !== '1'.
|
|
25
32
|
;(async () => {
|
|
26
33
|
if (import.meta.env.VITE_E2E === '1') {
|
|
27
34
|
const { getWallets } = await import('@mysten/wallet-standard')
|
|
28
|
-
const { getReadClient: getSuiClient } = await import('./lib/readClient.js')
|
|
29
35
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
// Stub gRPC client in place of the network (gRPC-web is binary, so it cannot be faked
|
|
37
|
+
// at the fetch layer). Returns a successful publish whose effects carry the created
|
|
38
|
+
// TreasuryCap/MetadataCap, and empty owned-object lists.
|
|
39
|
+
const publishDigest = 'E2E' + 'a'.repeat(41)
|
|
33
40
|
const packageId = '0x' + 'bb'.repeat(32)
|
|
34
|
-
const
|
|
41
|
+
const treasuryCapId = '0x' + 'cc'.repeat(32)
|
|
35
42
|
const metadataCapId = '0x' + 'dd'.repeat(32)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const created = (objectId: string) => ({
|
|
44
|
+
objectId,
|
|
45
|
+
outputState: 'ObjectWrite',
|
|
46
|
+
idOperation: 'Created',
|
|
47
|
+
outputVersion: '1',
|
|
48
|
+
outputDigest: publishDigest,
|
|
49
|
+
})
|
|
50
|
+
const executed = {
|
|
51
|
+
$kind: 'Transaction',
|
|
52
|
+
Transaction: {
|
|
53
|
+
digest: publishDigest,
|
|
54
|
+
status: { success: true, error: null },
|
|
55
|
+
effects: {
|
|
56
|
+
changedObjects: [
|
|
57
|
+
{ objectId: packageId, outputState: 'PackageWrite', idOperation: 'Created' },
|
|
58
|
+
created(treasuryCapId),
|
|
59
|
+
created(metadataCapId),
|
|
60
|
+
],
|
|
49
61
|
},
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
objectId: metadataCapId,
|
|
62
|
+
objectTypes: {
|
|
63
|
+
[treasuryCapId]: `0x2::coin::TreasuryCap<${packageId}::mytoken::MYTOKEN>`,
|
|
64
|
+
[metadataCapId]: `0x2::coin_registry::MetadataCap<${packageId}::mytoken::MYTOKEN>`,
|
|
54
65
|
},
|
|
55
|
-
|
|
56
|
-
events: [],
|
|
57
|
-
transaction: null,
|
|
58
|
-
balanceChanges: null,
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
62
|
-
const url = typeof input === 'string' ? input : input.toString()
|
|
63
|
-
const method = init?.method || 'GET'
|
|
64
|
-
const isRpcCall = (url.includes('fullnode') || url.includes('rpc')) && method === 'POST'
|
|
65
|
-
|
|
66
|
-
if (isRpcCall) {
|
|
67
|
-
const body = init?.body ? JSON.parse(String(init.body)) : {}
|
|
68
|
-
const rpcMethod = body.method
|
|
69
|
-
|
|
70
|
-
if (import.meta.env.VITE_E2E === '1') {
|
|
71
|
-
console.log('[E2E Fetch Mock]', rpcMethod, 'URL:', url)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const result =
|
|
75
|
-
rpcMethod === 'sui_executeTransactionBlock' || rpcMethod === 'sui_getTransactionBlock'
|
|
76
|
-
? mockRpcResponse
|
|
77
|
-
: rpcMethod === 'suix_getCoins' || rpcMethod === 'suix_getBalance'
|
|
78
|
-
? { data: [], nextCursor: null, hasNextPage: false }
|
|
79
|
-
: null
|
|
80
|
-
|
|
81
|
-
return new Response(JSON.stringify({ jsonrpc: '2.0', id: body.id || 1, result }), {
|
|
82
|
-
status: 200,
|
|
83
|
-
headers: { 'content-type': 'application/json' },
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return originalFetch(input, init)
|
|
66
|
+
},
|
|
88
67
|
}
|
|
68
|
+
const noObjects = async () => ({ objects: [], hasNextPage: false, cursor: null })
|
|
69
|
+
const stubClient = {
|
|
70
|
+
executeTransaction: async () => executed,
|
|
71
|
+
waitForTransaction: async () => executed,
|
|
72
|
+
listOwnedObjects: noObjects,
|
|
73
|
+
core: { listOwnedObjects: noObjects },
|
|
74
|
+
} as unknown as SuiGrpcClient
|
|
75
|
+
setReadClient('testnet', stubClient)
|
|
76
|
+
setReadClient('mainnet', stubClient)
|
|
89
77
|
|
|
90
78
|
const E2E_ADDR = '0x' + 'a'.repeat(64)
|
|
91
79
|
const E2E_PUB_KEY = new Uint8Array(32)
|
|
@@ -151,7 +139,7 @@ configureWasm(wasmUrl)
|
|
|
151
139
|
}
|
|
152
140
|
|
|
153
141
|
// Expose getSuiClient for tests to build transactions
|
|
154
|
-
w.__getSuiClient =
|
|
142
|
+
w.__getSuiClient = getReadClient
|
|
155
143
|
}
|
|
156
144
|
|
|
157
145
|
createApp(App).mount('#app')
|
package/src/styles.css
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
Token variables come from @meddleware/design-tokens, imported before this.
|
|
3
3
|
Component utility classes live in component-styles.css. */
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
justify-
|
|
5
|
+
/* The colour-mode control is the page header's first grid item, aligned to the inline end. */
|
|
6
|
+
.theme-toggle {
|
|
7
|
+
justify-self: end;
|
|
8
8
|
margin-bottom: 0.75rem;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
.page-footer {
|
|
12
|
+
margin-top: 3rem;
|
|
13
|
+
color: var(--muted);
|
|
14
|
+
font-size: 0.9rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
body {
|
|
12
18
|
margin: 0;
|
|
13
19
|
background: var(--bg);
|
|
@@ -21,12 +27,14 @@ body {
|
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
main {
|
|
24
|
-
max-width:
|
|
30
|
+
max-width: var(--mw-tool-content-max);
|
|
25
31
|
margin: 0 auto;
|
|
26
32
|
padding: 1.5rem 1.25rem 4rem;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
/* A single-column grid so the colour-mode control can sit at the inline end without a wrapper. */
|
|
36
|
+
.page-header {
|
|
37
|
+
display: grid;
|
|
30
38
|
padding-bottom: 1.5rem;
|
|
31
39
|
border-bottom: 1px solid var(--border);
|
|
32
40
|
margin-bottom: 1.5rem;
|