Skip to content

Commit 6f4f452

Browse files
Add bonding curve swap, bucket fetch, and registration flags (#109)
* Update genesis SDK to v0.34.0 for devnet API compatibility The devnet API now expects updated bonding curve constants (new supply/virtual token ratios). SDK 0.32.1 payloads were rejected with validation errors. * Add bonding curve swap command, bucket fetch, and registration flags - New `genesis swap` command for buying/selling on bonding curves with --info mode for price quotes and curve status - Add bonding curve bucket type to `genesis bucket fetch` with pricing, reserves, and status display - Add --creatorWallet and --twitterVerificationToken flags to `genesis launch create` and `genesis launch register` - Tests for all new features (12 new test cases) * align parameters * Address CodeRabbit review feedback - Use lamports() instead of sol(Number/1e9) to preserve precision - Add BigInt validation with clear errors in --info quote mode - Remove unused stripAnsi import in tests * Fix lint errors in genesis swap, bucket fetch, and launch commands Resolve ESLint errors including import/object/class member sorting, padding lines, prefer-switch, Number.isNaN, Boolean coercion, destructuring, and promise executor return issues. https://claude.ai/code/session_01LNfYbSi7Vkp8c6w6T3zLu8 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 349f84b commit 6f4f452

6 files changed

Lines changed: 1033 additions & 202 deletions

File tree

src/commands/genesis/bucket/fetch.ts

Lines changed: 200 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import {
2-
safeFetchGenesisAccountV2,
2+
findBondingCurveBucketV2Pda,
33
findLaunchPoolBucketV2Pda,
4-
safeFetchLaunchPoolBucketV2,
54
findPresaleBucketV2Pda,
6-
safeFetchPresaleBucketV2,
75
findUnlockedBucketV2Pda,
6+
getCurrentPrice,
7+
getCurrentPriceComponents,
8+
getCurrentPriceQuotePerBase,
9+
getFillPercentage,
10+
isFirstBuyPending,
11+
isSoldOut,
12+
isSwappable,
13+
safeFetchBondingCurveBucketV2,
14+
safeFetchGenesisAccountV2,
15+
safeFetchLaunchPoolBucketV2,
16+
safeFetchPresaleBucketV2,
817
safeFetchUnlockedBucketV2,
918
} from '@metaplex-foundation/genesis'
1019
import { publicKey } from '@metaplex-foundation/umi'
@@ -21,44 +30,49 @@ function formatCondition(condition: { __kind: string; time?: bigint }): string {
2130
if (timestamp === 0) return 'Not set'
2231
return new Date(timestamp * 1000).toISOString()
2332
}
33+
2434
return `${condition.__kind}`
2535
}
2636

