diff --git a/app/src/components/launchpad/LaunchForm.tsx b/app/src/components/launchpad/LaunchForm.tsx index b45c1d7..2f91025 100644 --- a/app/src/components/launchpad/LaunchForm.tsx +++ b/app/src/components/launchpad/LaunchForm.tsx @@ -947,6 +947,8 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = D

Includes price impact and the pool fee; the exact amount is quoted on-chain right before the buy.

+ ) : suggestion.reason === "not-issued" ? ( +

Nothing suggested: no {quote.symbol} has been issued yet, so there is none to buy with. The launch stays free.

) : suggestion.reason === "insufficient" ? (

Suggested {fmtQuoteUnits(Number(defaultFirstBuy(quote)), quote.decimals)} {quote.symbol}, but this wallet {sharedGas ? `does not hold enough ${quote.symbol} for the buy plus its gas` : "holds only gas"}. The launch stays free; you can buy on the token page later.

) : suggestion.reason === "no-gas" ? ( diff --git a/app/src/components/launchpad/launch-form.test.ts b/app/src/components/launchpad/launch-form.test.ts index ae6b4f9..9acd962 100644 --- a/app/src/components/launchpad/launch-form.test.ts +++ b/app/src/components/launchpad/launch-form.test.ts @@ -162,6 +162,8 @@ test("stock picker: a stock nobody holds yet is marked in the list, and picking // a warning, not a block: the launch works and trading starts once the issuer puts some in circulation assert.match(source, /\{stock\?\.issued === false \? \(\s*

\s*None of \{stock\.symbol\} has been issued yet, so nobody holds any and nobody can buy your token with it until Coinbase issues some\. You can still launch now\.\s*<\/p>\s*\) : null\}/); assert.doesNotMatch(source, /issued === true|issued !== false|!\w+\.issued\b/, "never read a missing or unknown flag as \"not issued\""); + // and no first buy is suggested in it (first-buy.ts): the step says why instead of showing an amount nobody can hold + assert.match(source, /\) : suggestion\.reason === "not-issued" \? \(\s*

