sui.ski 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.
Files changed (73) hide show
  1. package/AGENTS.md +311 -0
  2. package/CLAUDE.md +292 -0
  3. package/CODEBASE_GUIDE.md +217 -0
  4. package/README.md +77 -0
  5. package/biome.json +28 -0
  6. package/package.json +73 -0
  7. package/scripts/deploy-messaging-mainnet.sh +184 -0
  8. package/scripts/extract-suins-object.ts +180 -0
  9. package/scripts/full-deploy.sh +26 -0
  10. package/scripts/obsidian.ts +243 -0
  11. package/scripts/set-suins-contenthash.ts +130 -0
  12. package/scripts/setup-ika-dwallet.ts +338 -0
  13. package/scripts/transfer-upgrade-cap-from-nft.ts +86 -0
  14. package/src/durable-objects/wallet-session.ts +333 -0
  15. package/src/handlers/app.ts +1430 -0
  16. package/src/handlers/authenticated-events.ts +267 -0
  17. package/src/handlers/dashboard.ts +1659 -0
  18. package/src/handlers/landing.ts +6751 -0
  19. package/src/handlers/mcp.ts +556 -0
  20. package/src/handlers/messaging-sdk.ts +220 -0
  21. package/src/handlers/profile.css.ts +9332 -0
  22. package/src/handlers/profile.ts +12640 -0
  23. package/src/handlers/register2.ts +2811 -0
  24. package/src/handlers/ski-sign.ts +1901 -0
  25. package/src/handlers/ski.ts +314 -0
  26. package/src/handlers/thunder.ts +940 -0
  27. package/src/handlers/vault.ts +284 -0
  28. package/src/handlers/wallet-api.ts +169 -0
  29. package/src/handlers/x402-register.ts +601 -0
  30. package/src/index.test.ts +55 -0
  31. package/src/index.ts +512 -0
  32. package/src/resolvers/content.ts +231 -0
  33. package/src/resolvers/rpc.ts +222 -0
  34. package/src/resolvers/suins.ts +266 -0
  35. package/src/sdk/messaging.ts +279 -0
  36. package/src/types.ts +230 -0
  37. package/src/utils/agent-keypair.ts +40 -0
  38. package/src/utils/authenticated-events.ts +280 -0
  39. package/src/utils/cache.ts +82 -0
  40. package/src/utils/media-pack.ts +27 -0
  41. package/src/utils/mmr.ts +181 -0
  42. package/src/utils/ns-price.ts +529 -0
  43. package/src/utils/og-image.ts +141 -0
  44. package/src/utils/onchain-activity.ts +211 -0
  45. package/src/utils/onchain-listing.ts +39 -0
  46. package/src/utils/premium.ts +29 -0
  47. package/src/utils/pricing.ts +291 -0
  48. package/src/utils/pyth-price-info.ts +63 -0
  49. package/src/utils/response.ts +204 -0
  50. package/src/utils/rpc.ts +25 -0
  51. package/src/utils/shared-wallet-js.ts +166 -0
  52. package/src/utils/social.ts +152 -0
  53. package/src/utils/status.ts +39 -0
  54. package/src/utils/subdomain.ts +116 -0
  55. package/src/utils/surflux-grpc.ts +241 -0
  56. package/src/utils/swap-transactions.ts +1222 -0
  57. package/src/utils/thunder-css.ts +1341 -0
  58. package/src/utils/thunder-js.ts +5046 -0
  59. package/src/utils/transactions.ts +65 -0
  60. package/src/utils/vault.ts +18 -0
  61. package/src/utils/wallet-kit-js.ts +2312 -0
  62. package/src/utils/wallet-session-js.ts +192 -0
  63. package/src/utils/wallet-tx-js.ts +2287 -0
  64. package/src/utils/wallet-ui-js.ts +3057 -0
  65. package/src/utils/x402-middleware.ts +428 -0
  66. package/src/utils/x402-sui.ts +171 -0
  67. package/src/utils/zksend-js.ts +166 -0
  68. package/tsconfig.json +22 -0
  69. package/workers/x402-multichain/src/index.ts +237 -0
  70. package/workers/x402-multichain/src/types.ts +80 -0
  71. package/workers/x402-multichain/tsconfig.json +20 -0
  72. package/workers/x402-multichain/wrangler.toml +11 -0
  73. package/wrangler.toml +84 -0
