Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/commands/core/collection/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ora from 'ora'
import { Plugin, PluginData } from '../../../lib/types/pluginData.js'
import { txSignatureToString } from '../../../lib/util.js'
import pluginConfigurator, { mapPluginDataToArray } from '../../../prompts/pluginInquirer.js'
import { PluginFilterType, pluginSelector } from '../../../prompts/pluginSelector.js'
import { PluginFilterType, pluginSelector, validatePluginCompatibility, pluginDataToPluginIds } from '../../../prompts/pluginSelector.js'
import { TransactionCommand } from '../../../TransactionCommand.js'
import { ExplorerType, generateCoreExplorerUrl, generateExplorerUrl } from '../../../explorers.js'
import createAssetPrompt, { CreateAssetPromptResult } from '../../../prompts/createAssetPrompt.js'
Expand Down Expand Up @@ -86,22 +86,36 @@ export default class CoreCollectionCreate extends TransactionCommand<typeof Core
private async getPluginData(): Promise<PluginData | undefined> {
const { flags } = await this.parse(CoreCollectionCreate)

let pluginData: PluginData | undefined

if (flags.plugins) {
const selectedPlugins = await pluginSelector({ filter: PluginFilterType.Collection })
if (selectedPlugins) {
return await pluginConfigurator(selectedPlugins as Plugin[])
const incompatibleError = validatePluginCompatibility(selectedPlugins as Plugin[])
if (incompatibleError) {
this.error(incompatibleError)
}
pluginData = await pluginConfigurator(selectedPlugins as Plugin[])
}
} else if (flags.pluginsFile) {
try {
if (!fs.existsSync(flags.pluginsFile)) {
throw new Error(`Plugin file not found: ${flags.pluginsFile}`)
}
return JSON.parse(fs.readFileSync(flags.pluginsFile, 'utf-8')) as PluginData
pluginData = JSON.parse(fs.readFileSync(flags.pluginsFile, 'utf-8')) as PluginData
Comment thread
MarkSackerberg marked this conversation as resolved.
} catch (err) {
this.error(`Failed to read plugin data from file: ${err}`)
}
}
return undefined

if (pluginData) {
const incompatibleError = validatePluginCompatibility(pluginDataToPluginIds(pluginData))
if (incompatibleError) {
this.error(incompatibleError)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return pluginData
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
MarkSackerberg marked this conversation as resolved.

private async handleFileBasedCreation(umi: Umi, imagePath: string, jsonPath: string, explorer: ExplorerType) {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/types/pluginData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type Plugin =
| 'autograph'
| 'verifiedCreators'
| 'edition'
| 'bubblegumV2'

export interface PluginData {
attributes?: {
Expand Down Expand Up @@ -96,4 +97,7 @@ export interface PluginData {
type: 'Edition'
} & BasePlugin &
EditionPlugin
bubblegumV2?: {
type: 'BubblegumV2'
}
}
6 changes: 5 additions & 1 deletion src/prompts/createAssetPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { Plugin, PluginData } from '../lib/types/pluginData.js'
import pluginConfigurator from './pluginInquirer.js'
import { PluginFilterType, pluginSelector } from './pluginSelector.js'
import { PluginFilterType, pluginSelector, validatePluginCompatibility } from './pluginSelector.js'

export type NftType = 'image' | 'video' | 'audio' | 'model'

Expand Down Expand Up @@ -168,6 +168,10 @@ const createAssetPrompt = async (isCollection = false): Promise<CreateAssetPromp
if (wantsPlugins) {
const selectedPlugins = await pluginSelector({ filter: isCollection ? PluginFilterType.Collection : PluginFilterType.Asset })
if (selectedPlugins) {
const incompatibleError = validatePluginCompatibility(selectedPlugins as Plugin[])
if (incompatibleError) {
throw new Error(incompatibleError)
}
result.plugins = await pluginConfigurator(selectedPlugins as Plugin[])
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/prompts/pluginInquirer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,14 @@ const pluginConfigurator = async (plugins: Array<Plugin>): Promise<PluginData> =
}
break
}
case 'bubblegumV2': {
console.log(terminalColors.FgGreen + 'Bubblegum V2 Plugin Configuration')
console.log(terminalColors.FgCyan + 'No configuration needed for Bubblegum V2 plugin.')
pluginData.bubblegumV2 = {
type: 'BubblegumV2',
}
break
}
case 'edition': {
console.log(terminalColors.FgGreen + 'Edition Plugin Configuration')
let authority: string | undefined
Expand Down
177 changes: 133 additions & 44 deletions src/prompts/pluginSelector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { checkbox } from '@inquirer/prompts'
import { Plugin } from '../lib/types/pluginData.js'
import { Plugin, PluginData } from '../lib/types/pluginData.js'

export enum PluginFilterType {
Common,
Expand All @@ -18,43 +18,46 @@ interface PluginOption {

const pluginList: PluginOption[] = [
{
name: 'Attributes Plugin',
value: 'attributes',
name: 'Add Blocker Plugin',
value: 'addBlocker',
type: PluginFilterType.Common,
managed: PluginFilterType.Authority
},
{
name: 'Royalty Plugin',
value: 'royalties',
name: 'Attributes Plugin',
value: 'attributes',
type: PluginFilterType.Common,
managed: PluginFilterType.Authority
},
{
name: 'Update Delegate Plugin',
value: 'update',
name: 'Autograph Plugin',
value: 'autograph',
type: PluginFilterType.Common,
managed: PluginFilterType.Authority
managed: PluginFilterType.Owner
},
{
name: 'Permanent Freeze Plugin',
value: 'pFreeze',
type: PluginFilterType.Common,
name: 'Bubblegum V2 Plugin',
value: 'bubblegumV2',
type: PluginFilterType.Collection,
managed: PluginFilterType.Authority
},
{
name: 'Permanent Transfer Plugin',
value: 'pTransfer',
type: PluginFilterType.Common,
name: 'Burn Delegate Plugin',
value: 'burn',
type: PluginFilterType.Asset,
managed: PluginFilterType.Owner
},
{
name: 'Permanent Burn Plugin',
value: 'pBurn',
type: PluginFilterType.Common,
name: 'Edition Plugin',
value: 'edition',
type: PluginFilterType.Asset,
managed: PluginFilterType.Authority
},
{
name: 'Add Blocker Plugin',
value: 'addBlocker',
type: PluginFilterType.Common,
managed: PluginFilterType.Authority
name: 'Freeze Delegate Plugin',
value: 'freeze',
type: PluginFilterType.Asset,
managed: PluginFilterType.Owner
},
{
name: 'Immutable Metadata Plugin',
Expand All @@ -63,49 +66,92 @@ const pluginList: PluginOption[] = [
managed: PluginFilterType.Authority
},
{
name: 'Autograph Plugin',
value: 'autograph',
name: 'Master Edition Plugin',
value: 'masterEdition',
type: PluginFilterType.Collection,
managed: PluginFilterType.Authority
},
{
name: 'Permanent Burn Plugin',
value: 'pBurn',
type: PluginFilterType.Common,
managed: PluginFilterType.Owner
},
{
name: 'Verified Creators Plugin',
value: 'verifiedCreators',
name: 'Permanent Freeze Plugin',
value: 'pFreeze',
type: PluginFilterType.Common,
managed: PluginFilterType.Owner
},
{
name: 'Master Edition Plugin',
value: 'masterEdition',
type: PluginFilterType.Collection,
managed: PluginFilterType.Authority
name: 'Permanent Transfer Plugin',
value: 'pTransfer',
type: PluginFilterType.Common,
},
{
name: 'Edition Plugin',
value: 'edition',
type: PluginFilterType.Asset,
name: 'Royalty Plugin',
value: 'royalties',
type: PluginFilterType.Common,
managed: PluginFilterType.Authority
},
{
name: 'Freeze Delegate Plugin',
value: 'freeze',
name: 'Transfer Delegate Plugin',
value: 'transfer',
type: PluginFilterType.Asset,
managed: PluginFilterType.Owner
},
{
name: 'Burn Delegate Plugin',
value: 'burn',
type: PluginFilterType.Asset,
managed: PluginFilterType.Owner
name: 'Update Delegate Plugin',
value: 'update',
type: PluginFilterType.Common,
managed: PluginFilterType.Authority
},
{
name: 'Transfer Delegate Plugin',
value: 'transfer',
type: PluginFilterType.Asset,
name: 'Verified Creators Plugin',
value: 'verifiedCreators',
type: PluginFilterType.Common,
managed: PluginFilterType.Owner
},
]

// Defines which plugins are allowed to coexist.
// Key = plugin that imposes constraints, Value = set of plugins it allows.
// Plugins not listed here have no constraints.
const pluginAllowLists: Partial<Record<Plugin, Set<Plugin>>> = {
bubblegumV2: new Set([
'attributes',
'royalties',
'update',
'pFreeze',
'pTransfer',
'pBurn',
]),
}

// Given a set of selected plugins, returns the set of plugins still eligible.
const getCompatiblePlugins = (selected: Plugin[]): Set<Plugin> | null => {
let allowed: Set<Plugin> | null = null

for (const plugin of selected) {
const allowList = pluginAllowLists[plugin]
if (!allowList) continue

// The plugin that owns the allow list is always compatible with itself
const withSelf = new Set<Plugin>([...allowList, plugin])

if (allowed === null) {
allowed = withSelf
} else {
// Intersect: only keep plugins allowed by all constraining plugins
const intersection = new Set<Plugin>()
for (const p of allowed) {
if (withSelf.has(p)) intersection.add(p)
}
allowed = intersection
}
}

return allowed
}

interface PluginSelectorOptions {
filter: PluginFilterType.Asset | PluginFilterType.Collection,
managedBy?: PluginFilterType.Authority | PluginFilterType.Owner,
Expand All @@ -129,8 +175,51 @@ export const pluginSelector = async (options: PluginSelectorOptions): Promise<Pl
name: plugin.name,
value: plugin.value
}))

})

return selectedPlugin
}

// Maps plugin type discriminators (e.g. 'BubblegumV2', 'PermanentTransferDelegate')
// back to canonical Plugin IDs (e.g. 'bubblegumV2', 'pTransfer').
// This avoids relying on PluginData object keys which may have typos.
const typeToPluginId: Record<string, Plugin> = {
'Attributes': 'attributes',
'Royalties': 'royalties',
'BurnDelegate': 'burn',
'TransferDelegate': 'transfer',
'FreezeDelegate': 'freeze',
'PermanentBurnDelegate': 'pBurn',
'PermanentTransferDelegate': 'pTransfer',
'PermanentFreezeDelegate': 'pFreeze',
'UpdateDelegate': 'update',
'MasterEdition': 'masterEdition',
'AddBlocker': 'addBlocker',
'ImmutableMetadata': 'immutableMetadata',
'Autograph': 'autograph',
'VerifiedCreators': 'verifiedCreators',
'Edition': 'edition',
'BubblegumV2': 'bubblegumV2',
}

// Extracts canonical Plugin IDs from a PluginData object by reading each
// entry's type discriminator, avoiding reliance on potentially misspelled keys.
export const pluginDataToPluginIds = (data: PluginData): Plugin[] => {
return Object.values(data)
.map((entry: any) => typeToPluginId[entry?.type])
.filter((id): id is Plugin => id !== undefined)
}

// Validates that a set of selected plugins are compatible with each other.
// Returns an error message if incompatible, or null if valid.
export const validatePluginCompatibility = (selected: Plugin[]): string | null => {
const compatible = getCompatiblePlugins(selected)
if (compatible === null) return null

const incompatible = selected.filter(p => !compatible.has(p))
if (incompatible.length === 0) return null

const constraining = selected.filter(p => pluginAllowLists[p])
return `Plugin(s) ${incompatible.join(', ')} cannot be used together with ${constraining.join(', ')}.`
}
Loading