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.
- package/AGENTS.md +311 -0
- package/CLAUDE.md +292 -0
- package/CODEBASE_GUIDE.md +217 -0
- package/README.md +77 -0
- package/biome.json +28 -0
- package/package.json +73 -0
- package/scripts/deploy-messaging-mainnet.sh +184 -0
- package/scripts/extract-suins-object.ts +180 -0
- package/scripts/full-deploy.sh +26 -0
- package/scripts/obsidian.ts +243 -0
- package/scripts/set-suins-contenthash.ts +130 -0
- package/scripts/setup-ika-dwallet.ts +338 -0
- package/scripts/transfer-upgrade-cap-from-nft.ts +86 -0
- package/src/durable-objects/wallet-session.ts +333 -0
- package/src/handlers/app.ts +1430 -0
- package/src/handlers/authenticated-events.ts +267 -0
- package/src/handlers/dashboard.ts +1659 -0
- package/src/handlers/landing.ts +6751 -0
- package/src/handlers/mcp.ts +556 -0
- package/src/handlers/messaging-sdk.ts +220 -0
- package/src/handlers/profile.css.ts +9332 -0
- package/src/handlers/profile.ts +12640 -0
- package/src/handlers/register2.ts +2811 -0
- package/src/handlers/ski-sign.ts +1901 -0
- package/src/handlers/ski.ts +314 -0
- package/src/handlers/thunder.ts +940 -0
- package/src/handlers/vault.ts +284 -0
- package/src/handlers/wallet-api.ts +169 -0
- package/src/handlers/x402-register.ts +601 -0
- package/src/index.test.ts +55 -0
- package/src/index.ts +512 -0
- package/src/resolvers/content.ts +231 -0
- package/src/resolvers/rpc.ts +222 -0
- package/src/resolvers/suins.ts +266 -0
- package/src/sdk/messaging.ts +279 -0
- package/src/types.ts +230 -0
- package/src/utils/agent-keypair.ts +40 -0
- package/src/utils/authenticated-events.ts +280 -0
- package/src/utils/cache.ts +82 -0
- package/src/utils/media-pack.ts +27 -0
- package/src/utils/mmr.ts +181 -0
- package/src/utils/ns-price.ts +529 -0
- package/src/utils/og-image.ts +141 -0
- package/src/utils/onchain-activity.ts +211 -0
- package/src/utils/onchain-listing.ts +39 -0
- package/src/utils/premium.ts +29 -0
- package/src/utils/pricing.ts +291 -0
- package/src/utils/pyth-price-info.ts +63 -0
- package/src/utils/response.ts +204 -0
- package/src/utils/rpc.ts +25 -0
- package/src/utils/shared-wallet-js.ts +166 -0
- package/src/utils/social.ts +152 -0
- package/src/utils/status.ts +39 -0
- package/src/utils/subdomain.ts +116 -0
- package/src/utils/surflux-grpc.ts +241 -0
- package/src/utils/swap-transactions.ts +1222 -0
- package/src/utils/thunder-css.ts +1341 -0
- package/src/utils/thunder-js.ts +5046 -0
- package/src/utils/transactions.ts +65 -0
- package/src/utils/vault.ts +18 -0
- package/src/utils/wallet-kit-js.ts +2312 -0
- package/src/utils/wallet-session-js.ts +192 -0
- package/src/utils/wallet-tx-js.ts +2287 -0
- package/src/utils/wallet-ui-js.ts +3057 -0
- package/src/utils/x402-middleware.ts +428 -0
- package/src/utils/x402-sui.ts +171 -0
- package/src/utils/zksend-js.ts +166 -0
- package/tsconfig.json +22 -0
- package/workers/x402-multichain/src/index.ts +237 -0
- package/workers/x402-multichain/src/types.ts +80 -0
- package/workers/x402-multichain/tsconfig.json +20 -0
- package/workers/x402-multichain/wrangler.toml +11 -0
- 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 '&'
|
|
67
|
+
case '<':
|
|
68
|
+
return '<'
|
|
69
|
+
case '>':
|
|
70
|
+
return '>'
|
|
71
|
+
case '"':
|
|
72
|
+
return '"'
|
|
73
|
+
case "'":
|
|
74
|
+
return '''
|
|
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
|
+
}
|
package/src/utils/rpc.ts
ADDED
|
@@ -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
|
+
}
|