@@ -0,0 +1,291 @@
1
+ import { SuiJsonRpcClient as SuiClient } from '@mysten/sui/jsonRpc'
2
+ import { mainPackage, SuinsClient } from '@mysten/suins'
3
+ import type { Env } from '../types'
4
+ import { cacheKey, getCached, setCache } from './cache'
5
+ import { getNSSuiPrice, getUSDCSuiPrice, NS_SCALE } from './ns-price'
6
+ import { calculatePremium } from './premium'
7
+ import { getPythPriceInfoObjectId } from './pyth-price-info'
8
+ import { getDefaultRpcUrl } from './rpc'
9
+
10
+ const GRACE_PERIOD_MS = 30 * 24 * 60 * 60 * 1000
11
+ const PRICING_CACHE_TTL = 300
12
+ const NS_DISCOUNT_PERCENT = 25
13
+ const SWAP_SLIPPAGE_BPS = 100
14
+
15
+ interface PricingResult {
16
+ directSuiMist: bigint
17
+ discountedSuiMist: bigint
18
+ nsNeededMist: bigint
19
+ savingsMist: bigint
20
+ savingsPercent: number
21
+ nsPerSui: number
22
+ suiPerNs: number
23
+ isGracePeriod: boolean
24
+ priceInfoObjectId?: string
25
+ expirationMs?: number
26
+ gracePeriodEndMs?: number
27
+ breakdown: {
28
+ basePriceUsd: number
29
+ discountedPriceUsd: number
30
+ premiumUsd: number
31
+ suiPriceUsd: number
32
+ nsDiscountPercent: number
33
+ swapSlippageBps: number
34
+ }
35
+ }
36
+
37
+ interface CalculatePriceParams {
38
+ domain: string
39
+ years: number
40
+ expirationMs?: number
41
+ env: Env
42
+ }
43
+
44
+ export async function calculateRegistrationPrice(
45
+ params: CalculatePriceParams,
46
+ ): Promise<PricingResult> {
47
+ const { domain, years, expirationMs, env } = params
48
+ const cleanDomain = `${domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
49
+
50
+ const key = cacheKey('reg-price-v3', cleanDomain, String(years), String(expirationMs || 0))
51
+ const cached = await getCached<Record<string, unknown>>(key)
52
+ if (cached) {
53
+ return deserializePricingResult(cached)
54
+ }
55
+
56
+ const client = new SuiClient({
57
+ url: env.SUI_RPC_URL || getDefaultRpcUrl(env.SUI_NETWORK),
58
+ network: env.SUI_NETWORK,
59
+ })
60
+ const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
61
+ const suinsClient = new SuinsClient({ client: client as never, network })
62
+
63
+ const nsFeedId =
64
+ network === 'mainnet' ? mainPackage.mainnet.coins.NS.feed : mainPackage.testnet.coins.NS.feed
65
+ const [basePriceUnits, usdcResult, nsPriceResult, priceInfoObjectId] = await Promise.all([
66
+ suinsClient.calculatePrice({ name: cleanDomain, years }),
67
+ getUSDCSuiPrice(env),
68
+ getNSSuiPrice(env),
69
+ nsFeedId
70
+ ? getPythPriceInfoObjectId(client, network, nsFeedId).catch(() => undefined)
71
+ : Promise.resolve(undefined),
72
+ ])
73
+ const suiPriceUsd = usdcResult.usdcPerSui
74
+
75
+ const basePriceUsd = Number(basePriceUnits) / 1e6
76
+
77
+ let premiumUsd = 0
78
+ let isGracePeriod = false
79
+ let gracePeriodEndMs: number | undefined
80
+
81
+ if (expirationMs) {
82
+ const now = Date.now()
83
+ gracePeriodEndMs = expirationMs + GRACE_PERIOD_MS
84
+
85
+ if (now < gracePeriodEndMs) {
86
+ isGracePeriod = true
87
+ const premiumResult = calculatePremium(expirationMs, now, suiPriceUsd)
88
+ premiumUsd = premiumResult.suiPremium * suiPriceUsd
89
+ }
90
+ }
91
+
92
+ const totalPriceUsd = basePriceUsd + premiumUsd
93
+ const discountedPriceUsd = totalPriceUsd * (1 - NS_DISCOUNT_PERCENT / 100)
94
+
95
+ const directSuiAmount = totalPriceUsd / suiPriceUsd
96
+ const directSuiMist = BigInt(Math.ceil(directSuiAmount * 1e9))
97
+
98
+ const discountedSuiAmount = discountedPriceUsd / suiPriceUsd
99
+ const nsNeeded = discountedSuiAmount * nsPriceResult.nsPerSui
100
+ const nsNeededMist = BigInt(Math.ceil(nsNeeded * NS_SCALE))
101
+
102
+ const suiForSwap = nsNeeded * nsPriceResult.suiPerNs
103
+ const suiWithSlippage = suiForSwap * (1 + SWAP_SLIPPAGE_BPS / 10000)
104
+ const discountedSuiMist = BigInt(Math.ceil(suiWithSlippage * 1e9))
105
+
106
+ const savingsMist = directSuiMist - discountedSuiMist
107
+ const savingsPercent = Number((savingsMist * 10000n) / directSuiMist) / 100
108
+
109
+ const result: PricingResult = {
110
+ directSuiMist,
111
+ discountedSuiMist,
112
+ nsNeededMist,
113
+ savingsMist,
114
+ savingsPercent,
115
+ nsPerSui: nsPriceResult.nsPerSui,
116
+ suiPerNs: nsPriceResult.suiPerNs,
117
+ isGracePeriod,
118
+ priceInfoObjectId,
119
+ expirationMs,
120
+ gracePeriodEndMs,
121
+ breakdown: {
122
+ basePriceUsd,
123
+ discountedPriceUsd,
124
+ premiumUsd,
125
+ suiPriceUsd,
126
+ nsDiscountPercent: NS_DISCOUNT_PERCENT,
127
+ swapSlippageBps: SWAP_SLIPPAGE_BPS,
128
+ },
129
+ }
130
+
131
+ await setCache(key, serializablePricingResult(result), PRICING_CACHE_TTL)
132
+
133
+ return result
134
+ }
135
+
136
+ function serializablePricingResult(result: PricingResult): Record<string, unknown> {
137
+ return {
138
+ ...result,
139
+ directSuiMist: String(result.directSuiMist),
140
+ discountedSuiMist: String(result.discountedSuiMist),
141
+ nsNeededMist: String(result.nsNeededMist),
142
+ savingsMist: String(result.savingsMist),
143
+ priceInfoObjectId: result.priceInfoObjectId,
144
+ }
145
+ }
146
+
147
+ function deserializePricingResult(cached: Record<string, unknown>): PricingResult {
148
+ return {
149
+ ...cached,
150
+ directSuiMist: BigInt(cached.directSuiMist as string),
151
+ discountedSuiMist: BigInt(cached.discountedSuiMist as string),
152
+ nsNeededMist: BigInt(cached.nsNeededMist as string),
153
+ savingsMist: BigInt(cached.savingsMist as string),
154
+ } as PricingResult
155
+ }
156
+
157
+ export function formatPricingResponse(result: PricingResult): Record<string, unknown> {
158
+ return {
159
+ directSuiMist: String(result.directSuiMist),
160
+ discountedSuiMist: String(result.discountedSuiMist),
161
+ nsNeededMist: String(result.nsNeededMist),
162
+ savingsMist: String(result.savingsMist),
163
+ savingsPercent: result.savingsPercent,
164
+ nsPerSui: result.nsPerSui,
165
+ suiPerNs: result.suiPerNs,
166
+ isGracePeriod: result.isGracePeriod,
167
+ priceInfoObjectId: result.priceInfoObjectId,
168
+ expirationMs: result.expirationMs,
169
+ gracePeriodEndMs: result.gracePeriodEndMs,
170
+ breakdown: result.breakdown,
171
+ }
172
+ }
173
+
174
+ export async function calculateRenewalPrice(params: CalculatePriceParams): Promise<PricingResult> {
175
+ const { domain, years, env } = params
176
+ const cleanDomain = `${domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
177
+
178
+ const key = cacheKey('renew-price-v1', cleanDomain, String(years))
179
+ const cached = await getCached<Record<string, unknown>>(key)
180
+ if (cached) {
181
+ return deserializePricingResult(cached)
182
+ }
183
+
184
+ const client = new SuiClient({
185
+ url: env.SUI_RPC_URL || getDefaultRpcUrl(env.SUI_NETWORK),
186
+ network: env.SUI_NETWORK,
187
+ })
188
+ const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
189
+ const suinsClient = new SuinsClient({ client: client as never, network })
190
+
191
+ const nsFeedId =
192
+ network === 'mainnet' ? mainPackage.mainnet.coins.NS.feed : mainPackage.testnet.coins.NS.feed
193
+ const [basePriceUnits, usdcResult, nsPriceResult, priceInfoObjectId] = await Promise.all([
194
+ suinsClient.calculatePrice({ name: cleanDomain, years, isRegistration: false }),
195
+ getUSDCSuiPrice(env),
196
+ getNSSuiPrice(env),
197
+ nsFeedId
198
+ ? getPythPriceInfoObjectId(client, network, nsFeedId).catch(() => undefined)
199
+ : Promise.resolve(undefined),
200
+ ])
201
+ const suiPriceUsd = usdcResult.usdcPerSui
202
+
203
+ const basePriceUsd = Number(basePriceUnits) / 1e6
204
+ const discountedPriceUsd = basePriceUsd * (1 - NS_DISCOUNT_PERCENT / 100)
205
+
206
+ const directSuiAmount = basePriceUsd / suiPriceUsd
207
+ const directSuiMist = BigInt(Math.ceil(directSuiAmount * 1e9))
208
+
209
+ const discountedSuiAmount = discountedPriceUsd / suiPriceUsd
210
+ const nsNeeded = discountedSuiAmount * nsPriceResult.nsPerSui
211
+ const nsNeededMist = BigInt(Math.ceil(nsNeeded * NS_SCALE))
212
+
213
+ const suiForSwap = nsNeeded * nsPriceResult.suiPerNs
214
+ const suiWithSlippage = suiForSwap * (1 + SWAP_SLIPPAGE_BPS / 10000)
215
+ const discountedSuiMist = BigInt(Math.ceil(suiWithSlippage * 1e9))
216
+
217
+ const savingsMist = directSuiMist - discountedSuiMist
218
+ const savingsPercent = Number((savingsMist * 10000n) / directSuiMist) / 100
219
+
220
+ const result: PricingResult = {
221
+ directSuiMist,
222
+ discountedSuiMist,
223
+ nsNeededMist,
224
+ savingsMist,
225
+ savingsPercent,
226
+ nsPerSui: nsPriceResult.nsPerSui,
227
+ suiPerNs: nsPriceResult.suiPerNs,
228
+ isGracePeriod: false,
229
+ priceInfoObjectId,
230
+ breakdown: {
231
+ basePriceUsd,
232
+ discountedPriceUsd,
233
+ premiumUsd: 0,
234
+ suiPriceUsd,
235
+ nsDiscountPercent: NS_DISCOUNT_PERCENT,
236
+ swapSlippageBps: SWAP_SLIPPAGE_BPS,
237
+ },
238
+ }
239
+
240
+ await setCache(key, serializablePricingResult(result), PRICING_CACHE_TTL)
241
+
242
+ return result
243
+ }
244
+
245
+ export async function getBasePricing(env: Env): Promise<Record<string, unknown>> {
246
+ const client = new SuiClient({
247
+ url: env.SUI_RPC_URL || getDefaultRpcUrl(env.SUI_NETWORK),
248
+ network: env.SUI_NETWORK,
249
+ })
250
+ const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
251
+ const suinsClient = new SuinsClient({ client: client as never, network })
252
+
253
+ const [priceList, usdcResult, nsPriceResult] = await Promise.all([
254
+ suinsClient.getPriceList(),
255
+ getUSDCSuiPrice(env),
256
+ getNSSuiPrice(env),
257
+ ])
258
+ const suiPriceUsd = usdcResult.usdcPerSui
259
+
260
+ const pricing: Record<string, unknown> = {
261
+ suiPriceUsd,
262
+ nsPerSui: nsPriceResult.nsPerSui,
263
+ suiPerNs: nsPriceResult.suiPerNs,
264
+ nsDiscountPercent: NS_DISCOUNT_PERCENT,
265
+ swapSlippageBps: SWAP_SLIPPAGE_BPS,
266
+ tiers: {} as Record<string, unknown>,
267
+ }
268
+
269
+ const tiers = pricing.tiers as Record<string, unknown>
270
+
271
+ for (const [key, value] of priceList.entries()) {
272
+ const [minLen, maxLen] = key
273
+ const keyStr = minLen === maxLen ? String(minLen) : `${minLen}-${maxLen}`
274
+ const usdAmount = value / 1e6
275
+ const discountedUsd = usdAmount * (1 - NS_DISCOUNT_PERCENT / 100)
276
+
277
+ const directSui = usdAmount / suiPriceUsd
278
+ const nsNeeded = (discountedUsd / suiPriceUsd) * nsPriceResult.nsPerSui
279
+ const suiForSwap = nsNeeded * nsPriceResult.suiPerNs * (1 + SWAP_SLIPPAGE_BPS / 10000)
280
+
281
+ tiers[keyStr] = {
282
+ usd: usdAmount,
283
+ discountedUsd,
284
+ directSuiMist: Math.ceil(directSui * 1e9),
285
+ discountedSuiMist: Math.ceil(suiForSwap * 1e9),
286
+ savingsPercent: Math.round((1 - suiForSwap / directSui) * 10000) / 100,
287
+ }
288
+ }
289
+
290
+ return pricing
291
+ }
@@ -0,0 +1,63 @@
1
+ import type { SuiJsonRpcClient as SuiClient } from '@mysten/sui/jsonRpc'
2
+ import { parseStructTag } from '@mysten/sui/utils'
3
+ import { mainPackage } from '@mysten/suins'
4
+
5
+ const PYTH_MAINNET_STATE = mainPackage.mainnet.pyth.pythStateId
6
+ const PYTH_TESTNET_STATE = mainPackage.testnet.pyth.pythStateId
7
+
8
+ export async function getPythPriceInfoObjectId(
9
+ client: SuiClient,
10
+ network: 'mainnet' | 'testnet',
11
+ feedId: string,
12
+ ): Promise<string> {
13
+ const pythStateId = network === 'mainnet' ? PYTH_MAINNET_STATE : PYTH_TESTNET_STATE
14
+ const normalizedFeed = feedId.startsWith('0x') ? feedId : `0x${feedId}`
15
+
16
+ const priceTableResult = await client.getDynamicFieldObject({
17
+ parentId: pythStateId,
18
+ name: {
19
+ type: 'vector<u8>',
20
+ value: 'price_info',
21
+ },
22
+ })
23
+
24
+ if (!priceTableResult.data?.type) {
25
+ throw new Error('Pyth price table not found')
26
+ }
27
+
28
+ const priceIdentifier = parseStructTag(priceTableResult.data.type).typeParams[0]
29
+ if (
30
+ typeof priceIdentifier !== 'object' ||
31
+ priceIdentifier === null ||
32
+ priceIdentifier.name !== 'PriceIdentifier' ||
33
+ !('address' in priceIdentifier)
34
+ ) {
35
+ throw new Error('Pyth price table field type not found')
36
+ }
37
+
38
+ const fieldType = (priceIdentifier as { address: string }).address
39
+ const hex = normalizedFeed.startsWith('0x') ? normalizedFeed.slice(2) : normalizedFeed
40
+ const feedBytes: number[] = []
41
+ for (let i = 0; i < hex.length; i += 2) {
42
+ feedBytes.push(parseInt(hex.slice(i, i + 2), 16))
43
+ }
44
+
45
+ const feedResult = await client.getDynamicFieldObject({
46
+ parentId: priceTableResult.data.objectId,
47
+ name: {
48
+ type: `${fieldType}::price_identifier::PriceIdentifier`,
49
+ value: { bytes: feedBytes },
50
+ },
51
+ })
52
+
53
+ if (!feedResult.data?.content || feedResult.data.content.dataType !== 'moveObject') {
54
+ throw new Error(`Pyth price feed object for feed ${feedId} not found`)
55
+ }
56
+
57
+ const fields = feedResult.data.content.fields as { value?: string }
58
+ if (typeof fields.value !== 'string') {
59
+ throw new Error('Pyth price feed object has no value field')
60
+ }
61
+
62
+ return fields.value
63
+ }
@@ -0,0 +1,204 @@
1
+ import { generateRegistrationPage } from '../handlers/register2'
2
+ import type { Env, GatewayError } from '../types'
3
+
4
+ const CORS_HEADERS = {
5
+ 'Access-Control-Allow-Origin': '*',
6
+ 'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, DELETE, OPTIONS',
7
+ 'Access-Control-Allow-Headers': '*',
8
+ }
9
+
10
+ export function jsonResponse<T>(data: T, status = 200, headers: Record<string, string> = {}) {
11
+ return new Response(JSON.stringify(data), {
12
+ status,
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ ...CORS_HEADERS,
16
+ ...headers,
17
+ },
18
+ })
19
+ }
20
+
21
+ export function errorResponse(error: string, code: string, status = 400, details?: unknown) {
22
+ const body: GatewayError = { error, code, details }
23
+ return jsonResponse(body, status)
24
+ }
25
+
26
+ export function htmlResponse(html: string, status = 200, headers: Record<string, string> = {}) {
27
+ return new Response(html, {
28
+ status,
29
+ headers: {
30
+ 'Content-Type': 'text/html; charset=utf-8',
31
+ ...CORS_HEADERS,
32
+ ...headers,
33
+ },
34
+ })
35
+ }
36
+
37
+ export function proxyResponse(response: Response) {
38
+ // Clone response and add CORS headers
39
+ const newHeaders = new Headers(response.headers)
40
+ for (const [key, value] of Object.entries(CORS_HEADERS)) {
41
+ newHeaders.set(key, value)
42
+ }
43
+ return new Response(response.body, {
44
+ status: response.status,
45
+ statusText: response.statusText,
46
+ headers: newHeaders,
47
+ })
48
+ }
49
+
50
+ export function notFoundPage(
51
+ name: string,
52
+ env?: Env,
53
+ available?: boolean,
54
+ session?: { address: string | null; walletName: string | null; verified: boolean },
55
+ ) {
56
+ // Only show registration page if env is provided AND we confirmed the name is available
57
+ // If available is false/undefined, there was a resolution error - don't show registration
58
+ if (env && available === true) {
59
+ return htmlResponse(generateRegistrationPage(name, env, session), 200)
60
+ }
61
+
62
+ const escapeHtml = (value: string) =>
63
+ value.replace(/[&<>"']/g, (char) => {
64
+ switch (char) {
65
+ case '&':
66
+ return '&amp;'
67
+ case '<':
68
+ return '&lt;'
69
+ case '>':
70
+ return '&gt;'
71
+ case '"':
72
+ return '&quot;'
73
+ case "'":
74
+ return '&#39;'
75
+ default:
76
+ return char
77
+ }
78
+ })
79
+
80
+ // Fallback for cases without env
81
+ return htmlResponse(
82
+ `<!DOCTYPE html>
83
+ <html lang="en">
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>${escapeHtml(name)} - Not Found | sui.ski</title>
88
+ <style>
89
+ * { box-sizing: border-box; margin: 0; padding: 0; }
90
+ body {
91
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
92
+ background: #000;
93
+ background-attachment: fixed;
94
+ color: #e4e4e7;
95
+ min-height: 100vh;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ padding: 20px;
100
+ }
101
+ body::before {
102
+ content: '';
103
+ position: fixed;
104
+ top: 0;
105
+ left: 0;
106
+ right: 0;
107
+ bottom: 0;
108
+ background:
109
+ radial-gradient(ellipse at 20% 20%, rgba(96, 165, 250, 0.08) 0%, transparent 50%),
110
+ radial-gradient(ellipse at 80% 80%, rgba(139, 92, 246, 0.06) 0%, transparent 50%);
111
+ pointer-events: none;
112
+ }
113
+ .container {
114
+ max-width: 480px;
115
+ text-align: center;
116
+ position: relative;
117
+ }
118
+ .icon {
119
+ width: 80px;
120
+ height: 80px;
121
+ background: linear-gradient(135deg, rgba(96, 165, 250, 0.15), rgba(139, 92, 246, 0.12));
122
+ border-radius: 50%;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ margin: 0 auto 24px;
127
+ }
128
+ .icon svg {
129
+ width: 40px;
130
+ height: 40px;
131
+ color: #60a5fa;
132
+ }
133
+ h1 {
134
+ font-size: 1.75rem;
135
+ font-weight: 700;
136
+ background: linear-gradient(135deg, #60a5fa, #a78bfa);
137
+ -webkit-background-clip: text;
138
+ -webkit-text-fill-color: transparent;
139
+ background-clip: text;
140
+ margin-bottom: 16px;
141
+ }
142
+ p {
143
+ color: #71717a;
144
+ line-height: 1.6;
145
+ margin-bottom: 24px;
146
+ }
147
+ code {
148
+ background: rgba(96, 165, 250, 0.12);
149
+ padding: 3px 8px;
150
+ border-radius: 6px;
151
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
152
+ font-size: 0.9em;
153
+ color: #60a5fa;
154
+ }
155
+ .btn {
156
+ display: inline-block;
157
+ padding: 12px 24px;
158
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
159
+ color: white;
160
+ text-decoration: none;
161
+ border-radius: 10px;
162
+ font-weight: 600;
163
+ transition: all 0.2s;
164
+ box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
165
+ }
166
+ .btn:hover {
167
+ transform: translateY(-2px);
168
+ box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
169
+ }
170
+ .footer {
171
+ margin-top: 40px;
172
+ color: #71717a;
173
+ font-size: 0.875rem;
174
+ }
175
+ .footer a {
176
+ color: #60a5fa;
177
+ text-decoration: none;
178
+ }
179
+ .footer a:hover {
180
+ text-decoration: underline;
181
+ }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <div class="container">
186
+ <div class="icon">
187
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
188
+ <circle cx="11" cy="11" r="8"></circle>
189
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
190
+ <line x1="8" y1="11" x2="14" y2="11"></line>
191
+ </svg>
192
+ </div>
193
+ <h1>Name Not Found</h1>
194
+ <p>The name <code>${escapeHtml(name)}</code> could not be resolved on the Sui network.</p>
195
+ <a href="https://suins.io" class="btn">Register on SuiNS</a>
196
+ <div class="footer">
197
+ <p><a href="https://sui.ski">← Back to sui.ski</a></p>
198
+ </div>
199
+ </div>
200
+ </body>
201
+ </html>`,
202
+ 404,
203
+ )
204
+ }
@@ -0,0 +1,25 @@
1
+ import type { Env } from '../types'
2
+
3
+ const DEFAULT_RPC_URLS: Record<Env['SUI_NETWORK'], string> = {
4
+ mainnet: 'https://fullnode.mainnet.sui.io:443',
5
+ testnet: 'https://fullnode.testnet.sui.io:443',
6
+ devnet: 'https://fullnode.devnet.sui.io:443',
7
+ }
8
+
9
+ export function getDefaultRpcUrl(network: Env['SUI_NETWORK']): string {
10
+ return DEFAULT_RPC_URLS[network] || DEFAULT_RPC_URLS.mainnet
11
+ }
12
+
13
+ export function ensureRpcEnv(env: Env): Env {
14
+ const configured = (env.SUI_RPC_URL ?? '').trim()
15
+ if (configured) {
16
+ if (configured === env.SUI_RPC_URL) {
17
+ return env
18
+ }
19
+ return { ...env, SUI_RPC_URL: configured }
20
+ }
21
+
22
+ const fallback = getDefaultRpcUrl(env.SUI_NETWORK)
23
+ console.warn(`[sui.ski] SUI_RPC_URL not configured. Falling back to public RPC ${fallback}`)
24
+ return { ...env, SUI_RPC_URL: fallback }
25
+ }