@meddleware/token-deployer-ui 0.0.9 → 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/CLAUDE.md +13 -6
- package/Dockerfile +1 -1
- package/LICENSE +11 -118
- package/README.md +3 -5
- package/index.html +11 -2
- package/package.json +6 -5
- 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/components/LicensePicker.vue +2 -1
- package/src/composables/useAccessGate.ts +1 -1
- package/src/config.ts +10 -13
- package/src/lib/accessGate.ts +28 -14
- package/src/lib/deploy.ts +36 -0
- package/src/lib/deployExecutor.ts +14 -12
- 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/types.ts +1 -1
- package/src/lib/walrus.ts +22 -11
- package/src/main.ts +39 -56
- package/src/template-src/files.json +7 -7
- 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/vite.config.ts +9 -1
package/src/config.ts
CHANGED
|
@@ -7,16 +7,16 @@ import type { AccessGateConfig } from './lib/accessGate.js'
|
|
|
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
|
|
|
@@ -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
|
/**
|
package/src/lib/accessGate.ts
CHANGED
|
@@ -30,27 +30,41 @@ export interface OwnedAccessNft {
|
|
|
30
30
|
usesRemaining: number | null
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Minimal structural subset of the
|
|
33
|
+
/** Minimal structural subset of the core (gRPC) client used here. */
|
|
34
34
|
export interface OwnedObjectsClient {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
core: {
|
|
36
|
+
listOwnedObjects(params: {
|
|
37
|
+
owner: string
|
|
38
|
+
type?: string
|
|
39
|
+
include?: { json?: boolean }
|
|
40
|
+
}): Promise<{ objects: unknown[] }>
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Unwrap a Move-struct field bag from a core `json` value. The gRPC/core API returns struct
|
|
46
|
+
* fields flat; the old JSON-RPC shape nested them under `.fields`. Tolerate both.
|
|
47
|
+
*/
|
|
48
|
+
function structFields(v: unknown): Record<string, any> | undefined {
|
|
49
|
+
if (!v || typeof v !== 'object') return undefined
|
|
50
|
+
const o = v as Record<string, any>
|
|
51
|
+
const nested = o.fields
|
|
52
|
+
return nested && typeof nested === 'object' ? (nested as Record<string, any>) : o
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
function parseUsesRemaining(variant: any): number | null {
|
|
43
56
|
if (variant == null) return null
|
|
57
|
+
const tag: string | undefined = variant.variant ?? variant.type ?? variant.$kind
|
|
58
|
+
if (tag === 'UnlimitedPass') return null
|
|
44
59
|
const fields = variant.fields ?? variant
|
|
45
60
|
const ur = fields?.uses_remaining ?? fields?.SingleUse?.uses_remaining
|
|
46
61
|
return ur != null ? Number(ur) : null
|
|
47
62
|
}
|
|
48
63
|
|
|
49
|
-
/** Parse a `
|
|
64
|
+
/** Parse a `listOwnedObjects` entry into an access NFT, or `null` if it isn't one. */
|
|
50
65
|
export function parseOwnedAccessNft(entry: any): OwnedAccessNft | null {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const inner = obj?.content?.fields?.data?.fields
|
|
66
|
+
const objectId: string | undefined = entry?.objectId
|
|
67
|
+
const inner = structFields(structFields(entry?.json)?.data)
|
|
54
68
|
const gateId: string | undefined = inner?.gate_id ?? inner?.gateId
|
|
55
69
|
if (!objectId || !gateId) return null
|
|
56
70
|
return { objectId, gateId, usesRemaining: parseUsesRemaining(inner?.variant) }
|
|
@@ -63,12 +77,12 @@ export async function fetchAccessNfts(
|
|
|
63
77
|
nftType: string,
|
|
64
78
|
gateId?: string,
|
|
65
79
|
): Promise<OwnedAccessNft[]> {
|
|
66
|
-
const {
|
|
80
|
+
const { objects } = await client.core.listOwnedObjects({
|
|
67
81
|
owner,
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
type: nftType,
|
|
83
|
+
include: { json: true },
|
|
70
84
|
})
|
|
71
|
-
const parsed = (
|
|
85
|
+
const parsed = (objects ?? [])
|
|
72
86
|
.map(parseOwnedAccessNft)
|
|
73
87
|
.filter((n): n is OwnedAccessNft => n !== null)
|
|
74
88
|
return gateId ? parsed.filter((n) => n.gateId === gateId) : parsed
|
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 })
|
|
@@ -122,12 +122,12 @@ function renderMoveToml(cfg: TokenConfig): string {
|
|
|
122
122
|
function renderSource(cfg: TokenConfig): string {
|
|
123
123
|
let out = files['source.move']
|
|
124
124
|
|
|
125
|
-
// license header (lines 1-2)
|
|
125
|
+
// license header (lines 1-2) — always rewritten, whatever licence the template itself carries
|
|
126
126
|
const lines = out.split('\n')
|
|
127
127
|
if (isProprietary(cfg.license)) {
|
|
128
128
|
lines[0] = '// SPDX-License-Identifier: UNLICENSED'
|
|
129
129
|
lines[1] = '// All rights reserved. Proprietary and not licensed for redistribution.'
|
|
130
|
-
} else
|
|
130
|
+
} else {
|
|
131
131
|
lines[0] = `// SPDX-License-Identifier: ${spdxId(cfg.license)}`
|
|
132
132
|
lines[1] = `// Licensed under the ${spdxId(cfg.license)} license; see the LICENSE file.`
|
|
133
133
|
}
|
|
@@ -157,18 +157,18 @@ function renderPublishScript(cfg: TokenConfig): string {
|
|
|
157
157
|
return out
|
|
158
158
|
}
|
|
159
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
|
+
|
|
160
163
|
function renderReadme(cfg: TokenConfig): string {
|
|
161
|
-
|
|
164
|
+
const out = applyDocPlaceholders(files['README.md'], cfg)
|
|
162
165
|
const licenseName = cfg.licenseName || spdxId(cfg.license)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
'All rights reserved. This package is proprietary and not licensed for redistribution.'
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
out = out.replace(/^CC0 1\.0 Universal.*$/m, `${licenseName} — see the LICENSE file.`)
|
|
170
|
-
}
|
|
171
|
-
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
|
+
)
|
|
172
172
|
}
|
|
173
173
|
|
|
174
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
|
|
package/src/lib/listMyTokens.ts
CHANGED
|
@@ -19,25 +19,21 @@ export interface DeployedToken {
|
|
|
19
19
|
/** List the coins `owner` has deployed on `network` (via owned `TreasuryCap<T>` objects). */
|
|
20
20
|
export async function listMyTokens(owner: string, network: Network): Promise<DeployedToken[]> {
|
|
21
21
|
const client = getReadClient(network)
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
owner,
|
|
25
|
-
filter: { StructType: '0x2::coin::TreasuryCap' },
|
|
26
|
-
options: { showType: true },
|
|
27
|
-
})
|
|
22
|
+
// A type filter without type params matches every TreasuryCap<T> instantiation.
|
|
23
|
+
const { objects } = await client.listOwnedObjects({ owner, type: '0x2::coin::TreasuryCap' })
|
|
28
24
|
const seen = new Set<string>()
|
|
29
25
|
const out: DeployedToken[] = []
|
|
30
|
-
for (const
|
|
31
|
-
|
|
32
|
-
const m =
|
|
33
|
-
if (!m
|
|
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
|
|
34
30
|
const coinType = m[1]
|
|
35
31
|
if (seen.has(coinType)) continue
|
|
36
32
|
seen.add(coinType)
|
|
37
33
|
out.push({
|
|
38
34
|
coinType,
|
|
39
35
|
packageId: coinType.split('::')[0] ?? '',
|
|
40
|
-
treasuryCapId:
|
|
36
|
+
treasuryCapId: obj.objectId,
|
|
41
37
|
label: coinType.split('::').pop() ?? coinType,
|
|
42
38
|
})
|
|
43
39
|
}
|
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/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
|
@@ -8,7 +8,8 @@ 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
10
|
// import it), so a dynamic import in the E2E block below could not split it out anyway.
|
|
11
|
-
import { getReadClient } from './lib/readClient.js'
|
|
11
|
+
import { getReadClient, setReadClient } from './lib/readClient.js'
|
|
12
|
+
import type { SuiGrpcClient } from '@mysten/sui/grpc'
|
|
12
13
|
import { useColorMode, useSeason } from '@meddleware/ui'
|
|
13
14
|
|
|
14
15
|
// Apply the colour mode before mount so there is no theme flash. Defaults to
|
|
@@ -25,72 +26,54 @@ import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_b
|
|
|
25
26
|
// doesn't block initial page load.
|
|
26
27
|
configureWasm(wasmUrl)
|
|
27
28
|
|
|
28
|
-
// 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.
|
|
29
30
|
// Wrapped in async IIFE to ensure wallet is registered BEFORE app mounts (timing critical).
|
|
30
31
|
// Tree-shaking removes this entire block from production when VITE_E2E !== '1'.
|
|
31
32
|
;(async () => {
|
|
32
33
|
if (import.meta.env.VITE_E2E === '1') {
|
|
33
34
|
const { getWallets } = await import('@mysten/wallet-standard')
|
|
34
35
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
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)
|
|
38
40
|
const packageId = '0x' + 'bb'.repeat(32)
|
|
39
|
-
const
|
|
41
|
+
const treasuryCapId = '0x' + 'cc'.repeat(32)
|
|
40
42
|
const metadataCapId = '0x' + 'dd'.repeat(32)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
],
|
|
54
61
|
},
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
objectId: metadataCapId,
|
|
62
|
+
objectTypes: {
|
|
63
|
+
[treasuryCapId]: `0x2::coin::TreasuryCap<${packageId}::mytoken::MYTOKEN>`,
|
|
64
|
+
[metadataCapId]: `0x2::coin_registry::MetadataCap<${packageId}::mytoken::MYTOKEN>`,
|
|
59
65
|
},
|
|
60
|
-
|
|
61
|
-
events: [],
|
|
62
|
-
transaction: null,
|
|
63
|
-
balanceChanges: null,
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
67
|
-
const url = typeof input === 'string' ? input : input.toString()
|
|
68
|
-
const method = init?.method || 'GET'
|
|
69
|
-
const isRpcCall = (url.includes('fullnode') || url.includes('rpc')) && method === 'POST'
|
|
70
|
-
|
|
71
|
-
if (isRpcCall) {
|
|
72
|
-
const body = init?.body ? JSON.parse(String(init.body)) : {}
|
|
73
|
-
const rpcMethod = body.method
|
|
74
|
-
|
|
75
|
-
if (import.meta.env.VITE_E2E === '1') {
|
|
76
|
-
console.log('[E2E Fetch Mock]', rpcMethod, 'URL:', url)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const result =
|
|
80
|
-
rpcMethod === 'sui_executeTransactionBlock' || rpcMethod === 'sui_getTransactionBlock'
|
|
81
|
-
? mockRpcResponse
|
|
82
|
-
: rpcMethod === 'suix_getCoins' || rpcMethod === 'suix_getBalance'
|
|
83
|
-
? { data: [], nextCursor: null, hasNextPage: false }
|
|
84
|
-
: null
|
|
85
|
-
|
|
86
|
-
return new Response(JSON.stringify({ jsonrpc: '2.0', id: body.id || 1, result }), {
|
|
87
|
-
status: 200,
|
|
88
|
-
headers: { 'content-type': 'application/json' },
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return originalFetch(input, init)
|
|
66
|
+
},
|
|
93
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)
|
|
94
77
|
|
|
95
78
|
const E2E_ADDR = '0x' + 'a'.repeat(64)
|
|
96
79
|
const E2E_PUB_KEY = new Uint8Array(32)
|