@forgedevv/shared 0.0.0-stage → 0.2.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/api.d.ts +464 -0
- package/dist/api.js +307 -0
- package/dist/constants.d.ts +53 -0
- package/dist/constants.js +42 -0
- package/dist/db.d.ts +189 -0
- package/dist/db.js +75 -0
- package/dist/env.d.ts +21 -0
- package/dist/env.js +92 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/jobs.d.ts +44 -0
- package/dist/jobs.js +76 -0
- package/dist/site-config.d.ts +66 -0
- package/dist/site-config.js +48 -0
- package/dist/spec.d.ts +66 -0
- package/dist/spec.js +121 -0
- package/dist/web-client.d.ts +210 -0
- package/dist/web-client.js +52 -0
- package/package.json +38 -3
- package/README.md +0 -3
package/dist/api.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { JobStatus } from './jobs.js';
|
|
3
|
+
import { PaymentKind, PaymentStatus } from './db.js';
|
|
4
|
+
import { LaunchpadSpec, SolanaAddress } from './spec.js';
|
|
5
|
+
/*
|
|
6
|
+
* Request/response schemas of the apps/web API (docs/INTERFACES.md §6) and of the internal
|
|
7
|
+
* builder -> signer API (§7). Request bodies are strict (unknown keys are rejected); responses
|
|
8
|
+
* strip unknown keys.
|
|
9
|
+
*/
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Primitives
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** Lamports serialized as a decimal string (bigint). 1 SOL = 1_000_000_000 lamports. */
|
|
14
|
+
export const Lamports = z
|
|
15
|
+
.string()
|
|
16
|
+
.regex(/^\d+$/, { error: 'Lamports must be a decimal integer string' });
|
|
17
|
+
/** Raw token amount (smallest units) serialized as a decimal string. */
|
|
18
|
+
export const RawTokenAmount = z.string().regex(/^\d+$/);
|
|
19
|
+
/** Base58 ed25519 signature (transaction signature or signed message). */
|
|
20
|
+
export const Base58Signature = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{64,88}$/, {
|
|
21
|
+
error: 'Invalid base58 signature',
|
|
22
|
+
});
|
|
23
|
+
/** Serialized transaction, base64 encoded. */
|
|
24
|
+
export const Base64Transaction = z.base64().min(1);
|
|
25
|
+
export const Uuid = z.uuid();
|
|
26
|
+
/** ISO 8601 date-time (UTC `Z` or with an offset). */
|
|
27
|
+
export const IsoDateTime = z.iso.datetime({ offset: true });
|
|
28
|
+
/** Path params of routes with `:id`. */
|
|
29
|
+
export const RouteIdParams = z.strictObject({ id: Uuid });
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// apps/web routes (§6)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
export const WEB_API_ROUTES = {
|
|
34
|
+
authNonce: { method: 'POST', path: '/api/auth/nonce' },
|
|
35
|
+
authVerify: { method: 'POST', path: '/api/auth/verify' },
|
|
36
|
+
authSession: { method: 'GET', path: '/api/auth/session' },
|
|
37
|
+
authLogout: { method: 'POST', path: '/api/auth/logout' },
|
|
38
|
+
authSupabaseToken: { method: 'GET', path: '/api/auth/supabase-token' },
|
|
39
|
+
gating: { method: 'GET', path: '/api/gating' },
|
|
40
|
+
flags: { method: 'GET', path: '/api/flags' },
|
|
41
|
+
chat: { method: 'POST', path: '/api/chat' },
|
|
42
|
+
specConfirm: { method: 'POST', path: '/api/spec/confirm' },
|
|
43
|
+
launchpadModifications: { method: 'POST', path: '/api/launchpads/:id/modifications' },
|
|
44
|
+
paymentsQuote: { method: 'POST', path: '/api/payments/quote' },
|
|
45
|
+
paymentsConfirm: { method: 'POST', path: '/api/payments/confirm' },
|
|
46
|
+
jobApprove: { method: 'POST', path: '/api/jobs/:id/approve' },
|
|
47
|
+
ownerTransactionGet: { method: 'GET', path: '/api/jobs/:id/owner-transaction' },
|
|
48
|
+
ownerTransactionSubmit: { method: 'POST', path: '/api/jobs/:id/owner-transaction' },
|
|
49
|
+
jobGet: { method: 'GET', path: '/api/jobs/:id' },
|
|
50
|
+
launchpadsList: { method: 'GET', path: '/api/launchpads' },
|
|
51
|
+
launchpadGet: { method: 'GET', path: '/api/launchpads/:id' },
|
|
52
|
+
claimTransaction: { method: 'GET', path: '/api/launchpads/:id/claim-transaction' },
|
|
53
|
+
launchpadReactivate: { method: 'POST', path: '/api/launchpads/:id/reactivate' },
|
|
54
|
+
launchpadRetry: { method: 'POST', path: '/api/launchpads/:id/retry' },
|
|
55
|
+
rpc: { method: 'POST', path: '/api/rpc' },
|
|
56
|
+
};
|
|
57
|
+
// POST /api/auth/nonce
|
|
58
|
+
export const AuthNonceRequest = z.strictObject({ wallet: SolanaAddress });
|
|
59
|
+
/**
|
|
60
|
+
* `nonce` is an opaque server-signed token (stateless). `message` is the exact text the wallet
|
|
61
|
+
* must sign; the client never builds it. The token is also set as an HttpOnly cookie.
|
|
62
|
+
*/
|
|
63
|
+
export const AuthNonceResponse = z.object({
|
|
64
|
+
nonce: z.string().min(1),
|
|
65
|
+
message: z.string().min(1),
|
|
66
|
+
expiresAt: z.string().min(1),
|
|
67
|
+
});
|
|
68
|
+
// POST /api/auth/verify — signature of the message containing the nonce
|
|
69
|
+
export const AuthVerifyRequest = z.strictObject({
|
|
70
|
+
wallet: SolanaAddress,
|
|
71
|
+
signature: Base58Signature,
|
|
72
|
+
/** The token returned by /api/auth/nonce; optional because it is also sent as a cookie. */
|
|
73
|
+
nonce: z.string().min(1).optional(),
|
|
74
|
+
});
|
|
75
|
+
/** The session. */
|
|
76
|
+
export const AuthVerifyResponse = z.object({ wallet: SolanaAddress });
|
|
77
|
+
// POST /api/auth/logout
|
|
78
|
+
export const AuthLogoutResponse = z.object({ ok: z.literal(true) });
|
|
79
|
+
// GET /api/auth/supabase-token — short-lived JWT for Supabase RLS / Realtime (claim `wallet`)
|
|
80
|
+
export const SupabaseTokenResponse = z.object({
|
|
81
|
+
accessToken: z.string().min(1),
|
|
82
|
+
expiresAt: z.string().min(1),
|
|
83
|
+
});
|
|
84
|
+
export const Session = AuthVerifyResponse;
|
|
85
|
+
// GET /api/gating — FORGE_GATING_AMOUNT of mint FORGE_MINT. Without FORGE_MINT, gating is
|
|
86
|
+
// disabled: `enabled: false` and `ok: true`.
|
|
87
|
+
export const GatingResponse = z.object({
|
|
88
|
+
ok: z.boolean(),
|
|
89
|
+
/** Required raw amount (smallest units). */
|
|
90
|
+
required: RawTokenAmount,
|
|
91
|
+
/** Wallet raw balance (smallest units). */
|
|
92
|
+
balance: RawTokenAmount,
|
|
93
|
+
enabled: z.boolean(),
|
|
94
|
+
/** Decimals of the $FORGE mint, read on-chain with the balance; absent when nothing was read. */
|
|
95
|
+
decimals: z.number().int().min(0).max(18).optional(),
|
|
96
|
+
});
|
|
97
|
+
// GET /api/flags — public kill-switch state needed by the UI
|
|
98
|
+
export const FlagsResponse = z.object({ signupsPaused: z.boolean() });
|
|
99
|
+
// POST /api/chat — text/event-stream of ChatStreamEvent (web-client.ts); always starts with
|
|
100
|
+
// { type: 'conversation', conversationId } and ends with { type: 'done' }.
|
|
101
|
+
export const ChatRequest = z.strictObject({
|
|
102
|
+
conversationId: Uuid.optional(),
|
|
103
|
+
/** Set for a modification of an existing launchpad. */
|
|
104
|
+
launchpadId: Uuid.optional(),
|
|
105
|
+
message: z.string().min(1),
|
|
106
|
+
});
|
|
107
|
+
/** Payload of the first stream event. */
|
|
108
|
+
export const ChatResponse = z.object({ conversationId: Uuid });
|
|
109
|
+
// POST /api/spec/confirm
|
|
110
|
+
export const SpecConfirmRequest = z.strictObject({ conversationId: Uuid, spec: LaunchpadSpec });
|
|
111
|
+
export const SpecConfirmResponse = z.object({ launchpadId: Uuid, jobId: Uuid });
|
|
112
|
+
// POST /api/launchpads/:id/modifications
|
|
113
|
+
export const LaunchpadModificationRequest = z.strictObject({
|
|
114
|
+
conversationId: Uuid,
|
|
115
|
+
request: z.string().min(1),
|
|
116
|
+
});
|
|
117
|
+
export const LaunchpadModificationResponse = z.object({ jobId: Uuid });
|
|
118
|
+
/*
|
|
119
|
+
* POST /api/launchpads/:id/retry (no body) — "start again": a draft whose creation was refunded
|
|
120
|
+
* gets a new `create_launchpad` job in `spec_ready`, built from the launchpad's stored spec (same
|
|
121
|
+
* name and subdomain). Idempotent: while that job awaits payment, the same job id is returned.
|
|
122
|
+
*/
|
|
123
|
+
export const LaunchpadRetryResponse = z.object({ jobId: Uuid });
|
|
124
|
+
// POST /api/payments/quote
|
|
125
|
+
export const PaymentsQuoteRequest = z.strictObject({ jobId: Uuid, kind: PaymentKind });
|
|
126
|
+
/** Quote as seen by the UI (docs/frontend/CONTRACT.md). */
|
|
127
|
+
export const Quote = z.object({
|
|
128
|
+
paymentId: Uuid,
|
|
129
|
+
kind: PaymentKind,
|
|
130
|
+
lamports: Lamports,
|
|
131
|
+
usdAmount: z.number().nonnegative(),
|
|
132
|
+
expiresAt: IsoDateTime,
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* `transaction` is the unsigned payment transaction (base64). It is null when lamports = "0"
|
|
136
|
+
* (an included modification remains and the job goes straight to `paid`).
|
|
137
|
+
*/
|
|
138
|
+
export const PaymentsQuoteResponse = Quote.extend({ transaction: Base64Transaction.nullable() });
|
|
139
|
+
// POST /api/payments/confirm
|
|
140
|
+
export const PaymentsConfirmRequest = z.strictObject({
|
|
141
|
+
paymentId: Uuid,
|
|
142
|
+
signature: Base58Signature,
|
|
143
|
+
});
|
|
144
|
+
export const PaymentsConfirmResponse = z.object({ status: PaymentStatus });
|
|
145
|
+
// POST /api/jobs/:id/approve (no body)
|
|
146
|
+
export const JobApproveResponse = z.object({ status: JobStatus });
|
|
147
|
+
/** Summary shown before the client signs the launch transaction. */
|
|
148
|
+
export const OwnerTransactionSummary = z.object({
|
|
149
|
+
coinName: z.string(),
|
|
150
|
+
coinSymbol: z.string(),
|
|
151
|
+
firstBuyLamports: Lamports,
|
|
152
|
+
/** Everything the owner pays besides the first buy: network fee, rents, Metaplex fee, pool creation fee. */
|
|
153
|
+
estimatedNetworkFeeLamports: Lamports,
|
|
154
|
+
ownerWallet: SolanaAddress,
|
|
155
|
+
/**
|
|
156
|
+
* Balance the owner's wallet must hold before signing: first buy + everything above + the rent
|
|
157
|
+
* of the temporary WSOL account (created and closed within the transaction, so refunded, but
|
|
158
|
+
* held while it runs). Optional: absent when the server cannot compute it; older readers ignore it.
|
|
159
|
+
*/
|
|
160
|
+
ownerRequiredBalanceLamports: Lamports.optional(),
|
|
161
|
+
});
|
|
162
|
+
/** Absolute https URL (the launchpad coin metadata URI). Kept exactly as sent (no normalization). */
|
|
163
|
+
export const HttpsUrl = z.url({ protocol: /^https$/, error: 'Must be an https URL' });
|
|
164
|
+
/**
|
|
165
|
+
* The accounts of the launch transaction, pinned by the browser (`verifyLaunchCoinTransaction` of
|
|
166
|
+
* @forge/core) before the owner signs: the launchpad coin config and mint (launchpad row), the
|
|
167
|
+
* FORGE creator wallet that creates the pool and holds the durable nonce, the durable nonce
|
|
168
|
+
* account, and the coin metadata URI written by the pool creation.
|
|
169
|
+
*/
|
|
170
|
+
export const OwnerTransactionLaunch = z.object({
|
|
171
|
+
config: SolanaAddress,
|
|
172
|
+
mint: SolanaAddress,
|
|
173
|
+
creator: SolanaAddress,
|
|
174
|
+
nonceAccount: SolanaAddress,
|
|
175
|
+
metadataUri: HttpsUrl,
|
|
176
|
+
});
|
|
177
|
+
// GET /api/jobs/:id/owner-transaction — launch transaction partially signed by FORGE
|
|
178
|
+
export const OwnerTransactionGetResponse = z.object({
|
|
179
|
+
transaction: Base64Transaction,
|
|
180
|
+
/** Shown to the client before signing (INTERFACES §6). */
|
|
181
|
+
summary: OwnerTransactionSummary,
|
|
182
|
+
/** Whether the summary comes from a simulation of the exact message; when not `ok`, it is a lower bound. */
|
|
183
|
+
simulation: z.enum(['ok', 'failed', 'unavailable']).optional(),
|
|
184
|
+
/**
|
|
185
|
+
* The launch accounts the transaction must use. Optional in the schema so that older readers and
|
|
186
|
+
* writers still parse; the browser client refuses to sign a launch without it (fail closed).
|
|
187
|
+
*/
|
|
188
|
+
launch: OwnerTransactionLaunch.optional(),
|
|
189
|
+
});
|
|
190
|
+
// POST /api/jobs/:id/owner-transaction
|
|
191
|
+
export const OwnerTransactionSubmitRequest = z.strictObject({
|
|
192
|
+
signedTransaction: Base64Transaction,
|
|
193
|
+
});
|
|
194
|
+
export const OwnerTransactionSubmitResponse = z.object({ signature: Base58Signature });
|
|
195
|
+
// GET /api/launchpads/:id/claim-transaction — client partner fee claim
|
|
196
|
+
export const ClaimTransactionSummary = z.object({
|
|
197
|
+
/** The pool this transaction claims (one pool per transaction). */
|
|
198
|
+
pool: SolanaAddress,
|
|
199
|
+
/** Partner trading fees claimed by this transaction. */
|
|
200
|
+
tradingFeesLamports: Lamports,
|
|
201
|
+
/** Partner pool creation fee claimed by this transaction (0 when none). */
|
|
202
|
+
poolCreationFeeLamports: Lamports,
|
|
203
|
+
/** What the owner pays to claim: network fee and any account rent. */
|
|
204
|
+
ownerCostLamports: Lamports,
|
|
205
|
+
/** Pools still holding claimable fees after this one. */
|
|
206
|
+
remainingPools: z.number().int().nonnegative(),
|
|
207
|
+
});
|
|
208
|
+
export const ClaimTransactionResponse = z.object({
|
|
209
|
+
transaction: Base64Transaction,
|
|
210
|
+
summary: ClaimTransactionSummary.optional(),
|
|
211
|
+
});
|
|
212
|
+
// POST /api/rpc — Helius relay (method allow-list and per-IP rate limit enforced by apps/web)
|
|
213
|
+
const JsonRpcId = z.union([z.string(), z.number(), z.null()]);
|
|
214
|
+
export const RpcRequest = z.strictObject({
|
|
215
|
+
jsonrpc: z.literal('2.0'),
|
|
216
|
+
id: JsonRpcId,
|
|
217
|
+
method: z.string().min(1),
|
|
218
|
+
params: z.union([z.array(z.unknown()), z.record(z.string(), z.unknown())]).optional(),
|
|
219
|
+
});
|
|
220
|
+
export const RpcResponse = z.object({
|
|
221
|
+
jsonrpc: z.literal('2.0'),
|
|
222
|
+
id: JsonRpcId,
|
|
223
|
+
result: z.unknown().optional(),
|
|
224
|
+
error: z
|
|
225
|
+
.object({ code: z.number().int(), message: z.string(), data: z.unknown().optional() })
|
|
226
|
+
.optional(),
|
|
227
|
+
});
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Internal builder -> signer API (§7)
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
/** Lowercase hex HMAC-SHA256 computed with SIGNER_HMAC_SECRET over `signerSignedPayload(...)`. */
|
|
232
|
+
export const SIGNER_SIGNATURE_HEADER = 'X-Forge-Signature';
|
|
233
|
+
/** Unix time in seconds, as a decimal string. */
|
|
234
|
+
export const SIGNER_TIMESTAMP_HEADER = 'X-Forge-Timestamp';
|
|
235
|
+
/**
|
|
236
|
+
* 16 random bytes as 32 lowercase hex characters, fresh for every request (and every retry). The
|
|
237
|
+
* signer accepts a nonce once within the clock-skew window.
|
|
238
|
+
*/
|
|
239
|
+
export const SIGNER_NONCE_HEADER = 'X-Forge-Nonce';
|
|
240
|
+
/** Requests whose timestamp differs from the signer clock by more than this are rejected. */
|
|
241
|
+
export const SIGNER_MAX_CLOCK_SKEW_SECONDS = 60;
|
|
242
|
+
/** First line of the signed string. Any change to its layout gets a new tag. */
|
|
243
|
+
export const SIGNER_SIGNATURE_VERSION = 'forge-signer-v2';
|
|
244
|
+
const SIGNED_FIELD_FORMATS = {
|
|
245
|
+
method: /^[A-Z]{1,16}$/,
|
|
246
|
+
// Origin-form target: printable ASCII, no space, no control character.
|
|
247
|
+
path: /^\/[\x21-\x7e]{0,2047}$/,
|
|
248
|
+
timestamp: /^[0-9]{1,12}$/,
|
|
249
|
+
nonce: /^[0-9a-f]{32}$/,
|
|
250
|
+
bodySha256: /^[0-9a-f]{64}$/,
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* Exact string fed to HMAC-SHA256, one field per line, in this order:
|
|
254
|
+
*
|
|
255
|
+
* forge-signer-v2\n<METHOD>\n<path?query>\n<timestamp>\n<nonce>\n<body sha256>
|
|
256
|
+
*
|
|
257
|
+
* Every field has a strict format that excludes the line feed, and the field count is fixed, so
|
|
258
|
+
* the string decodes to exactly one tuple: a signature made for one method, route, query, time,
|
|
259
|
+
* nonce or body is worth nothing for any other. Throws on a field outside its format, so neither
|
|
260
|
+
* side can sign or check an ambiguous string.
|
|
261
|
+
*/
|
|
262
|
+
export function signerSignedPayload(fields) {
|
|
263
|
+
for (const key of Object.keys(SIGNED_FIELD_FORMATS)) {
|
|
264
|
+
const value = fields[key];
|
|
265
|
+
if (typeof value !== 'string' || !SIGNED_FIELD_FORMATS[key].test(value)) {
|
|
266
|
+
throw new TypeError(`signer signed payload: invalid ${key}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return [
|
|
270
|
+
SIGNER_SIGNATURE_VERSION,
|
|
271
|
+
fields.method,
|
|
272
|
+
fields.path,
|
|
273
|
+
fields.timestamp,
|
|
274
|
+
fields.nonce,
|
|
275
|
+
fields.bodySha256,
|
|
276
|
+
].join('\n');
|
|
277
|
+
}
|
|
278
|
+
export const SIGNER_ROUTES = {
|
|
279
|
+
configs: { method: 'POST', path: '/v1/configs' },
|
|
280
|
+
launchCoinPrepare: { method: 'POST', path: '/v1/launch-coin/prepare' },
|
|
281
|
+
health: { method: 'GET', path: '/v1/health' },
|
|
282
|
+
};
|
|
283
|
+
/*
|
|
284
|
+
* The signer never takes an address or an amount from the request: it re-reads everything in
|
|
285
|
+
* Supabase. Requests are therefore strict and carry only the job id. Both routes are idempotent.
|
|
286
|
+
*/
|
|
287
|
+
// POST /v1/configs
|
|
288
|
+
export const SignerConfigsRequest = z.strictObject({ jobId: Uuid });
|
|
289
|
+
export const SignerConfigsResponse = z.object({
|
|
290
|
+
launchpadConfig: SolanaAddress,
|
|
291
|
+
launchpadCoinConfig: SolanaAddress,
|
|
292
|
+
});
|
|
293
|
+
// POST /v1/launch-coin/prepare — durable-nonce transaction signed by the creator wallet
|
|
294
|
+
export const SignerLaunchPrepareRequest = z.strictObject({ jobId: Uuid });
|
|
295
|
+
export const SignerLaunchPrepareResponse = z.object({
|
|
296
|
+
partiallySignedTx: Base64Transaction,
|
|
297
|
+
mint: SolanaAddress,
|
|
298
|
+
summary: OwnerTransactionSummary.optional(),
|
|
299
|
+
details: z.record(z.string(), z.unknown()).optional(),
|
|
300
|
+
});
|
|
301
|
+
// GET /v1/health
|
|
302
|
+
export const SignerHealthResponse = z.object({
|
|
303
|
+
ok: z.boolean(),
|
|
304
|
+
cluster: z.string().optional(),
|
|
305
|
+
uptimeSeconds: z.number().optional(),
|
|
306
|
+
jobs: z.record(z.string(), z.unknown()).optional(),
|
|
307
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Business constants shared by every FORGE app (docs/INTERFACES.md §1).
|
|
3
|
+
* Percentages are expressed in basis points (bps) unless the name says `Pct`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const PRICING: {
|
|
6
|
+
readonly creationUsd: 33;
|
|
7
|
+
readonly modificationUsd: 5.5;
|
|
8
|
+
readonly includedModifications: 2;
|
|
9
|
+
readonly maxAttemptsBeforeRefund: 2;
|
|
10
|
+
readonly quoteValiditySeconds: 120;
|
|
11
|
+
};
|
|
12
|
+
export declare const FEES: {
|
|
13
|
+
/** Our fee on our own buy button (before graduation). */
|
|
14
|
+
readonly platformFeeBps: 30;
|
|
15
|
+
/** After graduation; Jupiter keeps 20 % of it. */
|
|
16
|
+
readonly jupiterIntegratorFeeBps: 30;
|
|
17
|
+
/** FORGE creator share on the coin of every launchpad. */
|
|
18
|
+
readonly forgeCreatorSharePct: 25;
|
|
19
|
+
};
|
|
20
|
+
export declare const CLIENT_BOUNDS: {
|
|
21
|
+
/** Trading fee range offered to clients. */
|
|
22
|
+
readonly tradingFeeBps: {
|
|
23
|
+
readonly min: 50;
|
|
24
|
+
readonly max: 200;
|
|
25
|
+
};
|
|
26
|
+
readonly coinCreatorSharePct: {
|
|
27
|
+
readonly min: 0;
|
|
28
|
+
readonly max: 50;
|
|
29
|
+
readonly default: 25;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Coin creation fee set by the client: 0 (no fee) or between 0.001 and 1 SOL
|
|
33
|
+
* (0.001 SOL = MIN_POOL_CREATION_FEE of the SDK). Whether the SDK accepts 0 must be
|
|
34
|
+
* confirmed by devnet test 1; otherwise the minimum becomes 0.001.
|
|
35
|
+
*/
|
|
36
|
+
readonly poolCreationFeeSol: {
|
|
37
|
+
readonly min: 0.001;
|
|
38
|
+
readonly max: 1;
|
|
39
|
+
readonly allowZero: true;
|
|
40
|
+
};
|
|
41
|
+
/** Imposed (Meteora migration bots). */
|
|
42
|
+
readonly migrationThresholdSol: 10;
|
|
43
|
+
};
|
|
44
|
+
export declare const OPS: {
|
|
45
|
+
readonly dormantDaysBeforeSleep: 30;
|
|
46
|
+
readonly claimIntervalHours: 24;
|
|
47
|
+
readonly agentBudgetUsdPerJob: 15;
|
|
48
|
+
readonly agentTimeoutMinutes: 30;
|
|
49
|
+
/** Past this delay, an active job with no news is considered crashed. */
|
|
50
|
+
readonly jobLockStaleMinutes: 45;
|
|
51
|
+
};
|
|
52
|
+
/** 1 SOL = 1_000_000_000 lamports. */
|
|
53
|
+
export declare const LAMPORTS_PER_SOL = 1000000000;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Business constants shared by every FORGE app (docs/INTERFACES.md §1).
|
|
3
|
+
* Percentages are expressed in basis points (bps) unless the name says `Pct`.
|
|
4
|
+
*/
|
|
5
|
+
export const PRICING = {
|
|
6
|
+
creationUsd: 33,
|
|
7
|
+
modificationUsd: 5.5,
|
|
8
|
+
includedModifications: 2,
|
|
9
|
+
maxAttemptsBeforeRefund: 2,
|
|
10
|
+
quoteValiditySeconds: 120,
|
|
11
|
+
};
|
|
12
|
+
export const FEES = {
|
|
13
|
+
/** Our fee on our own buy button (before graduation). */
|
|
14
|
+
platformFeeBps: 30,
|
|
15
|
+
/** After graduation; Jupiter keeps 20 % of it. */
|
|
16
|
+
jupiterIntegratorFeeBps: 30,
|
|
17
|
+
/** FORGE creator share on the coin of every launchpad. */
|
|
18
|
+
forgeCreatorSharePct: 25,
|
|
19
|
+
};
|
|
20
|
+
export const CLIENT_BOUNDS = {
|
|
21
|
+
/** Trading fee range offered to clients. */
|
|
22
|
+
tradingFeeBps: { min: 50, max: 200 },
|
|
23
|
+
coinCreatorSharePct: { min: 0, max: 50, default: 25 },
|
|
24
|
+
/**
|
|
25
|
+
* Coin creation fee set by the client: 0 (no fee) or between 0.001 and 1 SOL
|
|
26
|
+
* (0.001 SOL = MIN_POOL_CREATION_FEE of the SDK). Whether the SDK accepts 0 must be
|
|
27
|
+
* confirmed by devnet test 1; otherwise the minimum becomes 0.001.
|
|
28
|
+
*/
|
|
29
|
+
poolCreationFeeSol: { min: 0.001, max: 1, allowZero: true },
|
|
30
|
+
/** Imposed (Meteora migration bots). */
|
|
31
|
+
migrationThresholdSol: 10,
|
|
32
|
+
};
|
|
33
|
+
export const OPS = {
|
|
34
|
+
dormantDaysBeforeSleep: 30,
|
|
35
|
+
claimIntervalHours: 24,
|
|
36
|
+
agentBudgetUsdPerJob: 15,
|
|
37
|
+
agentTimeoutMinutes: 30,
|
|
38
|
+
/** Past this delay, an active job with no news is considered crashed. */
|
|
39
|
+
jobLockStaleMinutes: 45,
|
|
40
|
+
};
|
|
41
|
+
/** 1 SOL = 1_000_000_000 lamports. */
|
|
42
|
+
export const LAMPORTS_PER_SOL = 1_000_000_000;
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { FailedStage, JobStatus, JobType } from './jobs.js';
|
|
3
|
+
import { type LaunchpadSpec } from './spec.js';
|
|
4
|
+
/**
|
|
5
|
+
* Row types of the Supabase tables (docs/INTERFACES.md §5). Column names are kept in snake_case.
|
|
6
|
+
* Nullability mirrors the SQL exactly: a column without `not null` is typed `| null`, even when
|
|
7
|
+
* it has a default.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LaunchpadStatus: z.ZodEnum<{
|
|
10
|
+
live: "live";
|
|
11
|
+
draft: "draft";
|
|
12
|
+
sleeping: "sleeping";
|
|
13
|
+
disabled: "disabled";
|
|
14
|
+
}>;
|
|
15
|
+
export type LaunchpadStatus = z.infer<typeof LaunchpadStatus>;
|
|
16
|
+
/**
|
|
17
|
+
* Shape of `launchpads.site_host` (migration 0011 holds the same CHECK): a lowercase DNS name
|
|
18
|
+
* without scheme, port, path or trailing dot. Under `vercel.app` (Vercel's production aliases),
|
|
19
|
+
* exactly one label; `vercel.app` itself is never a site host.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isSiteHost(value: unknown): value is string;
|
|
22
|
+
/** A site host on `vercel.app` (`<one label>.vercel.app`). */
|
|
23
|
+
export declare function isVercelAppHost(value: unknown): value is string;
|
|
24
|
+
/**
|
|
25
|
+
* The recorded `site_host` of the launchpad `slug`, when a reader may trust it (relay origins,
|
|
26
|
+
* upload `auth.host`, links to the site): exactly `<one label>.vercel.app` (an alias of the
|
|
27
|
+
* launchpad's own Vercel project, checked by the builder before it records one), or exactly
|
|
28
|
+
* `<slug>.<clientsDomain>`. Any other host, however well formed (another domain, the subdomain of
|
|
29
|
+
* another launchpad), is ignored: null. `clientsDomain` is lowercase (readClientsDomain).
|
|
30
|
+
*/
|
|
31
|
+
export declare function trustedSiteHost(siteHost: unknown, slug: string, clientsDomain: string | null): string | null;
|
|
32
|
+
export declare const SiteHost: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
33
|
+
export declare const PaymentStatus: z.ZodEnum<{
|
|
34
|
+
refunded: "refunded";
|
|
35
|
+
quoted: "quoted";
|
|
36
|
+
confirmed: "confirmed";
|
|
37
|
+
expired: "expired";
|
|
38
|
+
}>;
|
|
39
|
+
export type PaymentStatus = z.infer<typeof PaymentStatus>;
|
|
40
|
+
export declare const PaymentKind: z.ZodEnum<{
|
|
41
|
+
creation: "creation";
|
|
42
|
+
modification: "modification";
|
|
43
|
+
}>;
|
|
44
|
+
export type PaymentKind = z.infer<typeof PaymentKind>;
|
|
45
|
+
export declare const ChatRole: z.ZodEnum<{
|
|
46
|
+
user: "user";
|
|
47
|
+
assistant: "assistant";
|
|
48
|
+
system: "system";
|
|
49
|
+
}>;
|
|
50
|
+
export type ChatRole = z.infer<typeof ChatRole>;
|
|
51
|
+
/** Kill switches. */
|
|
52
|
+
export declare const FlagKey: z.ZodEnum<{
|
|
53
|
+
deploys_paused: "deploys_paused";
|
|
54
|
+
buyback_paused: "buyback_paused";
|
|
55
|
+
signups_paused: "signups_paused";
|
|
56
|
+
claims_paused: "claims_paused";
|
|
57
|
+
cashbox_sweep_paused: "cashbox_sweep_paused";
|
|
58
|
+
refunds_paused: "refunds_paused";
|
|
59
|
+
jupiter_claims_paused: "jupiter_claims_paused";
|
|
60
|
+
}>;
|
|
61
|
+
export type FlagKey = z.infer<typeof FlagKey>;
|
|
62
|
+
/** ISO 8601 timestamp as returned by PostgREST for `timestamptz`. */
|
|
63
|
+
export type Timestamptz = string;
|
|
64
|
+
/**
|
|
65
|
+
* Postgres `bigint` / `numeric`: PostgREST may return a JSON number or a string. Convert with
|
|
66
|
+
* `BigInt(String(value))` for lamports, never with floating point math.
|
|
67
|
+
*/
|
|
68
|
+
export type DbBigint = number | string;
|
|
69
|
+
export type DbNumeric = number | string;
|
|
70
|
+
export interface UserRow {
|
|
71
|
+
wallet: string;
|
|
72
|
+
created_at: Timestamptz | null;
|
|
73
|
+
}
|
|
74
|
+
export interface LaunchpadRow {
|
|
75
|
+
id: string;
|
|
76
|
+
owner_wallet: string;
|
|
77
|
+
slug: string;
|
|
78
|
+
spec: LaunchpadSpec;
|
|
79
|
+
github_repo: string | null;
|
|
80
|
+
vercel_project_id: string | null;
|
|
81
|
+
/** Meteora config address. */
|
|
82
|
+
launchpad_config: string | null;
|
|
83
|
+
launchpad_coin_config: string | null;
|
|
84
|
+
launchpad_coin_mint: string | null;
|
|
85
|
+
/** Identifier of the creator wallet inside the signer (never the key). */
|
|
86
|
+
creator_wallet_ref: string | null;
|
|
87
|
+
included_modifications_left: number | null;
|
|
88
|
+
status: LaunchpadStatus;
|
|
89
|
+
last_trade_at: Timestamptz | null;
|
|
90
|
+
created_at: Timestamptz | null;
|
|
91
|
+
/**
|
|
92
|
+
* Exact production host of the client site (`SiteHost`), recorded by the builder from the Vercel
|
|
93
|
+
* API when the site goes live: the verified `<slug>.<CLIENTS_DOMAIN>`, else a `*.vercel.app`
|
|
94
|
+
* alias of the launchpad's own Vercel project. Trusted by exact match only (RPC relay origins,
|
|
95
|
+
* coin uploads, site links). Null until recorded (migration 0011).
|
|
96
|
+
*/
|
|
97
|
+
site_host: string | null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `jobs.owner_tx_details` (jsonb): the public accounts of the launch transaction stored in
|
|
101
|
+
* `jobs.owner_tx`, as the signer reported them (`details` of `POST /v1/launch-coin/prepare`). The
|
|
102
|
+
* builder writes it in the same update as `owner_tx`; apps/web pins these accounts when it checks
|
|
103
|
+
* the stored transaction (`verifyLaunchCoinTransaction` of @forge/core) and hands them to the
|
|
104
|
+
* browser (`OwnerTransactionLaunch`), instead of reading them back from the transaction.
|
|
105
|
+
* `config` is the launchpad coin config. Null on rows written before migration 0009.
|
|
106
|
+
*/
|
|
107
|
+
export declare const JobOwnerTxDetails: z.ZodObject<{
|
|
108
|
+
config: z.ZodString;
|
|
109
|
+
pool: z.ZodString;
|
|
110
|
+
creator: z.ZodString;
|
|
111
|
+
nonceAccount: z.ZodString;
|
|
112
|
+
metadataUri: z.ZodURL;
|
|
113
|
+
}, z.core.$strip>;
|
|
114
|
+
export type JobOwnerTxDetails = z.infer<typeof JobOwnerTxDetails>;
|
|
115
|
+
export interface JobRow {
|
|
116
|
+
id: string;
|
|
117
|
+
launchpad_id: string;
|
|
118
|
+
type: JobType;
|
|
119
|
+
status: JobStatus;
|
|
120
|
+
/** Build attempts only. */
|
|
121
|
+
attempts: number;
|
|
122
|
+
/** Set when status = 'failed'. */
|
|
123
|
+
failed_stage: FailedStage | null;
|
|
124
|
+
/** Client request (modifications). */
|
|
125
|
+
request: string | null;
|
|
126
|
+
preview_url: string | null;
|
|
127
|
+
/** Partially signed launch transaction (base64) with a durable nonce. */
|
|
128
|
+
owner_tx: string | null;
|
|
129
|
+
/** Launch accounts of `owner_tx` (jsonb, validate with `JobOwnerTxDetails` when reading). */
|
|
130
|
+
owner_tx_details: JobOwnerTxDetails | null;
|
|
131
|
+
error: string | null;
|
|
132
|
+
api_cost_usd: DbNumeric | null;
|
|
133
|
+
/** Id of the worker holding the job. */
|
|
134
|
+
locked_by: string | null;
|
|
135
|
+
locked_at: Timestamptz | null;
|
|
136
|
+
created_at: Timestamptz | null;
|
|
137
|
+
updated_at: Timestamptz | null;
|
|
138
|
+
}
|
|
139
|
+
export interface PaymentRow {
|
|
140
|
+
id: string;
|
|
141
|
+
job_id: string;
|
|
142
|
+
/** = launchpads.owner_wallet */
|
|
143
|
+
payer_wallet: string;
|
|
144
|
+
kind: PaymentKind;
|
|
145
|
+
usd_amount: DbNumeric;
|
|
146
|
+
lamports: DbBigint;
|
|
147
|
+
quote_expires_at: Timestamptz;
|
|
148
|
+
tx_signature: string | null;
|
|
149
|
+
status: PaymentStatus;
|
|
150
|
+
refund_signature: string | null;
|
|
151
|
+
created_at: Timestamptz | null;
|
|
152
|
+
}
|
|
153
|
+
export interface ConversationRow {
|
|
154
|
+
id: string;
|
|
155
|
+
owner_wallet: string;
|
|
156
|
+
/** Null while designing, set when the spec is confirmed. */
|
|
157
|
+
launchpad_id: string | null;
|
|
158
|
+
created_at: Timestamptz | null;
|
|
159
|
+
}
|
|
160
|
+
export interface ChatMessageRow {
|
|
161
|
+
id: number;
|
|
162
|
+
conversation_id: string;
|
|
163
|
+
role: ChatRole;
|
|
164
|
+
content: string;
|
|
165
|
+
created_at: Timestamptz | null;
|
|
166
|
+
}
|
|
167
|
+
export interface JobEventRow {
|
|
168
|
+
id: number;
|
|
169
|
+
job_id: string;
|
|
170
|
+
/** Shown in the chat in real time. */
|
|
171
|
+
message: string;
|
|
172
|
+
created_at: Timestamptz | null;
|
|
173
|
+
}
|
|
174
|
+
export interface FlagRow {
|
|
175
|
+
key: FlagKey;
|
|
176
|
+
value: boolean;
|
|
177
|
+
}
|
|
178
|
+
/** Table name -> row type. */
|
|
179
|
+
export interface Tables {
|
|
180
|
+
users: UserRow;
|
|
181
|
+
launchpads: LaunchpadRow;
|
|
182
|
+
jobs: JobRow;
|
|
183
|
+
payments: PaymentRow;
|
|
184
|
+
conversations: ConversationRow;
|
|
185
|
+
chat_messages: ChatMessageRow;
|
|
186
|
+
job_events: JobEventRow;
|
|
187
|
+
flags: FlagRow;
|
|
188
|
+
}
|
|
189
|
+
export type TableName = keyof Tables;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SolanaAddress } from './spec.js';
|
|
3
|
+
/**
|
|
4
|
+
* Row types of the Supabase tables (docs/INTERFACES.md §5). Column names are kept in snake_case.
|
|
5
|
+
* Nullability mirrors the SQL exactly: a column without `not null` is typed `| null`, even when
|
|
6
|
+
* it has a default.
|
|
7
|
+
*/
|
|
8
|
+
export const LaunchpadStatus = z.enum(['draft', 'live', 'sleeping', 'disabled']);
|
|
9
|
+
/** One DNS label: lowercase letters, digits and inner dashes, 1 to 63 characters. */
|
|
10
|
+
const HOST_LABEL = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
|
|
11
|
+
const HOST_NAME_RE = new RegExp(`^(?=.{4,253}$)(?:${HOST_LABEL}\\.)+[a-z]{2,63}$`);
|
|
12
|
+
const VERCEL_APP_HOST_RE = new RegExp(`^${HOST_LABEL}\\.vercel\\.app$`);
|
|
13
|
+
/**
|
|
14
|
+
* Shape of `launchpads.site_host` (migration 0011 holds the same CHECK): a lowercase DNS name
|
|
15
|
+
* without scheme, port, path or trailing dot. Under `vercel.app` (Vercel's production aliases),
|
|
16
|
+
* exactly one label; `vercel.app` itself is never a site host.
|
|
17
|
+
*/
|
|
18
|
+
export function isSiteHost(value) {
|
|
19
|
+
if (typeof value !== 'string' || value === 'vercel.app' || !HOST_NAME_RE.test(value)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return !value.endsWith('.vercel.app') || VERCEL_APP_HOST_RE.test(value);
|
|
23
|
+
}
|
|
24
|
+
/** A site host on `vercel.app` (`<one label>.vercel.app`). */
|
|
25
|
+
export function isVercelAppHost(value) {
|
|
26
|
+
return isSiteHost(value) && value.endsWith('.vercel.app');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The recorded `site_host` of the launchpad `slug`, when a reader may trust it (relay origins,
|
|
30
|
+
* upload `auth.host`, links to the site): exactly `<one label>.vercel.app` (an alias of the
|
|
31
|
+
* launchpad's own Vercel project, checked by the builder before it records one), or exactly
|
|
32
|
+
* `<slug>.<clientsDomain>`. Any other host, however well formed (another domain, the subdomain of
|
|
33
|
+
* another launchpad), is ignored: null. `clientsDomain` is lowercase (readClientsDomain).
|
|
34
|
+
*/
|
|
35
|
+
export function trustedSiteHost(siteHost, slug, clientsDomain) {
|
|
36
|
+
if (!isSiteHost(siteHost))
|
|
37
|
+
return null;
|
|
38
|
+
if (isVercelAppHost(siteHost))
|
|
39
|
+
return siteHost;
|
|
40
|
+
return clientsDomain !== null && siteHost === `${slug}.${clientsDomain}` ? siteHost : null;
|
|
41
|
+
}
|
|
42
|
+
export const SiteHost = z.string().refine(isSiteHost, {
|
|
43
|
+
error: 'Must be a lowercase host name (one label under vercel.app)',
|
|
44
|
+
});
|
|
45
|
+
export const PaymentStatus = z.enum(['quoted', 'confirmed', 'expired', 'refunded']);
|
|
46
|
+
export const PaymentKind = z.enum(['creation', 'modification']);
|
|
47
|
+
export const ChatRole = z.enum(['user', 'assistant', 'system']);
|
|
48
|
+
/** Kill switches. */
|
|
49
|
+
export const FlagKey = z.enum([
|
|
50
|
+
'deploys_paused',
|
|
51
|
+
'buyback_paused',
|
|
52
|
+
'signups_paused',
|
|
53
|
+
'claims_paused',
|
|
54
|
+
'cashbox_sweep_paused',
|
|
55
|
+
'refunds_paused',
|
|
56
|
+
'jupiter_claims_paused',
|
|
57
|
+
]);
|
|
58
|
+
/**
|
|
59
|
+
* `jobs.owner_tx_details` (jsonb): the public accounts of the launch transaction stored in
|
|
60
|
+
* `jobs.owner_tx`, as the signer reported them (`details` of `POST /v1/launch-coin/prepare`). The
|
|
61
|
+
* builder writes it in the same update as `owner_tx`; apps/web pins these accounts when it checks
|
|
62
|
+
* the stored transaction (`verifyLaunchCoinTransaction` of @forge/core) and hands them to the
|
|
63
|
+
* browser (`OwnerTransactionLaunch`), instead of reading them back from the transaction.
|
|
64
|
+
* `config` is the launchpad coin config. Null on rows written before migration 0009.
|
|
65
|
+
*/
|
|
66
|
+
export const JobOwnerTxDetails = z.object({
|
|
67
|
+
config: SolanaAddress,
|
|
68
|
+
pool: SolanaAddress,
|
|
69
|
+
/** FORGE creator wallet of the launchpad: pool creator and durable nonce authority. */
|
|
70
|
+
creator: SolanaAddress,
|
|
71
|
+
/** Durable nonce account of the launch transaction. */
|
|
72
|
+
nonceAccount: SolanaAddress,
|
|
73
|
+
/** Coin metadata URI written by the pool creation (https, kept exactly as sent). */
|
|
74
|
+
metadataUri: z.url({ protocol: /^https$/, error: 'Must be an https URL' }),
|
|
75
|
+
});
|