@meddleware/token-deployer-ui 0.0.16 → 0.0.17
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/README.md +26 -39
- package/package.json +2 -9
- package/src/components/ConfigForm.vue +1 -1
- package/src/components/DeployProgress.vue +1 -1
- package/src/components/GithubPush.vue +2 -2
- package/src/components/IconPicker.vue +2 -2
- package/src/components/MyTokens.vue +8 -10
- package/src/components/ResultPanel.vue +2 -2
- package/src/components/ReviewPanel.vue +1 -1
- package/src/components/TokenDeployerView.vue +3 -3
- package/src/components/WalletBar.vue +1 -1
- package/src/components/WalrusBlobBrowser.vue +4 -4
- package/src/config.ts +1 -1
- package/src/index.ts +2 -0
- package/src/lib/deployExecutor.ts +3 -4
- package/src/lib/form.ts +1 -1
- package/src/lib/validation.ts +36 -129
- package/src/main.ts +3 -10
- package/src/templateWasm.ts +8 -0
- package/src/wallet.ts +19 -0
- package/src/lib/buildPublishTx.ts +0 -182
- package/src/lib/deploy.ts +0 -210
- package/src/lib/generatePackage.ts +0 -317
- package/src/lib/listMyTokens.ts +0 -41
- package/src/lib/readClient.ts +0 -25
- package/src/lib/template.ts +0 -134
- package/src/lib/types.ts +0 -69
- package/src/move-template/sui_token_template.mv +0 -0
- package/src/move-template/template.ts +0 -31
- package/src/template-src/files.json +0 -10
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
// PTB builders for deploying a coin.
|
|
2
|
-
//
|
|
3
|
-
// Two transactions, because the OTW `init()` creates the TreasuryCap/MetadataCap
|
|
4
|
-
// and sends them to the sender — they are NOT results of `tx.publish`, so they
|
|
5
|
-
// can only be used after the publish executes:
|
|
6
|
-
//
|
|
7
|
-
// 1. buildPublishTransaction — publish the patched module, take the trivial
|
|
8
|
-
// fee, and apply the UpgradeCap policy (make immutable, or keep). The
|
|
9
|
-
// TreasuryCap + MetadataCap land with the sender automatically.
|
|
10
|
-
// 2. buildFinalizeTransaction — (optional) mint the initial supply, apply the
|
|
11
|
-
// supply + metadata policies, promote the currency in the registry, and move
|
|
12
|
-
// the caps/supply to the recipient. Only needed when any of those differ
|
|
13
|
-
// from the defaults.
|
|
14
|
-
|
|
15
|
-
import { Transaction } from '@mysten/sui/transactions'
|
|
16
|
-
import { toBase64 } from '@mysten/sui/utils'
|
|
17
|
-
import type { Network, PublishResult, TokenConfig } from './types.js'
|
|
18
|
-
|
|
19
|
-
const SUI_FRAMEWORK = '0x2'
|
|
20
|
-
const MOVE_STDLIB = '0x1'
|
|
21
|
-
|
|
22
|
-
export interface BuildPublishArgs {
|
|
23
|
-
moduleBytes: Uint8Array
|
|
24
|
-
sender: string
|
|
25
|
-
feeMist: bigint
|
|
26
|
-
feeRecipient: string
|
|
27
|
-
gasBudget: bigint
|
|
28
|
-
packagePolicy: TokenConfig['packagePolicy']
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Publish PTB: publish -> (make_immutable | transfer UpgradeCap) + fee split.
|
|
33
|
-
* The user signs this; they pay gas and the fee.
|
|
34
|
-
*/
|
|
35
|
-
export function buildPublishTransaction(args: BuildPublishArgs): Transaction {
|
|
36
|
-
const tx = new Transaction()
|
|
37
|
-
tx.setSender(args.sender)
|
|
38
|
-
|
|
39
|
-
const [upgradeCap] = tx.publish({
|
|
40
|
-
modules: [toBase64(args.moduleBytes)],
|
|
41
|
-
dependencies: [MOVE_STDLIB, SUI_FRAMEWORK],
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
if (args.packagePolicy === 'immutable') {
|
|
45
|
-
tx.moveCall({ target: `${SUI_FRAMEWORK}::package::make_immutable`, arguments: [upgradeCap] })
|
|
46
|
-
} else {
|
|
47
|
-
tx.transferObjects([upgradeCap], args.sender)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Trivial fee: split from the gas coin into the operator treasury.
|
|
51
|
-
if (args.feeMist > 0n) {
|
|
52
|
-
const [fee] = tx.splitCoins(tx.gas, [args.feeMist])
|
|
53
|
-
tx.transferObjects([fee], args.feeRecipient)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
tx.setGasBudget(args.gasBudget)
|
|
57
|
-
return tx
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface BuildFinalizeArgs {
|
|
61
|
-
config: TokenConfig
|
|
62
|
-
coinType: string
|
|
63
|
-
treasuryCapId: string
|
|
64
|
-
metadataCapId: string
|
|
65
|
-
/**
|
|
66
|
-
* Full object reference for the pending Currency<T> object (at address 0xc).
|
|
67
|
-
* When provided, `finalize_registration` is called first in the PTB, promoting the
|
|
68
|
-
* pending Currency to a shared, RPC-discoverable object so wallets can read the
|
|
69
|
-
* coin's decimal metadata via suix_getCoinMetadata.
|
|
70
|
-
*/
|
|
71
|
-
currencyRef?: { objectId: string; version: string; digest: string }
|
|
72
|
-
sender: string
|
|
73
|
-
gasBudget: bigint
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Finalize PTB: register the coin in the registry, mint initial supply, apply
|
|
78
|
-
* supply/metadata policy, and route caps to the recipient.
|
|
79
|
-
* Built after the publish effects reveal the cap object IDs.
|
|
80
|
-
*/
|
|
81
|
-
export function buildFinalizeTransaction(args: BuildFinalizeArgs): Transaction {
|
|
82
|
-
const { config, coinType } = args
|
|
83
|
-
const tx = new Transaction()
|
|
84
|
-
tx.setSender(args.sender)
|
|
85
|
-
const recipient = config.recipient || args.sender
|
|
86
|
-
|
|
87
|
-
// Always call finalize_registration when we have the full object ref for the pending
|
|
88
|
-
// Currency<T>. This promotes it from a pending owned object (at @0xc) to a shared,
|
|
89
|
-
// RPC-discoverable object so wallets can read decimals via suix_getCoinMetadata.
|
|
90
|
-
// Without this step wallets fall back to 0 decimals and display raw base-unit counts.
|
|
91
|
-
if (args.currencyRef) {
|
|
92
|
-
tx.moveCall({
|
|
93
|
-
target: `${SUI_FRAMEWORK}::coin_registry::finalize_registration`,
|
|
94
|
-
typeArguments: [coinType],
|
|
95
|
-
arguments: [
|
|
96
|
-
tx.object('0xc'), // &mut CoinRegistry (shared object at well-known address)
|
|
97
|
-
tx.receivingRef(args.currencyRef), // Receiving<Currency<T>> — pending object at @0xc
|
|
98
|
-
],
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Mint the initial supply (scaled by decimals) to the recipient.
|
|
103
|
-
if (config.initialSupply > 0n) {
|
|
104
|
-
const amount = config.initialSupply * 10n ** BigInt(config.decimals)
|
|
105
|
-
const minted = tx.moveCall({
|
|
106
|
-
target: `${SUI_FRAMEWORK}::coin::mint`,
|
|
107
|
-
typeArguments: [coinType],
|
|
108
|
-
arguments: [tx.object(args.treasuryCapId), tx.pure.u64(amount)],
|
|
109
|
-
})
|
|
110
|
-
tx.transferObjects([minted], recipient)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Supply policy: fixed => freeze the TreasuryCap (no further minting possible);
|
|
114
|
-
// mintable => keep it with the recipient.
|
|
115
|
-
if (config.supplyPolicy === 'fixed') {
|
|
116
|
-
tx.moveCall({
|
|
117
|
-
target: `${SUI_FRAMEWORK}::transfer::public_freeze_object`,
|
|
118
|
-
typeArguments: [`${SUI_FRAMEWORK}::coin::TreasuryCap<${coinType}>`],
|
|
119
|
-
arguments: [tx.object(args.treasuryCapId)],
|
|
120
|
-
})
|
|
121
|
-
} else if (recipient.toLowerCase() !== args.sender.toLowerCase()) {
|
|
122
|
-
tx.transferObjects([tx.object(args.treasuryCapId)], recipient)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Metadata policy: frozen => freeze the MetadataCap (no further metadata edits);
|
|
126
|
-
// updatable => keep it with the recipient.
|
|
127
|
-
if (config.metadataPolicy === 'frozen') {
|
|
128
|
-
tx.moveCall({
|
|
129
|
-
target: `${SUI_FRAMEWORK}::transfer::public_freeze_object`,
|
|
130
|
-
typeArguments: [`${SUI_FRAMEWORK}::coin_registry::MetadataCap<${coinType}>`],
|
|
131
|
-
arguments: [tx.object(args.metadataCapId)],
|
|
132
|
-
})
|
|
133
|
-
} else if (recipient.toLowerCase() !== args.sender.toLowerCase()) {
|
|
134
|
-
tx.transferObjects([tx.object(args.metadataCapId)], recipient)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
tx.setGasBudget(args.gasBudget)
|
|
138
|
-
return tx
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
interface ObjectChange {
|
|
142
|
-
type: string
|
|
143
|
-
objectType?: string
|
|
144
|
-
objectId?: string
|
|
145
|
-
packageId?: string
|
|
146
|
-
/** Version and digest are present for created/mutated objects in the full RPC response. */
|
|
147
|
-
version?: string
|
|
148
|
-
digest?: string
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Pull package id, coin type and cap ids out of publish `objectChanges`. */
|
|
152
|
-
export function extractPublishResult(
|
|
153
|
-
objectChanges: ObjectChange[],
|
|
154
|
-
ctx: { network: Network; digest: string; feeRecipient: string; feeMist: bigint },
|
|
155
|
-
): PublishResult {
|
|
156
|
-
const published = objectChanges.find((c) => c.type === 'published')
|
|
157
|
-
const packageId = published?.packageId ?? ''
|
|
158
|
-
|
|
159
|
-
const createdOfType = (needle: string) =>
|
|
160
|
-
objectChanges.find((c) => c.type === 'created' && (c.objectType ?? '').includes(needle))
|
|
161
|
-
|
|
162
|
-
const treasury = createdOfType('coin::TreasuryCap')
|
|
163
|
-
const coinTypeMatch = treasury?.objectType?.match(/TreasuryCap<(.+)>/)
|
|
164
|
-
const coinType = coinTypeMatch?.[1] ?? ''
|
|
165
|
-
|
|
166
|
-
const currency = createdOfType('coin_registry::Currency')
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
network: ctx.network,
|
|
170
|
-
packageId,
|
|
171
|
-
coinType,
|
|
172
|
-
treasuryCapId: treasury?.objectId,
|
|
173
|
-
metadataCapId: createdOfType('coin_registry::MetadataCap')?.objectId,
|
|
174
|
-
currencyId: currency?.objectId,
|
|
175
|
-
currencyVersion: currency?.version,
|
|
176
|
-
currencyDigest: currency?.digest,
|
|
177
|
-
upgradeCapId: createdOfType('package::UpgradeCap')?.objectId,
|
|
178
|
-
digest: ctx.digest,
|
|
179
|
-
feeRecipient: ctx.feeRecipient,
|
|
180
|
-
feeMist: ctx.feeMist.toString(),
|
|
181
|
-
}
|
|
182
|
-
}
|
package/src/lib/deploy.ts
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
// Deployment orchestration: patch -> publish -> wait -> (finalize) -> result.
|
|
2
|
-
// Decoupled from the wallet via an `Executor` so it is unit-testable.
|
|
3
|
-
|
|
4
|
-
import type { Transaction } from '@mysten/sui/transactions'
|
|
5
|
-
import type { SuiClientTypes } from '@mysten/sui/client'
|
|
6
|
-
import { patchTemplateModule } from './template.js'
|
|
7
|
-
import {
|
|
8
|
-
buildPublishTransaction,
|
|
9
|
-
buildFinalizeTransaction,
|
|
10
|
-
extractPublishResult,
|
|
11
|
-
} from './buildPublishTx.js'
|
|
12
|
-
import { deriveStructName } from './validation.js'
|
|
13
|
-
import type { Network, PublishResult, TokenConfig } from './types.js'
|
|
14
|
-
|
|
15
|
-
export interface SuiTxResult {
|
|
16
|
-
digest: string
|
|
17
|
-
objectChanges?: {
|
|
18
|
-
type: string
|
|
19
|
-
objectType?: string
|
|
20
|
-
objectId?: string
|
|
21
|
-
packageId?: string
|
|
22
|
-
/** Present for created/mutated objects in the full Sui RPC response. */
|
|
23
|
-
version?: string
|
|
24
|
-
digest?: string
|
|
25
|
-
}[]
|
|
26
|
-
effects?: { status?: { status?: string; error?: string } }
|
|
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
|
-
|
|
64
|
-
export interface Executor {
|
|
65
|
-
/** Sign with the wallet and execute; must request object changes + effects. */
|
|
66
|
-
signAndExecute(tx: Transaction): Promise<SuiTxResult>
|
|
67
|
-
/** Block until the digest is indexed (so new objects resolve). */
|
|
68
|
-
waitForTransaction(digest: string): Promise<void>
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export type DeployStep =
|
|
72
|
-
| 'patching'
|
|
73
|
-
| 'publishing'
|
|
74
|
-
| 'confirming'
|
|
75
|
-
| 'finalizing'
|
|
76
|
-
| 'confirming-finalize'
|
|
77
|
-
| 'done'
|
|
78
|
-
|
|
79
|
-
export interface DeployArgs {
|
|
80
|
-
config: TokenConfig
|
|
81
|
-
network: Network
|
|
82
|
-
sender: string
|
|
83
|
-
feeMist: bigint
|
|
84
|
-
feeTreasury: string
|
|
85
|
-
gasBudget: bigint
|
|
86
|
-
executor: Executor
|
|
87
|
-
onStep?: (step: DeployStep) => void
|
|
88
|
-
}
|
|
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
|
-
*/
|
|
94
|
-
function assertSuccess(res: SuiTxResult, what: string): void {
|
|
95
|
-
const status = res.effects?.status?.status
|
|
96
|
-
if (status !== 'success') {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`${what} failed: ${res.effects?.status?.error ?? status ?? 'no effects status returned'}`,
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Deploy a token end-to-end. Returns the publish result once any required
|
|
105
|
-
* finalize transaction has also confirmed.
|
|
106
|
-
*/
|
|
107
|
-
export async function deployToken(args: DeployArgs): Promise<PublishResult> {
|
|
108
|
-
const { config, executor, onStep } = args
|
|
109
|
-
const structName = deriveStructName(config.moduleName)
|
|
110
|
-
const normalizedConfig: TokenConfig = { ...config, structName }
|
|
111
|
-
const recipient = normalizedConfig.recipient || args.sender
|
|
112
|
-
const isE2e = import.meta.env.MODE === 'e2e' && import.meta.env.VITE_E2E === '1'
|
|
113
|
-
|
|
114
|
-
onStep?.('patching')
|
|
115
|
-
const moduleBytes = await patchTemplateModule({
|
|
116
|
-
moduleName: normalizedConfig.moduleName,
|
|
117
|
-
structName,
|
|
118
|
-
symbol: normalizedConfig.symbol,
|
|
119
|
-
name: normalizedConfig.name,
|
|
120
|
-
description: normalizedConfig.description,
|
|
121
|
-
iconUrl: normalizedConfig.iconUrl,
|
|
122
|
-
decimals: normalizedConfig.decimals,
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
onStep?.('publishing')
|
|
126
|
-
const publishTx = buildPublishTransaction({
|
|
127
|
-
moduleBytes,
|
|
128
|
-
sender: args.sender,
|
|
129
|
-
feeMist: args.feeMist,
|
|
130
|
-
feeRecipient: args.feeTreasury,
|
|
131
|
-
gasBudget: args.gasBudget,
|
|
132
|
-
packagePolicy: normalizedConfig.packagePolicy,
|
|
133
|
-
})
|
|
134
|
-
const pub = await (async () => {
|
|
135
|
-
try {
|
|
136
|
-
const result = await executor.signAndExecute(publishTx)
|
|
137
|
-
if (isE2e) console.log('[E2E Deploy]', 'Publish TX result:', result)
|
|
138
|
-
assertSuccess(result, 'Publish')
|
|
139
|
-
|
|
140
|
-
onStep?.('confirming')
|
|
141
|
-
try {
|
|
142
|
-
if (isE2e) console.log('[E2E Deploy]', 'Waiting for transaction digest:', result.digest)
|
|
143
|
-
await executor.waitForTransaction(result.digest)
|
|
144
|
-
if (isE2e) console.log('[E2E Deploy]', 'Transaction confirmed')
|
|
145
|
-
} catch (e) {
|
|
146
|
-
if (isE2e) console.error('[E2E Deploy]', 'waitForTransaction error:', e)
|
|
147
|
-
throw e
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return result
|
|
151
|
-
} catch (e) {
|
|
152
|
-
if (isE2e) console.error('[E2E Deploy]', 'Publish TX error:', e)
|
|
153
|
-
throw e
|
|
154
|
-
}
|
|
155
|
-
})()
|
|
156
|
-
|
|
157
|
-
const result = extractPublishResult(pub.objectChanges ?? [], {
|
|
158
|
-
network: args.network,
|
|
159
|
-
digest: pub.digest,
|
|
160
|
-
feeRecipient: args.feeTreasury,
|
|
161
|
-
feeMist: args.feeMist,
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
// A successful publish must surface the package id + coin type in its object
|
|
165
|
-
// changes; if not, fail loudly rather than returning an empty result panel.
|
|
166
|
-
if (!result.packageId || !result.coinType) {
|
|
167
|
-
throw new Error(
|
|
168
|
-
'Publish succeeded but the package id / coin type could not be read from the transaction effects.',
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Build the currency receiving ref for finalize_registration (requires version + digest
|
|
173
|
-
// from the publish objectChanges, present in production Sui RPC responses).
|
|
174
|
-
const currencyChange = (pub.objectChanges ?? []).find(
|
|
175
|
-
(c) => c.type === 'created' && (c.objectType ?? '').includes('coin_registry::Currency'),
|
|
176
|
-
)
|
|
177
|
-
const currencyRef =
|
|
178
|
-
currencyChange?.objectId && currencyChange?.version && currencyChange?.digest
|
|
179
|
-
? {
|
|
180
|
-
objectId: currencyChange.objectId,
|
|
181
|
-
version: currencyChange.version,
|
|
182
|
-
digest: currencyChange.digest,
|
|
183
|
-
}
|
|
184
|
-
: undefined
|
|
185
|
-
|
|
186
|
-
// Always finalize: the finalize PTB calls coin_registry::finalize_registration,
|
|
187
|
-
// which is required to promote the pending Currency<T> to a shared, wallet-discoverable
|
|
188
|
-
// object. Without it, suix_getCoinMetadata returns null and wallets fall back to
|
|
189
|
-
// 0 decimals, displaying raw base-unit counts instead of whole-token amounts.
|
|
190
|
-
if (!result.treasuryCapId || !result.metadataCapId) {
|
|
191
|
-
throw new Error('Published, but could not locate the TreasuryCap/MetadataCap to finalize.')
|
|
192
|
-
}
|
|
193
|
-
onStep?.('finalizing')
|
|
194
|
-
const finalizeTx = buildFinalizeTransaction({
|
|
195
|
-
config: { ...normalizedConfig, recipient },
|
|
196
|
-
coinType: result.coinType,
|
|
197
|
-
treasuryCapId: result.treasuryCapId,
|
|
198
|
-
metadataCapId: result.metadataCapId,
|
|
199
|
-
currencyRef,
|
|
200
|
-
sender: args.sender,
|
|
201
|
-
gasBudget: args.gasBudget,
|
|
202
|
-
})
|
|
203
|
-
const fin = await executor.signAndExecute(finalizeTx)
|
|
204
|
-
assertSuccess(fin, 'Finalize')
|
|
205
|
-
onStep?.('confirming-finalize')
|
|
206
|
-
await executor.waitForTransaction(fin.digest)
|
|
207
|
-
|
|
208
|
-
onStep?.('done')
|
|
209
|
-
return result
|
|
210
|
-
}
|