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