@forgedevv/shared 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/spec.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ /** Base58 Solana public key (32 to 44 characters). */
3
+ export declare const SolanaAddress: z.ZodString;
4
+ export type SolanaAddress = z.infer<typeof SolanaAddress>;
5
+ /** Launchpad slug: becomes `<slug>.<clients-domain>`. */
6
+ export declare const Slug: z.ZodString;
7
+ /** Pure check of the pool creation fee bound: 0 (when allowed) or within [min, max] SOL. */
8
+ export declare function isValidPoolCreationFeeSol(value: number): boolean;
9
+ /**
10
+ * True when a SOL amount converts exactly to lamports: finite, with at most 9 decimals (the same
11
+ * rule as `solToLamports` in @forge/core, which refuses anything else at build time).
12
+ */
13
+ export declare function hasAtMostSolDecimals(value: number): boolean;
14
+ /**
15
+ * Launchpad specification produced by the design chat, consumed by the builder and the signer
16
+ * (docs/INTERFACES.md §2). Bounds come from CLIENT_BOUNDS.
17
+ */
18
+ export declare const LaunchpadSpec: z.ZodObject<{
19
+ version: z.ZodLiteral<1>;
20
+ ownerWallet: z.ZodString;
21
+ name: z.ZodString;
22
+ slug: z.ZodString;
23
+ quote: z.ZodLiteral<"SOL">;
24
+ tradingFeeBps: z.ZodNumber;
25
+ coinCreatorSharePct: z.ZodDefault<z.ZodNumber>;
26
+ poolCreationFeeSol: z.ZodNumber;
27
+ antiSniper: z.ZodDefault<z.ZodBoolean>;
28
+ theme: z.ZodObject<{
29
+ primaryColor: z.ZodString;
30
+ accentColor: z.ZodString;
31
+ darkMode: z.ZodBoolean;
32
+ tagline: z.ZodString;
33
+ logoUrl: z.ZodOptional<z.ZodURL>;
34
+ designNotes: z.ZodString;
35
+ }, z.core.$strip>;
36
+ launchpadCoin: z.ZodObject<{
37
+ name: z.ZodString;
38
+ symbol: z.ZodString;
39
+ description: z.ZodString;
40
+ imageUrl: z.ZodURL;
41
+ firstBuySol: z.ZodNumber;
42
+ }, z.core.$strip>;
43
+ }, z.core.$strip>;
44
+ /** Parsed spec (defaults applied). */
45
+ export type LaunchpadSpec = z.infer<typeof LaunchpadSpec>;
46
+ /** Spec before parsing (fields with defaults are optional). */
47
+ export type LaunchpadSpecInput = z.input<typeof LaunchpadSpec>;
48
+ type DeepPartial<T> = T extends readonly unknown[] ? T : T extends object ? {
49
+ [K in keyof T]?: DeepPartial<T[K]>;
50
+ } : T;
51
+ /** Partial spec while the chat fills it (docs/frontend/CONTRACT.md). Complete = LaunchpadSpec. */
52
+ export type LaunchpadSpecDraft = DeepPartial<Omit<LaunchpadSpecInput, 'version' | 'quote'>> & {
53
+ version?: 1;
54
+ quote?: 'SOL';
55
+ };
56
+ /** Draft validation result. `errors` keys are dotted field paths, e.g. "launchpadCoin.symbol". */
57
+ export interface SpecValidation {
58
+ complete: boolean;
59
+ errors: Record<string, string>;
60
+ }
61
+ /**
62
+ * Validates a draft against LaunchpadSpec. `version` and `quote` are fixed by the contract and
63
+ * filled in when missing. Keeps the first error message per field path.
64
+ */
65
+ export declare function validateSpecDraft(draft: LaunchpadSpecDraft): SpecValidation;
66
+ export {};
package/dist/spec.js ADDED
@@ -0,0 +1,121 @@
1
+ import { z } from 'zod';
2
+ import { CLIENT_BOUNDS } from './constants.js';
3
+ /** Base58 Solana public key (32 to 44 characters). */
4
+ export const SolanaAddress = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/, {
5
+ error: 'Invalid Solana address',
6
+ });
7
+ const HexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/, {
8
+ error: 'Color must be a 6-digit hex value like #7C3AED',
9
+ });
10
+ /** Only http(s) URLs are accepted for images and logos rendered on client sites. */
11
+ const HttpUrl = z.url({ protocol: /^https?$/, error: 'Must be an http(s) URL' });
12
+ /** Launchpad slug: becomes `<slug>.<clients-domain>`. */
13
+ export const Slug = z.string().regex(/^(?=.{3,32}$)[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$/, {
14
+ error: 'Slug must be 3 to 32 characters: lowercase letters, digits and single dashes, not at the start or end',
15
+ });
16
+ /** Metaplex limits on-chain names to 32 UTF-8 bytes, not 32 characters. */
17
+ function utf8Bytes(value) {
18
+ let bytes = 0;
19
+ for (const char of value) {
20
+ const code = char.codePointAt(0) ?? 0;
21
+ bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
22
+ }
23
+ return bytes;
24
+ }
25
+ const { tradingFeeBps, coinCreatorSharePct, poolCreationFeeSol } = CLIENT_BOUNDS;
26
+ /** Pure check of the pool creation fee bound: 0 (when allowed) or within [min, max] SOL. */
27
+ export function isValidPoolCreationFeeSol(value) {
28
+ if (value === 0)
29
+ return poolCreationFeeSol.allowZero;
30
+ return value >= poolCreationFeeSol.min && value <= poolCreationFeeSol.max;
31
+ }
32
+ /** Decimals of SOL: 1 SOL = 10^9 lamports. */
33
+ const SOL_DECIMALS = 9;
34
+ /**
35
+ * True when a SOL amount converts exactly to lamports: finite, with at most 9 decimals (the same
36
+ * rule as `solToLamports` in @forge/core, which refuses anything else at build time).
37
+ */
38
+ export function hasAtMostSolDecimals(value) {
39
+ if (!Number.isFinite(value))
40
+ return false;
41
+ return Number(value.toFixed(SOL_DECIMALS)) === value;
42
+ }
43
+ const SOL_DECIMALS_ERROR = `must have at most ${SOL_DECIMALS} decimals (1 lamport)`;
44
+ /**
45
+ * Launchpad specification produced by the design chat, consumed by the builder and the signer
46
+ * (docs/INTERFACES.md §2). Bounds come from CLIENT_BOUNDS.
47
+ */
48
+ export const LaunchpadSpec = z.object({
49
+ version: z.literal(1),
50
+ /** Client wallet = session wallet = payer = feeClaimer of both configs. */
51
+ ownerWallet: SolanaAddress,
52
+ name: z.string().min(2).max(32),
53
+ slug: Slug,
54
+ quote: z.literal('SOL'),
55
+ tradingFeeBps: z.number().int().min(tradingFeeBps.min).max(tradingFeeBps.max),
56
+ coinCreatorSharePct: z
57
+ .number()
58
+ .int()
59
+ .min(coinCreatorSharePct.min)
60
+ .max(coinCreatorSharePct.max)
61
+ .default(coinCreatorSharePct.default),
62
+ poolCreationFeeSol: z
63
+ .number()
64
+ .refine(isValidPoolCreationFeeSol, {
65
+ error: `poolCreationFeeSol must be 0 or between ${poolCreationFeeSol.min} and ${poolCreationFeeSol.max} SOL`,
66
+ })
67
+ .refine(hasAtMostSolDecimals, { error: `poolCreationFeeSol ${SOL_DECIMALS_ERROR}` }),
68
+ antiSniper: z.boolean().default(true),
69
+ theme: z.object({
70
+ primaryColor: HexColor,
71
+ accentColor: HexColor,
72
+ darkMode: z.boolean(),
73
+ tagline: z.string().max(120),
74
+ logoUrl: HttpUrl.optional(),
75
+ /** Free-form instructions for the builder agent. */
76
+ designNotes: z.string().max(2000),
77
+ }),
78
+ launchpadCoin: z.object({
79
+ /**
80
+ * Written on-chain as is: no surrounding spaces (the on-chain launch refuses them), at most
81
+ * 32 UTF-8 bytes. Name, symbol and the metadata URI also share one byte budget in the launch
82
+ * transaction (`LAUNCH_METADATA_BUDGET_BYTES` in @forge/core).
83
+ */
84
+ name: z
85
+ .string()
86
+ .min(1)
87
+ .refine((value) => value.trim() === value, {
88
+ error: 'Coin name must not start or end with spaces',
89
+ })
90
+ .refine((value) => utf8Bytes(value) <= 32, { error: 'Coin name must be at most 32 bytes' }),
91
+ /** Written on-chain as is: the pattern leaves no room for spaces. */
92
+ symbol: z.string().regex(/^[A-Z0-9]{2,10}$/, {
93
+ error: 'Symbol must be 2 to 10 uppercase letters or digits',
94
+ }),
95
+ description: z.string().max(500),
96
+ imageUrl: HttpUrl,
97
+ /** Paid by the client. */
98
+ // The 10 SOL migration threshold means a first buy above ~10 SOL cannot fit in the curve.
99
+ firstBuySol: z
100
+ .number()
101
+ .min(0)
102
+ .max(10)
103
+ .refine(hasAtMostSolDecimals, { error: `firstBuySol ${SOL_DECIMALS_ERROR}` }),
104
+ }),
105
+ });
106
+ /**
107
+ * Validates a draft against LaunchpadSpec. `version` and `quote` are fixed by the contract and
108
+ * filled in when missing. Keeps the first error message per field path.
109
+ */
110
+ export function validateSpecDraft(draft) {
111
+ const result = LaunchpadSpec.safeParse({ ...draft, version: 1, quote: 'SOL' });
112
+ if (result.success)
113
+ return { complete: true, errors: {} };
114
+ const errors = {};
115
+ for (const issue of result.error.issues) {
116
+ const key = issue.path.map(String).join('.');
117
+ if (!(key in errors))
118
+ errors[key] = issue.message;
119
+ }
120
+ return { complete: false, errors };
121
+ }
@@ -0,0 +1,176 @@
1
+ import type { GatingResponse, Lamports, OwnerTransactionSummary, Quote, Session } from './api.js';
2
+ import type { LaunchpadStatus, PaymentKind } from './db.js';
3
+ import type { FailedStage, JobStatus, JobType } from './jobs.js';
4
+ import type { LaunchpadSpec, LaunchpadSpecDraft, SpecValidation } from './spec.js';
5
+ /** `enabled: false` before the $FORGE launch (then `ok: true`). Amounts are raw token units. */
6
+ export type Gating = GatingResponse;
7
+ export interface Flags {
8
+ signupsPaused: boolean;
9
+ }
10
+ export interface Job {
11
+ id: string;
12
+ launchpadId: string;
13
+ type: JobType;
14
+ status: JobStatus;
15
+ attempts: number;
16
+ failedStage: FailedStage | null;
17
+ error: string | null;
18
+ previewUrl: string | null;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ refund?: {
22
+ lamports: Lamports;
23
+ signature: string;
24
+ } | null;
25
+ }
26
+ export interface JobEvent {
27
+ id: number;
28
+ jobId: string;
29
+ message: string;
30
+ createdAt: string;
31
+ }
32
+ export interface LaunchpadSummary {
33
+ id: string;
34
+ name: string;
35
+ slug: string;
36
+ siteUrl: string | null;
37
+ status: LaunchpadStatus;
38
+ coin: {
39
+ name: string;
40
+ symbol: string;
41
+ mint: string | null;
42
+ imageUrl: string;
43
+ };
44
+ coinsCount: number;
45
+ claimablePartnerFeesLamports: Lamports;
46
+ includedModificationsLeft: number;
47
+ activeJobId: string | null;
48
+ }
49
+ export interface LaunchpadDetail extends LaunchpadSummary {
50
+ /** Complete spec. */
51
+ spec: LaunchpadSpecDraft;
52
+ onchain: {
53
+ launchpadConfig: string | null;
54
+ launchpadCoinConfig: string | null;
55
+ launchpadCoinMint: string | null;
56
+ };
57
+ versions: {
58
+ jobId: string;
59
+ createdAt: string;
60
+ request: string | null;
61
+ previewUrl: string | null;
62
+ }[];
63
+ jobs: Job[];
64
+ }
65
+ export type TxPhase = 'idle' | 'awaiting_signature' | 'sending' | 'confirmed' | 'rejected' | 'error';
66
+ export interface TxResult {
67
+ phase: TxPhase;
68
+ signature?: string;
69
+ error?: string;
70
+ }
71
+ export declare const CLIENT_ERROR_CODES: readonly ["RATE_LIMITED", "UNAUTHORIZED", "GATING_REQUIRED", "SIGNUPS_PAUSED", "QUOTE_EXPIRED", "WRONG_WALLET", "BETA_NOT_INVITED", "NETWORK"];
72
+ export type ClientErrorCode = (typeof CLIENT_ERROR_CODES)[number];
73
+ /** Rejection of client functions; `message` is readable by the end user. */
74
+ export declare class ClientError extends Error {
75
+ readonly code: ClientErrorCode;
76
+ constructor(code: ClientErrorCode, message: string);
77
+ }
78
+ export declare function isClientError(value: unknown): value is ClientError;
79
+ export interface UseSessionResult {
80
+ session: Session | null;
81
+ connect(): Promise<void>;
82
+ disconnect(): Promise<void>;
83
+ status: 'idle' | 'signing' | 'error';
84
+ }
85
+ export interface UseGatingResult {
86
+ gating: Gating | null;
87
+ loading: boolean;
88
+ recheck(): Promise<void>;
89
+ }
90
+ export interface UiChatMessage {
91
+ role: 'user' | 'assistant';
92
+ content: string;
93
+ }
94
+ export interface UseChatOptions {
95
+ conversationId?: string;
96
+ launchpadId?: string;
97
+ }
98
+ export interface UseChatResult {
99
+ messages: UiChatMessage[];
100
+ send(text: string): void;
101
+ streaming: boolean;
102
+ spec: LaunchpadSpecDraft;
103
+ validation: SpecValidation;
104
+ rateLimited: boolean;
105
+ conversationId: string | null;
106
+ }
107
+ export interface UseJobResult {
108
+ job: Job | null;
109
+ events: JobEvent[];
110
+ loading: boolean;
111
+ }
112
+ export interface UseLaunchpadsResult {
113
+ launchpads: LaunchpadSummary[];
114
+ loading: boolean;
115
+ refresh(): void;
116
+ }
117
+ export interface UseLaunchpadResult {
118
+ launchpad: LaunchpadDetail | null;
119
+ loading: boolean;
120
+ }
121
+ /** Options of `formatSol` (not detailed in CONTRACT.md). */
122
+ export interface FormatSolOptions {
123
+ /** Maximum number of fraction digits. */
124
+ maxFractionDigits?: number;
125
+ /** Append the " SOL" unit. */
126
+ withUnit?: boolean;
127
+ }
128
+ export type OnTxPhase = (phase: TxPhase) => void;
129
+ /** One `data:` line of the `POST /api/chat` event stream (INTERFACES §6). */
130
+ export type ChatStreamEvent = {
131
+ type: 'conversation';
132
+ conversationId: string;
133
+ } | {
134
+ type: 'text';
135
+ delta: string;
136
+ } | {
137
+ type: 'spec';
138
+ spec: LaunchpadSpecDraft;
139
+ validation: SpecValidation;
140
+ } | {
141
+ type: 'error';
142
+ code: ClientErrorCode;
143
+ message: string;
144
+ } | {
145
+ type: 'done';
146
+ };
147
+ /** Every function and hook exported by `apps/web/src/client/` (and by its mocks). */
148
+ export interface WebClient {
149
+ useSession(): UseSessionResult;
150
+ useFlags(): Flags;
151
+ useGating(): UseGatingResult;
152
+ useChat(opts: UseChatOptions): UseChatResult;
153
+ confirmSpec(conversationId: string, spec: LaunchpadSpec): Promise<{
154
+ launchpadId: string;
155
+ jobId: string;
156
+ }>;
157
+ requestModification(launchpadId: string, conversationId: string, request: string): Promise<{
158
+ jobId: string;
159
+ }>;
160
+ getQuote(jobId: string, kind: PaymentKind): Promise<Quote>;
161
+ payQuote(quote: Quote, onPhase: OnTxPhase): Promise<TxResult>;
162
+ useJob(jobId: string): UseJobResult;
163
+ approvePreview(jobId: string): Promise<void>;
164
+ getOwnerTransactionSummary(jobId: string): Promise<OwnerTransactionSummary>;
165
+ signOwnerTransaction(jobId: string, onPhase: OnTxPhase): Promise<TxResult>;
166
+ useLaunchpads(): UseLaunchpadsResult;
167
+ useLaunchpad(id: string): UseLaunchpadResult;
168
+ claimPartnerFees(launchpadId: string, onPhase: OnTxPhase): Promise<TxResult>;
169
+ reactivateLaunchpad(launchpadId: string): Promise<void>;
170
+ formatSol(lamports: Lamports, opts?: FormatSolOptions): string;
171
+ formatUsd(n: number): string;
172
+ shortAddress(a: string): string;
173
+ explorerTxUrl(sig: string): string;
174
+ }
175
+ /** Client-facing labels of job statuses. For `failed`, show details according to `failedStage`. */
176
+ export declare const JOB_STATUS_LABELS: Record<JobStatus, string>;
@@ -0,0 +1,37 @@
1
+ export const CLIENT_ERROR_CODES = [
2
+ 'RATE_LIMITED',
3
+ 'UNAUTHORIZED',
4
+ 'GATING_REQUIRED',
5
+ 'SIGNUPS_PAUSED',
6
+ 'QUOTE_EXPIRED',
7
+ 'WRONG_WALLET',
8
+ 'BETA_NOT_INVITED',
9
+ 'NETWORK',
10
+ ];
11
+ /** Rejection of client functions; `message` is readable by the end user. */
12
+ export class ClientError extends Error {
13
+ code;
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.name = 'ClientError';
17
+ this.code = code;
18
+ }
19
+ }
20
+ export function isClientError(value) {
21
+ return value instanceof ClientError;
22
+ }
23
+ /** Client-facing labels of job statuses. For `failed`, show details according to `failedStage`. */
24
+ export const JOB_STATUS_LABELS = {
25
+ spec_ready: 'Awaiting payment',
26
+ paid: 'Payment received',
27
+ building: 'Building',
28
+ preview_ready: 'Preview ready',
29
+ approved: 'Approved',
30
+ onchain_setup: 'Connecting to Meteora',
31
+ awaiting_owner_signature: 'Your turn to sign the launch',
32
+ owner_signed: 'Launch signed',
33
+ deploying: 'Going live',
34
+ live: 'Live',
35
+ failed: 'Problem',
36
+ refunded: 'Refunded',
37
+ };
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@forgedevv/shared",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Shared contracts (constants, zod schemas, types) between all FORGE apps. Published on npm as @forgedevv/shared.",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Forgedevv/Forge.git",
9
+ "directory": "packages/shared"
10
+ },
11
+ "homepage": "https://github.com/Forgedevv/Forge/tree/main/packages/shared#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Forgedevv/Forge/issues"
14
+ },
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "type": "module",
19
+ "sideEffects": false,
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "files": [
30
+ "dist/**/*.js",
31
+ "dist/**/*.d.ts",
32
+ "!dist/**/*.test.*"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "dependencies": {
39
+ "zod": "4.6.5"
40
+ }
6
41
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.