2737
export default class BucketFetch extends BaseCommand<typeof BucketFetch> {
28-
static override description = `Fetch a Genesis bucket by genesis address and bucket index.
29-
30-
This command retrieves and displays information about a bucket in a Genesis account.
31-
Supports Launch Pool, Presale, and Unlocked bucket types.`
32-
33-
static override examples = [
34-
'$ mplx genesis bucket fetch GenesisAddress... --bucketIndex 0',
35-
'$ mplx genesis bucket fetch GenesisAddress... -b 1 --type presale',
36-
'$ mplx genesis bucket fetch GenesisAddress... -b 2 --type unlocked',
37-
]
38-
39-
static override usage = 'genesis bucket fetch [GENESIS] [FLAGS]'
40-
4138
static override args = {
4239
genesis: Args.string({
4340
description: 'The Genesis account address',
4441
required: true,
4542
}),
4643
}
4744

45+
static override description = `Fetch a Genesis bucket by genesis address and bucket index.
46+
47+
This command retrieves and displays information about a bucket in a Genesis account.
48+
Supports Launch Pool, Presale, Unlocked, and Bonding Curve bucket types.
49+
50+
When --type is not specified, the command auto-detects the bucket type by trying
51+
all known types at the given index.`
52+
53+
static override examples = [
54+
'$ mplx genesis bucket fetch GenesisAddress...',
55+
'$ mplx genesis bucket fetch GenesisAddress... -b 1',
56+
'$ mplx genesis bucket fetch GenesisAddress... --type presale -b 0',
57+
'$ mplx genesis bucket fetch GenesisAddress... --type bonding-curve',
58+
]
59+
4860
static override flags = {
4961
bucketIndex: Flags.integer({
5062
char: 'b',
51-
description: 'Index of the bucket to fetch',
5263
default: 0,
64+
description: 'Index of the bucket to fetch',
5365
}),
5466
type: Flags.option({
5567
char: 't',
56-
description: 'Type of bucket to fetch',
57-
default: 'launch-pool',
58-
options: ['launch-pool', 'presale', 'unlocked'] as const,
68+
description: 'Type of bucket to fetch (auto-detected if not specified)',
69+
options: ['launch-pool', 'presale', 'unlocked', 'bonding-curve'] as const,
70+
required: false,
5971
})(),
6072
}
6173

74+
static override usage = 'genesis bucket fetch [GENESIS] [FLAGS]'
75+
6276
public async run(): Promise<unknown> {
6377
const { args, flags } = await this.parse(BucketFetch)
6478
const spinner = ora('Fetching bucket...').start()
@@ -77,24 +91,170 @@ Supports Launch Pool, Presale, and Unlocked bucket types.`
7791

7892
spinner.text = 'Fetching bucket details...'
7993

80-
if (flags.type === 'presale') {
81-
return await this.fetchPresaleBucket(genesisAddress, flags.bucketIndex, spinner)
82-
} else if (flags.type === 'unlocked') {
83-
return await this.fetchUnlockedBucket(genesisAddress, flags.bucketIndex, spinner)
84-
} else {
85-
return await this.fetchLaunchPoolBucket(genesisAddress, flags.bucketIndex, spinner)
94+
if (flags.type) {
95+
// Explicit type specified
96+
if (flags.type === 'presale') {
97+
return await this.fetchPresaleBucket(genesisAddress, flags.bucketIndex, spinner)
98+
}
99+
100+
if (flags.type === 'unlocked') {
101+
return await this.fetchUnlockedBucket(genesisAddress, flags.bucketIndex, spinner)
102+
}
103+
104+
if (flags.type === 'bonding-curve') {
105+
return await this.fetchBondingCurveBucket(genesisAddress, flags.bucketIndex, spinner)
106+
}
107+
108+
return await this.fetchLaunchPoolBucket(genesisAddress, flags.bucketIndex, spinner)
109+
86110
}
87111

112+
// Auto-detect: try all bucket types at this index
113+
spinner.text = 'Detecting bucket type...'
114+
return await this.fetchAutoDetect(genesisAddress, flags.bucketIndex, spinner)
115+
88116
} catch (error) {
89117
spinner.fail('Failed to fetch bucket')
90118
throw error
91119
}
92120
}
93121

122+
private async fetchAutoDetect(genesisAddress: ReturnType<typeof publicKey>, bucketIndex: number, spinner: ReturnType<typeof ora>): Promise<unknown> {
123+
// Probe all bucket types at this index in parallel
124+
const [bc, lp, pre, ul] = await Promise.all([
125+
safeFetchBondingCurveBucketV2(this.context.umi, findBondingCurveBucketV2Pda(this.context.umi, { bucketIndex, genesisAccount: genesisAddress })[0]),
126+
safeFetchLaunchPoolBucketV2(this.context.umi, findLaunchPoolBucketV2Pda(this.context.umi, { bucketIndex, genesisAccount: genesisAddress })[0]),
127+
safeFetchPresaleBucketV2(this.context.umi, findPresaleBucketV2Pda(this.context.umi, { bucketIndex, genesisAccount: genesisAddress })[0]),
128+
safeFetchUnlockedBucketV2(this.context.umi, findUnlockedBucketV2Pda(this.context.umi, { bucketIndex, genesisAccount: genesisAddress })[0]),
129+
])
130+
131+
const found: { fn: () => Promise<unknown>; type: string }[] = []
132+
if (bc) found.push({ fn: () => this.fetchBondingCurveBucket(genesisAddress, bucketIndex, spinner), type: 'bonding-curve' })
133+
if (lp) found.push({ fn: () => this.fetchLaunchPoolBucket(genesisAddress, bucketIndex, spinner), type: 'launch-pool' })
134+
if (pre) found.push({ fn: () => this.fetchPresaleBucket(genesisAddress, bucketIndex, spinner), type: 'presale' })
135+
if (ul) found.push({ fn: () => this.fetchUnlockedBucket(genesisAddress, bucketIndex, spinner), type: 'unlocked' })
136+
137+
if (found.length === 0) {
138+
spinner.fail('Bucket not found')
139+
this.error(`No bucket found at index ${bucketIndex}. Tried all bucket types (launch-pool, presale, unlocked, bonding-curve).`)
140+
}
141+
142+
if (found.length === 1) {
143+
return found[0].fn()
144+
}
145+
146+
// Multiple bucket types at the same index — show all
147+
spinner.succeed(`Found ${found.length} buckets at index ${bucketIndex}`)
148+
const results: unknown[] = await Promise.all(found.map(async ({ fn }) => {
149+
const result = await fn()
150+
this.log('')
151+
return result
152+
}))
153+
154+
return results
155+
}
156+
157+
private async fetchBondingCurveBucket(genesisAddress: ReturnType<typeof publicKey>, bucketIndex: number, spinner: ReturnType<typeof ora>): Promise<unknown> {
158+
const [bucketPda] = findBondingCurveBucketV2Pda(this.context.umi, {
159+
bucketIndex,
160+
genesisAccount: genesisAddress,
161+
})
162+
163+
const bucket = await safeFetchBondingCurveBucketV2(this.context.umi, bucketPda)
164+
165+
if (!bucket) {
166+
spinner.fail('Bucket not found')
167+
this.error(`Bonding curve bucket not found at index ${bucketIndex}. It may be a different bucket type or not exist.`)
168+
}
169+
170+
spinner.succeed('Bucket fetched successfully!')
171+
172+
const price = getCurrentPrice(bucket)
173+
const priceQuotePerBase = getCurrentPriceQuotePerBase(bucket)
174+
const { baseReserves, quoteReserves } = getCurrentPriceComponents(bucket)
175+
const firstBuyPending = isFirstBuyPending(bucket)
176+
const swappable = isSwappable(bucket)
177+
const soldOut = isSoldOut(bucket)
178+
const fillPct = getFillPercentage(bucket)
179+
180+
this.log('')
181+
this.logSuccess(`Bonding Curve Bucket`)
182+
this.log('')
183+
this.log('Bucket Details:')
184+
this.log(` Address: ${bucketPda}`)
185+
this.log(` Type: ${KEY_TYPES[bucket.key] || 'BondingCurveV2'}`)
186+
this.log(` Genesis Account: ${bucket.bucket.genesis}`)
187+
this.log(` Bucket Index: ${bucket.bucket.bucketIndex}`)
188+
this.log('')
189+
this.log('Allocation:')
190+
this.log(` Base Token Allocation: ${bucket.bucket.baseTokenAllocation.toString()}`)
191+
this.log(` Base Token Balance: ${bucket.bucket.baseTokenBalance.toString()}`)
192+
this.log(` Quote Token Deposit Total: ${bucket.quoteTokenDepositTotal.toString()}`)
193+
this.log('')
194+
this.log('Pricing:')
195+
this.log(` Current Price (tokens per quote): ${price.toString()}`)
196+
this.log(` Current Price (quote per token): ${priceQuotePerBase.toString()}`)
197+
this.log(` Base Reserves: ${baseReserves.toString()}`)
198+
this.log(` Quote Reserves: ${quoteReserves.toString()}`)
199+
this.log('')
200+
this.log('Constant Product Params:')
201+
this.log(` Virtual SOL: ${bucket.constantProductParams.virtualSol.toString()}`)
202+
this.log(` Virtual Tokens: ${bucket.constantProductParams.virtualTokens.toString()}`)
203+
this.log('')
204+
this.log('Status:')
205+
this.log(` First Buy Pending: ${firstBuyPending ? 'Yes' : 'No'}`)
206+
this.log(` Swappable: ${swappable ? 'Yes' : 'No'}`)
207+
this.log(` Sold Out: ${soldOut ? 'Yes' : 'No'}`)
208+
this.log(` Fill Percentage: ${fillPct.toFixed(2)}%`)
209+
this.log('')
210+
this.log('Conditions:')
211+
this.log(` Swap Start: ${formatCondition(bucket.swapStartCondition)}`)
212+
this.log(` Swap End: ${formatCondition(bucket.swapEndCondition)}`)
213+
this.log('')
214+
this.log('Fees:')
215+
this.log(` Deposit Fee: ${bucket.depositFee.toString()}`)
216+
this.log(` Withdraw Fee: ${bucket.withdrawFee.toString()}`)
217+
this.log(` Creator Fee Accrued: ${bucket.creatorFeeAccrued.toString()}`)
218+
this.log(` Creator Fee Claimed: ${bucket.creatorFeeClaimed.toString()}`)
219+
this.log('')
220+
this.log('View on Explorer:')
221+
this.log(
222+
generateExplorerUrl(
223+
this.context.explorer,
224+
this.context.chain,
225+
bucketPda,
226+
'account'
227+
)
228+
)
229+
230+
return {
231+
address: bucketPda.toString(),
232+
baseReserves: baseReserves.toString(),
233+
baseTokenAllocation: bucket.bucket.baseTokenAllocation.toString(),
234+
baseTokenBalance: bucket.bucket.baseTokenBalance.toString(),
235+
bucketIndex: bucket.bucket.bucketIndex,
236+
creatorFeeAccrued: bucket.creatorFeeAccrued.toString(),
237+
creatorFeeClaimed: bucket.creatorFeeClaimed.toString(),
238+
currentPrice: price.toString(),
239+
currentPriceQuotePerBase: priceQuotePerBase.toString(),
240+
explorer: generateExplorerUrl(this.context.explorer, this.context.chain, bucketPda, 'account'),
241+
fillPercentage: fillPct,
242+
firstBuyPending,
243+
genesisAccount: bucket.bucket.genesis.toString(),
244+
quoteReserves: quoteReserves.toString(),
245+
quoteTokenDepositTotal: bucket.quoteTokenDepositTotal.toString(),
246+
soldOut,
247+
swappable,
248+
type: 'bonding-curve',
249+
virtualSol: bucket.constantProductParams.virtualSol.toString(),
250+
virtualTokens: bucket.constantProductParams.virtualTokens.toString(),
251+
}
252+
}
253+
94254
private async fetchLaunchPoolBucket(genesisAddress: ReturnType<typeof publicKey>, bucketIndex: number, spinner: ReturnType<typeof ora>): Promise<unknown> {
95255
const [bucketPda] = findLaunchPoolBucketV2Pda(this.context.umi, {
96-
genesisAccount: genesisAddress,
97256
bucketIndex,
257+
genesisAccount: genesisAddress,
98258
})
99259

100260
const bucket = await safeFetchLaunchPoolBucketV2(this.context.umi, bucketPda)
@@ -149,20 +309,20 @@ Supports Launch Pool, Presale, and Unlocked bucket types.`
149309
)
150310

151311
return {
152-
type: 'launch-pool',
153312
address: bucketPda.toString(),
154-
genesisAccount: bucket.bucket.genesis.toString(),
155-
bucketIndex: bucket.bucket.bucketIndex,
156313
baseTokenAllocation: bucket.bucket.baseTokenAllocation.toString(),
157314
baseTokenBalance: bucket.bucket.baseTokenBalance.toString(),
315+
bucketIndex: bucket.bucket.bucketIndex,
158316
explorer: generateExplorerUrl(this.context.explorer, this.context.chain, bucketPda, 'account'),
317+
genesisAccount: bucket.bucket.genesis.toString(),
318+
type: 'launch-pool',
159319
}
160320
}
161321