Nothing suggested: no \{quote\.symbol\} has been issued yet, so there is none to buy with\. The launch stays free\.<\/p>/); }); test("beneficiary split: recipients come from the tested helper, and the summary reflects the split", () => { diff --git a/app/src/lib/launchpad/first-buy.test.ts b/app/src/lib/launchpad/first-buy.test.ts index 781c77d..ae228e3 100644 --- a/app/src/lib/launchpad/first-buy.test.ts +++ b/app/src/lib/launchpad/first-buy.test.ts @@ -18,6 +18,18 @@ test("default: the first preset for ETH, USDG and GITLAWB; $25 worth for a stock assert.equal(defaultFirstBuy({ ...nvda, usd: null }), null); }); +test("a stock nobody holds yet (issued false): nothing to suggest, wallet or not; an unknown issue state changes nothing", () => { + const coin = { ...nvda, issued: false }; + assert.equal(defaultFirstBuy(coin), null, "so the form offers no \"suggest again\" either"); + assert.deepEqual(suggestFirstBuy({ ...base, quote: coin, connected: false, balance: undefined, nativeBalance: undefined, gasReserve: GAS }), { amount: null, reason: "not-issued" }, "before a wallet connects too: no provisional 0.11 COINc in the preview"); + assert.deepEqual(suggestFirstBuy({ ...base, quote: coin, balance: 0n, nativeBalance: GAS, gasReserve: GAS }), { amount: null, reason: "not-issued" }, "named for why, not as a short balance"); + assert.deepEqual(suggestFirstBuy({ ...base, quote: coin, balance: 0n, nativeBalance: GAS, gasReserve: GAS, declined: true }), { amount: null, reason: "declined" }); + for (const issued of [true, null, undefined]) { + assert.equal(defaultFirstBuy({ ...nvda, issued }), "0.11", String(issued)); + assert.deepEqual(suggestFirstBuy({ ...base, quote: { ...nvda, issued }, connected: false, balance: undefined, nativeBalance: undefined, gasReserve: GAS }), { amount: "0.11", provisional: true }, String(issued)); + } +}); + test("amountForUsd: short decimals parseUnits accepts, never more places than the quote has, null when it rounds away", () => { assert.equal(amountForUsd(25, 2500, 18), "0.01"); assert.equal(amountForUsd(25, 1, 6), "25"); diff --git a/app/src/lib/launchpad/first-buy.ts b/app/src/lib/launchpad/first-buy.ts index 3a2f344..6ea5aa8 100644 --- a/app/src/lib/launchpad/first-buy.ts +++ b/app/src/lib/launchpad/first-buy.ts @@ -9,6 +9,7 @@ * quote balance cannot cover the amount, the native balance cannot cover the buy's gas (for an ETH quote, and for a quote * that is the chain's gas token in ERC-20 form such as USDC on Arc, both come from the same balance), or a balance read failed. While a read is still pending the suggestion stays, marked provisional. * The creator can clear it at any time. An amount the creator TYPED keeps the strict checks (LaunchForm.tsx). + * Nothing is suggested in a stock the issuer has not put in circulation yet (`issued === false`): no wallet can hold any. */ import type { Quote } from "./config"; @@ -18,7 +19,7 @@ export const SUGGESTED_BUY_USD = 25; /** Quick-pick amounts per quote; the first one is the suggestion (about $25 at the prices this was written at). */ export const BUY_PRESETS: Record = { eth: ["0.01", "0.05", "0.1", "0.25"], usdg: ["25", "100", "250"], usdc: ["25", "100", "250"], gitlawb: ["500000", "1000000", "5000000"], twig: ["1000000", "2500000", "5000000"], museworld: [] /* not offered in the form */, stock: [], other: [] /* the form never offers an unlisted quote */ }; -export type NoSuggestion = "no-price" | "unknown-balance" | "insufficient" | "no-gas" | "declined"; +export type NoSuggestion = "no-price" | "not-issued" | "unknown-balance" | "insufficient" | "no-gas" | "declined"; /** `provisional`: shown as selected but not yet confirmed against a balance (no wallet yet, or the read is pending). */ export type FirstBuySuggestion = { amount: string; provisional: boolean; reason?: undefined } | { amount: null; reason: NoSuggestion }; @@ -40,15 +41,16 @@ export function amountForUsd(usd: number, quoteUsd: number, decimals: number): s return rounded === "0" || rounded === "" ? null : rounded; } -/** The amount the form suggests for a quote before looking at the wallet: the first preset, or $25 worth for a stock. */ -export function defaultFirstBuy(quote: Pick): string | null { +/** The amount the form suggests for a quote before looking at the wallet: the first preset, or $25 worth for a stock (none for a stock nobody holds yet). */ +export function defaultFirstBuy(quote: Pick): string | null { + if (quote.issued === false) return null; const preset = BUY_PRESETS[quote.key][0]; if (preset) return preset; return quote.usd ? amountForUsd(SUGGESTED_BUY_USD, quote.usd, quote.decimals) : null; } export type SuggestInput = { - quote: Pick; + quote: Pick; connected: boolean; /** Wallet balance of what the buy is paid with (wei of the quote); undefined while unknown or after a failed read. */ balance: bigint | undefined; @@ -73,6 +75,7 @@ export function gasReserveInQuote(reserveWei: bigint, quoteDecimals: number): bi export function suggestFirstBuy(i: SuggestInput): FirstBuySuggestion { if (i.declined) return { amount: null, reason: "declined" }; + if (i.quote.issued === false) return { amount: null, reason: "not-issued" }; const amount = defaultFirstBuy(i.quote); if (!amount) return { amount: null, reason: "no-price" }; if (!i.connected) return { amount, provisional: true }; // selected from the start; a wallet can only take it away