Conversation
…mands Adds --wizard flag to both `genesis create` and `genesis launch create` with: - Interactive prompts for all configuration (token metadata, buckets, scheduling) - Two wizard paths: API-based (recommended) and manual on-chain - Input validation: future-only timestamps for launch wizards, URL validation for social links (twitter.com/x.com, t.me), minimum raise goals (250 SOL / 25k USDC) - Shared prompt module in src/lib/genesis/createGenesisWizardPrompt.ts - Extracted buildLaunchInput helper to deduplicate API input construction
Summary by CodeRabbitRelease Notes
WalkthroughIntroduces interactive wizard-driven flows for Genesis account and token launch creation, replacing manual flag-based configuration. Adds new high-level operation APIs for account and bucket management, comprehensive prompt utilities for guided user input, and extensive end-to-end test coverage for interactive wizard scenarios. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as CLI (create command)
participant Wizard as Wizard Prompts
participant Ops as Operations API
participant Chain as Blockchain/Umi
User->>CLI: Run with --wizard flag
CLI->>Wizard: promptLaunchWizard()
Wizard->>User: Ask launch type, token details, timing
User->>Wizard: Provide launch parameters
Wizard-->>CLI: Return LaunchWizardResult
CLI->>Ops: buildLaunchInput(wallet, chain, params)
Ops-->>CLI: Return CreateLaunchInput
CLI->>Ops: createAndRegisterLaunch(umi, launchInput)
Ops->>Chain: Initialize & register launch
Chain-->>Ops: Tx signature
Ops-->>CLI: Success response
CLI->>User: Display launch details & explorer URL
sequenceDiagram
participant User
participant CLI as CLI (create command)
participant Wizard as Wizard Prompts
participant Ops as Operations API
participant Chain as Blockchain/Umi
User->>CLI: Run with --wizard flag
CLI->>Wizard: promptGenesisCreate()
Wizard->>User: Ask name, symbol, supply, funding mode
User->>Wizard: Provide genesis parameters
Wizard-->>CLI: Return GenesisCreateResult
CLI->>Ops: createGenesisAccount(umi, signer, params)
Ops->>Chain: Initialize genesis account
Chain-->>Ops: Tx signature, account PDA, mint details
Ops-->>CLI: CreateGenesisAccountResult
loop Add Buckets
CLI->>Wizard: promptBucketChoice()
User->>Wizard: Select bucket type
Wizard->>User: Collect bucket parameters
User->>Wizard: Provide details
Wizard-->>CLI: Return bucket config
CLI->>Ops: addLaunchPoolBucket/Presale/Unlocked(...)
Ops->>Chain: Create bucket
Chain-->>Ops: Bucket PDA, signature
Ops-->>CLI: AddBucketResult
CLI->>User: Display bucket transaction
end
CLI->>Ops: finalizeGenesis(umi, genesisAccount)
Ops->>Chain: Finalize with all buckets
Chain-->>Ops: Finalization signature
Ops-->>CLI: Return signature
CLI->>User: Display completion & summary
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip CodeRabbit can suggest fixes for GitHub Check annotations.Configure the |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/genesis/create.ts`:
- Line 193: Extract the duplicated hardcoded API URL into a shared constant and
use it in both runApiWizard and runManualWizard instead of literal strings;
e.g., create a constant (e.g., DEFAULT_METAPLEX_API_URL) and replace the inline
'https://api.metaplex.com' assignments where GenesisApiConfig is constructed so
both functions reference the same value for consistency and easier maintenance.
In `@src/lib/genesis/createGenesisWizardPrompt.ts`:
- Around line 486-721: promptLaunchWizard and promptRegisterLaunch duplicate
prompting logic for token metadata, social links, quote mint, and
project-specific fields; extract that shared flow into small helper functions
(e.g., promptTokenMetadata returning {name,symbol,image,description},
promptSocialLinks returning {website,twitter,telegram}, promptQuoteMint
returning 'SOL'|'USDC', and promptProjectConfig(quoteMint) returning
{tokenAllocation,raiseGoal,raydiumLiquidityBps,fundsRecipient}) and call them
from both functions; ensure each helper does the same validation/abort checks
(checkAbort, validateUrl, parseTimestamp, isPublicKey) and use the helpers'
returned typed objects to assemble the final LaunchWizardResult in
promptLaunchWizard and the analogous result in promptRegisterLaunch.
In `@src/lib/genesis/launchApi.ts`:
- Around line 58-73: The return uses non-null assertions for project launch
fields (params.tokenAllocation!, params.raiseGoal!, params.raydiumLiquidityBps!,
params.fundsRecipient!) which can propagate undefined; add a defensive guard at
the start of the function in launchApi.ts that validates those fields on params
when launchType is 'project' (check params.tokenAllocation, params.raiseGoal,
params.raydiumLiquidityBps, params.fundsRecipient) and throw a clear error if
any are missing, then remove the non-null assertions and pass the validated
values into the object that satisfies CreateProjectLaunchInput.
In `@src/lib/genesis/operations.ts`:
- Around line 150-176: The current flow sends the base transaction via
umiSendAndConfirmTransaction (result) then sends extensions using
addLaunchPoolBucketV2Extensions (extensionsTx) which can fail and leave partial
state; modify create logic so when extensions.length > 0 you capture and return
both signatures (base and extensions) on success, and if the extensionsTx fails,
throw an error that includes the base transaction signature
(result.transaction.signature) and bucketPda to surface the partial state;
ensure the function signature/return type is updated to include optional
extensionsSignature and that failures from umiSendAndConfirmTransaction for
extensions are re-thrown with contextual information about the bucket and base
signature.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7f7946b4-aeec-4d1d-951c-7a6f614d0bce
📒 Files selected for processing (8)
src/commands/genesis/create.tssrc/commands/genesis/launch/create.tssrc/lib/genesis/createGenesisWizardPrompt.tssrc/lib/genesis/launchApi.tssrc/lib/genesis/operations.tstest/commands/genesis/genesis.create.test.tstest/commands/genesis/genesis.launch.test.tstest/commands/genesis/genesis.wizard.test.ts
| const spinner = ora('Creating token launch via Genesis API...').start() | ||
|
|
||
| try { | ||
| const apiConfig: GenesisApiConfig = { baseUrl: 'https://api.metaplex.com' } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Hardcoded API URL duplicated in two places.
The API base URL 'https://api.metaplex.com' is hardcoded in both runApiWizard (line 193) and runManualWizard (line 353). Consider extracting this to a constant or configuration value for consistency and easier maintenance.
♻️ Extract API URL constant
+const DEFAULT_GENESIS_API_URL = 'https://api.metaplex.com'
+
// In runApiWizard:
- const apiConfig: GenesisApiConfig = { baseUrl: 'https://api.metaplex.com' }
+ const apiConfig: GenesisApiConfig = { baseUrl: DEFAULT_GENESIS_API_URL }
// In runManualWizard:
- const apiConfig: GenesisApiConfig = { baseUrl: 'https://api.metaplex.com' }
+ const apiConfig: GenesisApiConfig = { baseUrl: DEFAULT_GENESIS_API_URL }Also applies to: 353-353
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/genesis/create.ts` at line 193, Extract the duplicated hardcoded
API URL into a shared constant and use it in both runApiWizard and
runManualWizard instead of literal strings; e.g., create a constant (e.g.,
DEFAULT_METAPLEX_API_URL) and replace the inline 'https://api.metaplex.com'
assignments where GenesisApiConfig is constructed so both functions reference
the same value for consistency and easier maintenance.
| export async function promptLaunchWizard(): Promise<LaunchWizardResult> { | ||
| // Launch type | ||
| const launchType = await select({ | ||
| message: 'What type of launch?', | ||
| choices: [ | ||
| { name: 'Project - Configurable allocations, 48h deposit, locked vesting', value: 'project' as const }, | ||
| { name: 'Memecoin - Simple launch, 1h deposit, hardcoded fund flows', value: 'memecoin' as const }, | ||
| ], | ||
| }) | ||
|
|
||
| // Token metadata | ||
| console.log('\n--- Token Metadata ---') | ||
|
|
||
| const name = await input({ | ||
| message: 'Token name (1-32 characters) (or q to quit):', | ||
| validate: (v) => { | ||
| if (v.trim().toLowerCase() === 'q') return true | ||
| if (!v.trim()) return 'Name is required' | ||
| if (v.length > 32) return 'Max 32 characters' | ||
| return true | ||
| }, | ||
| }) | ||
| checkAbort(name) | ||
|
|
||
| const symbol = await input({ | ||
| message: 'Token symbol (1-10 characters) (or q to quit):', | ||
| validate: (v) => { | ||
| if (v.trim().toLowerCase() === 'q') return true | ||
| if (!v.trim()) return 'Symbol is required' | ||
| if (v.length > 10) return 'Max 10 characters' | ||
| return true | ||
| }, | ||
| }) | ||
| checkAbort(symbol) | ||
|
|
||
| const image = await input({ | ||
| message: 'Token image URL (must start with https://gateway.irys.xyz/) (or q to quit):', | ||
| validate: (v) => { | ||
| if (v.trim().toLowerCase() === 'q') return true | ||
| if (!v.startsWith('https://gateway.irys.xyz/')) return 'Must start with https://gateway.irys.xyz/' | ||
| return true | ||
| }, | ||
| }) | ||
| checkAbort(image) | ||
|
|
||
| const addDescription = await confirm({ | ||
| message: 'Add a token description?', | ||
| default: false, | ||
| }) | ||
| let description: string | undefined | ||
| if (addDescription) { | ||
| const desc = await input({ | ||
| message: 'Description (max 250 characters) (or q to quit):', | ||
| validate: (v) => { | ||
| if (v.trim().toLowerCase() === 'q') return true | ||
| if (v.length > 250) return 'Max 250 characters' | ||
| return true | ||
| }, | ||
| }) | ||
| checkAbort(desc) | ||
| description = desc | ||
| } | ||
|
|
||
| // Social links | ||
| const addSocials = await confirm({ | ||
| message: 'Add social links (website, twitter, telegram)?', | ||
| default: false, | ||
| }) | ||
| let website: string | undefined | ||
| let twitter: string | undefined | ||
| let telegram: string | undefined | ||
| if (addSocials) { | ||
| const w = await input({ | ||
| message: 'Website URL (or press enter to skip, q to quit):', | ||
| validate: (v) => { | ||
| if (!v.trim()) return true | ||
| checkAbort(v) | ||
| return validateUrl(v, 'website') | ||
| }, | ||
| }) | ||
| if (w.trim()) website = w.trim() | ||
|
|
||
| const t = await input({ | ||
| message: 'Twitter URL (or press enter to skip, q to quit):', | ||
| validate: (v) => { | ||
| if (!v.trim()) return true | ||
| checkAbort(v) | ||
| return validateUrl(v, 'twitter') | ||
| }, | ||
| }) | ||
| if (t.trim()) twitter = t.trim() | ||
|
|
||
| const tg = await input({ | ||
| message: 'Telegram URL (or press enter to skip, q to quit):', | ||
| validate: (v) => { | ||
| if (!v.trim()) return true | ||
| checkAbort(v) | ||
| return validateUrl(v, 'telegram') | ||
| }, | ||
| }) | ||
| if (tg.trim()) telegram = tg.trim() | ||
| } | ||
|
|
||
| // Quote mint | ||
| const quoteMint = await select({ | ||
| message: 'Quote token (what depositors pay with):', | ||
| choices: [ | ||
| { name: 'SOL', value: 'SOL' as const }, | ||
| { name: 'USDC', value: 'USDC' as const }, | ||
| ], | ||
| }) | ||
|
|
||
| // Deposit start time | ||
| console.log('\n--- Launch Schedule ---') | ||
| const depositPeriod = launchType === 'memecoin' ? '1 hour' : '48 hours' | ||
| const depositStartTime = await input({ | ||
| message: `Deposit start time (ISO date or unix timestamp, deposit lasts ${depositPeriod}) (or q to quit):`, | ||
| validate: (v) => { checkAbort(v); return parseTimestamp(v, { requireFuture: true }) }, | ||
| }) | ||
|
|
||
| // Project-only options | ||
| let tokenAllocation: number | undefined | ||
| let raiseGoal: number | undefined | ||
| let raydiumLiquidityBps: number | undefined | ||
| let fundsRecipient: string | undefined | ||
|
|
||
| if (launchType === 'project') { | ||
| console.log('\n--- Launch Pool Configuration ---') | ||
| console.log('Total token supply is 1,000,000,000 (1 billion).') | ||
|
|
||
| const tokenAllocationStr = await input({ | ||
| message: 'Token allocation for launch pool (portion of 1B, e.g. 500000000) (or q to quit):', | ||
| validate: (v) => { | ||
| if (v.trim().toLowerCase() === 'q') return true | ||
| const n = Number(v) | ||
| if (isNaN(n) || n <= 0 || !Number.isInteger(n)) return 'Must be a positive integer' | ||
| if (n > 1_000_000_000) return 'Cannot exceed 1 billion total supply' | ||
| return true | ||
| }, | ||
| }) | ||
| checkAbort(tokenAllocationStr) | ||
| tokenAllocation = Number(tokenAllocationStr) | ||
|
|
||
| const quoteLabel = quoteMint === 'SOL' ? 'SOL' : 'USDC' | ||
| const raiseGoalStr = await input({ | ||
| message: `Raise goal in whole ${quoteLabel} (minimum ${MIN_RAISE_GOAL[quoteLabel]} ${quoteLabel}) (or q to quit):`, | ||
| validate: (v) => { | ||
| if (v.trim().toLowerCase() === 'q') return true | ||
| const n = Number(v) | ||
| if (isNaN(n) || n <= 0) return 'Must be a positive number' | ||
| const min = MIN_RAISE_GOAL[quoteLabel] ?? 0 | ||
| if (n < min) return `Raise goal must be at least ${min} ${quoteLabel}` | ||
| return true | ||
| }, | ||
| }) | ||
| checkAbort(raiseGoalStr) | ||
| raiseGoal = Number(raiseGoalStr) | ||
|
|
||
| const raydiumBpsStr = await input({ | ||
| message: 'Raydium liquidity percentage (20-100, e.g. 50 for 50%) (or q to quit):', | ||
| validate: (v) => { | ||
| if (v.trim().toLowerCase() === 'q') return true | ||
| const n = Number(v) | ||
| if (isNaN(n) || n < 20 || n > 100) return 'Must be between 20 and 100' | ||
| return true | ||
| }, | ||
| }) | ||
| checkAbort(raydiumBpsStr) | ||
| raydiumLiquidityBps = Number(raydiumBpsStr) * 100 | ||
|
|
||
| const remainderPct = 100 - Number(raydiumBpsStr) | ||
| if (remainderPct > 0) { | ||
| console.log(` ${Number(raydiumBpsStr)}% goes to Raydium LP, ${remainderPct}% goes to funds recipient.`) | ||
| } | ||
|
|
||
| const fundsRecipientAddr = await input({ | ||
| message: 'Funds recipient wallet address (or q to quit):', | ||
| validate: (v) => { | ||
| if (v.trim().toLowerCase() === 'q') return true | ||
| if (!isPublicKey(v)) return 'Invalid Solana public key' | ||
| return true | ||
| }, | ||
| }) | ||
| checkAbort(fundsRecipientAddr) | ||
| fundsRecipient = fundsRecipientAddr | ||
| } | ||
|
|
||
| // Confirmation summary | ||
| console.log('\n--- Launch Summary ---') | ||
| console.log(` Type: ${launchType}`) | ||
| console.log(` Token: ${name} (${symbol})`) | ||
| console.log(` Image: ${image}`) | ||
| if (description) console.log(` Description: ${description}`) | ||
| if (website) console.log(` Website: ${website}`) | ||
| if (twitter) console.log(` Twitter: ${twitter}`) | ||
| if (telegram) console.log(` Telegram: ${telegram}`) | ||
| console.log(` Quote Mint: ${quoteMint}`) | ||
| console.log(` Deposit Start: ${depositStartTime}`) | ||
| console.log(` Deposit Duration: ${depositPeriod}`) | ||
| if (launchType === 'project') { | ||
| console.log(` Token Allocation: ${tokenAllocation!.toLocaleString()}`) | ||
| console.log(` Raise Goal: ${raiseGoal} ${quoteMint}`) | ||
| console.log(` Raydium Liquidity: ${raydiumLiquidityBps! / 100}%`) | ||
| console.log(` Funds Recipient: ${fundsRecipient}`) | ||
| } | ||
| console.log('') | ||
|
|
||
| const proceed = await confirm({ | ||
| message: 'Proceed with creating this launch?', | ||
| default: true, | ||
| }) | ||
|
|
||
| if (!proceed) { | ||
| console.log('Aborting wizard.') | ||
| process.exit(0) | ||
| } | ||
|
|
||
| return { | ||
| launchType, | ||
| name, | ||
| symbol, | ||
| image, | ||
| description, | ||
| website, | ||
| twitter, | ||
| telegram, | ||
| depositStartTime, | ||
| quoteMint, | ||
| ...(launchType === 'project' && { | ||
| tokenAllocation, | ||
| raiseGoal, | ||
| raydiumLiquidityBps, | ||
| fundsRecipient, | ||
| }), | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Significant code duplication between promptLaunchWizard and promptRegisterLaunch.
These two functions share substantial overlapping logic:
- Token metadata collection (name, symbol, image, description)
- Social links prompts (website, twitter, telegram)
- Quote mint selection
- Project-specific fields (tokenAllocation, raiseGoal, raydiumLiquidityBps, fundsRecipient)
Consider extracting shared prompt sequences into helper functions to reduce duplication and improve maintainability.
♻️ Suggested extraction approach
// Extract shared helpers like:
async function promptTokenMetadata(): Promise<{name: string; symbol: string; image: string; description?: string}> { ... }
async function promptSocialLinks(): Promise<{website?: string; twitter?: string; telegram?: string}> { ... }
async function promptProjectConfig(quoteMint: 'SOL' | 'USDC'): Promise<{tokenAllocation: number; raiseGoal: number; raydiumLiquidityBps: number; fundsRecipient: string}> { ... }
// Then compose in promptLaunchWizard and promptRegisterLaunchAlso applies to: 757-926
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/genesis/createGenesisWizardPrompt.ts` around lines 486 - 721,
promptLaunchWizard and promptRegisterLaunch duplicate prompting logic for token
metadata, social links, quote mint, and project-specific fields; extract that
shared flow into small helper functions (e.g., promptTokenMetadata returning
{name,symbol,image,description}, promptSocialLinks returning
{website,twitter,telegram}, promptQuoteMint returning 'SOL'|'USDC', and
promptProjectConfig(quoteMint) returning
{tokenAllocation,raiseGoal,raydiumLiquidityBps,fundsRecipient}) and call them
from both functions; ensure each helper does the same validation/abort checks
(checkAbort, validateUrl, parseTimestamp, isPublicKey) and use the helpers'
returned typed objects to assemble the final LaunchWizardResult in
promptLaunchWizard and the analogous result in promptRegisterLaunch.
| return { | ||
| wallet, | ||
| token, | ||
| launchType: 'project', | ||
| launch: { | ||
| launchpool: { | ||
| tokenAllocation: params.tokenAllocation!, | ||
| depositStartTime: params.depositStartTime, | ||
| raiseGoal: params.raiseGoal!, | ||
| raydiumLiquidityBps: params.raydiumLiquidityBps!, | ||
| fundsRecipient: params.fundsRecipient!, | ||
| }, | ||
| }, | ||
| network, | ||
| ...(params.quoteMint !== 'SOL' && { quoteMint: params.quoteMint as QuoteMintInput }), | ||
| } satisfies CreateProjectLaunchInput |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Non-null assertions rely on caller validation for project launch fields.
The project launch path uses non-null assertions (!) for required fields (tokenAllocation, raiseGoal, raydiumLiquidityBps, fundsRecipient), which will pass undefined to the API if the caller doesn't validate these fields first. While the command layer does validate, this function could be called from other contexts in the future.
Consider adding a guard clause for defensive programming:
🛡️ Proposed defensive validation
+ if (params.launchType === 'project') {
+ if (params.tokenAllocation === undefined || params.raiseGoal === undefined ||
+ params.raydiumLiquidityBps === undefined || !params.fundsRecipient) {
+ throw new Error('Project launches require tokenAllocation, raiseGoal, raydiumLiquidityBps, and fundsRecipient')
+ }
+ }
+
return {
wallet,
token,
launchType: 'project',
launch: {
launchpool: {
- tokenAllocation: params.tokenAllocation!,
+ tokenAllocation: params.tokenAllocation,
depositStartTime: params.depositStartTime,
- raiseGoal: params.raiseGoal!,
- raydiumLiquidityBps: params.raydiumLiquidityBps!,
- fundsRecipient: params.fundsRecipient!,
+ raiseGoal: params.raiseGoal,
+ raydiumLiquidityBps: params.raydiumLiquidityBps,
+ fundsRecipient: params.fundsRecipient,
},
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return { | |
| wallet, | |
| token, | |
| launchType: 'project', | |
| launch: { | |
| launchpool: { | |
| tokenAllocation: params.tokenAllocation!, | |
| depositStartTime: params.depositStartTime, | |
| raiseGoal: params.raiseGoal!, | |
| raydiumLiquidityBps: params.raydiumLiquidityBps!, | |
| fundsRecipient: params.fundsRecipient!, | |
| }, | |
| }, | |
| network, | |
| ...(params.quoteMint !== 'SOL' && { quoteMint: params.quoteMint as QuoteMintInput }), | |
| } satisfies CreateProjectLaunchInput | |
| if (params.launchType === 'project') { | |
| if (params.tokenAllocation === undefined || params.raiseGoal === undefined || | |
| params.raydiumLiquidityBps === undefined || !params.fundsRecipient) { | |
| throw new Error('Project launches require tokenAllocation, raiseGoal, raydiumLiquidityBps, and fundsRecipient') | |
| } | |
| } | |
| return { | |
| wallet, | |
| token, | |
| launchType: 'project', | |
| launch: { | |
| launchpool: { | |
| tokenAllocation: params.tokenAllocation, | |
| depositStartTime: params.depositStartTime, | |
| raiseGoal: params.raiseGoal, | |
| raydiumLiquidityBps: params.raydiumLiquidityBps, | |
| fundsRecipient: params.fundsRecipient, | |
| }, | |
| }, | |
| network, | |
| ...(params.quoteMint !== 'SOL' && { quoteMint: params.quoteMint as QuoteMintInput }), | |
| } satisfies CreateProjectLaunchInput |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/genesis/launchApi.ts` around lines 58 - 73, The return uses non-null
assertions for project launch fields (params.tokenAllocation!,
params.raiseGoal!, params.raydiumLiquidityBps!, params.fundsRecipient!) which
can propagate undefined; add a defensive guard at the start of the function in
launchApi.ts that validates those fields on params when launchType is 'project'
(check params.tokenAllocation, params.raiseGoal, params.raydiumLiquidityBps,
params.fundsRecipient) and throw a clear error if any are missing, then remove
the non-null assertions and pass the validated values into the object that
satisfies CreateProjectLaunchInput.
| const result = await umiSendAndConfirmTransaction(umi, baseTx) | ||
|
|
||
| // Add extensions if any | ||
| const extensions: LaunchPoolV2ExtensionArgs[] = [] | ||
| if (params.depositLimit) { | ||
| extensions.push({ __kind: 'DepositLimit', depositLimit: { limit: BigInt(params.depositLimit) } }) | ||
| } | ||
| if (params.minimumDeposit) { | ||
| extensions.push({ __kind: 'MinimumDepositAmount', minimumDepositAmount: { amount: BigInt(params.minimumDeposit) } }) | ||
| } | ||
| if (params.minimumQuoteTokenThreshold) { | ||
| extensions.push({ __kind: 'MinimumQuoteTokenThreshold', minimumQuoteTokenThreshold: { amount: BigInt(params.minimumQuoteTokenThreshold) } }) | ||
| } | ||
|
|
||
| if (extensions.length > 0) { | ||
| const extensionsTx = addLaunchPoolBucketV2Extensions(umi, { | ||
| authority: signer, | ||
| bucket: bucketPda, | ||
| extensions, | ||
| genesisAccount, | ||
| padding: Array.from({ length: 3 }, () => 0), | ||
| payer, | ||
| }) | ||
| await umiSendAndConfirmTransaction(umi, extensionsTx) | ||
| } | ||
|
|
||
| return { bucketPda, signature: result.transaction.signature as Uint8Array } |
There was a problem hiding this comment.
Extensions added in separate transaction may cause partial state on failure.
If the base launch pool bucket transaction succeeds (line 150) but the extensions transaction fails (line 173), the bucket will exist without the requested extensions (deposit limit, minimum deposit, etc.). The function returns the base transaction signature, so the caller won't know extensions failed.
Consider handling this scenario:
🔧 Potential improvement options
- Return both signatures when extensions are added:
export interface AddBucketResult {
bucketPda: PublicKey
signature: Uint8Array
+ extensionsSignature?: Uint8Array
}- Or throw on extension failure with context about partial state:
if (extensions.length > 0) {
const extensionsTx = addLaunchPoolBucketV2Extensions(...)
- await umiSendAndConfirmTransaction(umi, extensionsTx)
+ try {
+ await umiSendAndConfirmTransaction(umi, extensionsTx)
+ } catch (err) {
+ throw new Error(`Bucket created but extensions failed. Bucket PDA: ${bucketPda}. Original error: ${err}`)
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const result = await umiSendAndConfirmTransaction(umi, baseTx) | |
| // Add extensions if any | |
| const extensions: LaunchPoolV2ExtensionArgs[] = [] | |
| if (params.depositLimit) { | |
| extensions.push({ __kind: 'DepositLimit', depositLimit: { limit: BigInt(params.depositLimit) } }) | |
| } | |
| if (params.minimumDeposit) { | |
| extensions.push({ __kind: 'MinimumDepositAmount', minimumDepositAmount: { amount: BigInt(params.minimumDeposit) } }) | |
| } | |
| if (params.minimumQuoteTokenThreshold) { | |
| extensions.push({ __kind: 'MinimumQuoteTokenThreshold', minimumQuoteTokenThreshold: { amount: BigInt(params.minimumQuoteTokenThreshold) } }) | |
| } | |
| if (extensions.length > 0) { | |
| const extensionsTx = addLaunchPoolBucketV2Extensions(umi, { | |
| authority: signer, | |
| bucket: bucketPda, | |
| extensions, | |
| genesisAccount, | |
| padding: Array.from({ length: 3 }, () => 0), | |
| payer, | |
| }) | |
| await umiSendAndConfirmTransaction(umi, extensionsTx) | |
| } | |
| return { bucketPda, signature: result.transaction.signature as Uint8Array } | |
| const result = await umiSendAndConfirmTransaction(umi, baseTx) | |
| // Add extensions if any | |
| const extensions: LaunchPoolV2ExtensionArgs[] = [] | |
| if (params.depositLimit) { | |
| extensions.push({ __kind: 'DepositLimit', depositLimit: { limit: BigInt(params.depositLimit) } }) | |
| } | |
| if (params.minimumDeposit) { | |
| extensions.push({ __kind: 'MinimumDepositAmount', minimumDepositAmount: { amount: BigInt(params.minimumDeposit) } }) | |
| } | |
| if (params.minimumQuoteTokenThreshold) { | |
| extensions.push({ __kind: 'MinimumQuoteTokenThreshold', minimumQuoteTokenThreshold: { amount: BigInt(params.minimumQuoteTokenThreshold) } }) | |
| } | |
| if (extensions.length > 0) { | |
| const extensionsTx = addLaunchPoolBucketV2Extensions(umi, { | |
| authority: signer, | |
| bucket: bucketPda, | |
| extensions, | |
| genesisAccount, | |
| padding: Array.from({ length: 3 }, () => 0), | |
| payer, | |
| }) | |
| try { | |
| await umiSendAndConfirmTransaction(umi, extensionsTx) | |
| } catch (err) { | |
| throw new Error(`Bucket created but extensions failed. Bucket PDA: ${bucketPda}. Original error: ${err}`) | |
| } | |
| } | |
| return { bucketPda, signature: result.transaction.signature as Uint8Array } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/genesis/operations.ts` around lines 150 - 176, The current flow sends
the base transaction via umiSendAndConfirmTransaction (result) then sends
extensions using addLaunchPoolBucketV2Extensions (extensionsTx) which can fail
and leave partial state; modify create logic so when extensions.length > 0 you
capture and return both signatures (base and extensions) on success, and if the
extensionsTx fails, throw an error that includes the base transaction signature
(result.transaction.signature) and bucketPda to surface the partial state;
ensure the function signature/return type is updated to include optional
extensionsSignature and that failures from umiSendAndConfirmTransaction for
extensions are re-thrown with contextual information about the bucket and base
signature.
Adding
--wizardflag to the genesis create command.Test with
pnpm run mplx genesis create --wizard, then choose if you want to go with the API route and register on metaplex.com, or use the more flexible configs