162322
private async fetchPresaleBucket(genesisAddress: ReturnType<typeof publicKey>, bucketIndex: number, spinner: ReturnType<typeof ora>): Promise<unknown> {
163323
const [bucketPda] = findPresaleBucketV2Pda(this.context.umi, {
164-
genesisAccount: genesisAddress,
165324
bucketIndex,
325+
genesisAccount: genesisAddress,
166326
})
167327

168328
const bucket = await safeFetchPresaleBucketV2(this.context.umi, bucketPda)
@@ -216,21 +376,21 @@ Supports Launch Pool, Presale, and Unlocked bucket types.`
216376
)
217377

218378
return {
219-
type: 'presale',
220379
address: bucketPda.toString(),
221-
genesisAccount: bucket.bucket.genesis.toString(),
222-
bucketIndex: bucket.bucket.bucketIndex,
380+
allocationQuoteTokenCap: bucket.allocationQuoteTokenCap.toString(),
223381
baseTokenAllocation: bucket.bucket.baseTokenAllocation.toString(),
224382
baseTokenBalance: bucket.bucket.baseTokenBalance.toString(),
225-
allocationQuoteTokenCap: bucket.allocationQuoteTokenCap.toString(),
383+
bucketIndex: bucket.bucket.bucketIndex,
226384
explorer: generateExplorerUrl(this.context.explorer, this.context.chain, bucketPda, 'account'),
385+
genesisAccount: bucket.bucket.genesis.toString(),
386+
type: 'presale',
227387
}
228388
}
229389

230390
private async fetchUnlockedBucket(genesisAddress: ReturnType<typeof publicKey>, bucketIndex: number, spinner: ReturnType<typeof ora>): Promise<unknown> {
231391
const [bucketPda] = findUnlockedBucketV2Pda(this.context.umi, {
232-
genesisAccount: genesisAddress,
233392
bucketIndex,
393+
genesisAccount: genesisAddress,
234394
})
235395

236396
const bucket = await safeFetchUnlockedBucketV2(this.context.umi, bucketPda)
@@ -274,15 +434,15 @@ Supports Launch Pool, Presale, and Unlocked bucket types.`
274434
)
275435

276436
return {
277-
type: 'unlocked',
278437
address: bucketPda.toString(),
279-
genesisAccount: bucket.bucket.genesis.toString(),
280-
bucketIndex: bucket.bucket.bucketIndex,
281438
baseTokenAllocation: bucket.bucket.baseTokenAllocation.toString(),
282439
baseTokenBalance: bucket.bucket.baseTokenBalance.toString(),
283-
recipient: bucket.recipient.toString(),
440+
bucketIndex: bucket.bucket.bucketIndex,
284441
claimed: bucket.claimed,
285442
explorer: generateExplorerUrl(this.context.explorer, this.context.chain, bucketPda, 'account'),
443+
genesisAccount: bucket.bucket.genesis.toString(),
444+
recipient: bucket.recipient.toString(),
445+
type: 'unlocked',
286446
}
287447
}
288448
}

src/commands/genesis/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export default class Genesis extends Command {
99
'<%= config.bin %> genesis deposit GenesisAddress123... --amount 1000',
1010
'<%= config.bin %> genesis claim GenesisAddress123...',
1111
'<%= config.bin %> genesis finalize GenesisAddress123...',
12+
'<%= config.bin %> genesis swap GenesisAddress123... --direction buy --amount 100000000',
13+
'<%= config.bin %> genesis swap GenesisAddress123... --info',
1214
]
1315

1416
public async run(): Promise<void> {
@@ -23,6 +25,7 @@ export default class Genesis extends Command {
2325
this.log(' genesis withdraw Withdraw from a launch pool')
2426
this.log(' genesis claim Claim tokens from a completed launch')
2527
this.log(' genesis claim-unlocked Claim tokens from an unlocked bucket')
28+
this.log(' genesis swap Buy/sell on a bonding curve (also: --info for status & quotes)')
2629
this.log(' genesis transition Execute end behaviors for a bucket')
2730
this.log(' genesis finalize Finalize a Genesis launch')
2831
this.log(' genesis revoke Revoke/cancel a Genesis launch')

0 commit comments

Comments
 (0)