Skip to content

Add wizard for genesis#82

Open
MarkSackerberg wants to merge 3 commits intomainfrom
feat/genesis-wizard
Open

Add wizard for genesis#82
MarkSackerberg wants to merge 3 commits intomainfrom
feat/genesis-wizard

Conversation

@MarkSackerberg
Copy link
Contributor

@MarkSackerberg MarkSackerberg commented Mar 17, 2026

Adding --wizard flag 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

…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
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added interactive wizard mode (--wizard flag) for Genesis account creation with step-by-step guidance through setup and bucket configuration
    • Added interactive wizard mode (--wizard flag) for Genesis launch creation with streamlined token and project parameter entry
    • Introduced guided prompts for bucket management, validation, and platform registration within the wizard flows
  • Bug Fixes

    • Updated error messages for missing required flags to provide clearer feedback

Walkthrough

Introduces 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

Cohort / File(s) Summary
Genesis Command Creation & Launch
src/commands/genesis/create.ts, src/commands/genesis/launch/create.ts
Replaces flag-based flows with --wizard flag support. Introduces runWizard, executeLaunch, and runApiWizard methods. Changes name, symbol, totalSupply from required to optional when wizard is enabled. Refactors input construction to use centralized params object and new buildLaunchInput/API functions.
Genesis Wizard Prompts
src/lib/genesis/createGenesisWizardPrompt.ts
New comprehensive interactive wizard module with 9 exported prompt functions covering genesis account creation, launch pool/presale/unlocked bucket configuration, launch type selection, and platform registration. Includes input validation, timestamp parsing, URL validation, and abort handling.
Genesis API & Operations
src/lib/genesis/launchApi.ts, src/lib/genesis/operations.ts
Introduces buildLaunchInput factory for constructing API launch inputs and five new operation functions (createGenesisAccount, addLaunchPoolBucket, addPresaleBucket, addUnlockedBucket, finalizeGenesis) with corresponding interfaces for high-level genesis lifecycle management.
Genesis Tests
test/commands/genesis/genesis.create.test.ts, test/commands/genesis/genesis.launch.test.ts, test/commands/genesis/genesis.wizard.test.ts
Updates error message assertions in existing tests and adds extensive PTY-based interactive test suite covering wizard abort, manual genesis creation, API-driven launch creation, and validation scenarios.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • blockiosaurus
  • tonyboylehub
  • nhanphan
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No description was provided by the author; the PR lacks any explanatory content about the changes. Add a description explaining the wizard functionality, key changes, and how users should test the new interactive flows.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add wizard for genesis' directly summarizes the main change: adding an interactive wizard interface for genesis account and launch creation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/genesis-wizard
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can suggest fixes for GitHub Check annotations.

Configure the reviews.tools.github-checks setting to adjust the time to wait for GitHub Checks to complete.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8e48e76 and 344475d.

📒 Files selected for processing (8)
  • src/commands/genesis/create.ts
  • src/commands/genesis/launch/create.ts
  • src/lib/genesis/createGenesisWizardPrompt.ts
  • src/lib/genesis/launchApi.ts
  • src/lib/genesis/operations.ts
  • test/commands/genesis/genesis.create.test.ts
  • test/commands/genesis/genesis.launch.test.ts
  • test/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' }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +486 to +721
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,
}),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 promptRegisterLaunch

Also 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.

Comment on lines +58 to +73
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Suggested change
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.

Comment on lines +150 to +176
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 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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
  1. Return both signatures when extensions are added:
 export interface AddBucketResult {
   bucketPda: PublicKey
   signature: Uint8Array
+  extensionsSignature?: Uint8Array
 }
  1. 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant