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,2811 @@
|
|
|
1
|
+
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
|
|
2
|
+
import type { Transaction } from '@mysten/sui/transactions'
|
|
3
|
+
import { SuinsClient, SuinsTransaction } from '@mysten/suins'
|
|
4
|
+
import type { Env } from '../types'
|
|
5
|
+
import { getDeepBookSuiPools } from '../utils/ns-price'
|
|
6
|
+
import { generateLogoSvg } from '../utils/og-image'
|
|
7
|
+
import { calculateRegistrationPrice } from '../utils/pricing'
|
|
8
|
+
import { jsonResponse } from '../utils/response'
|
|
9
|
+
import { getDefaultRpcUrl } from '../utils/rpc'
|
|
10
|
+
import { generateSharedWalletMountJs } from '../utils/shared-wallet-js'
|
|
11
|
+
import {
|
|
12
|
+
buildMultiCoinRegisterTx,
|
|
13
|
+
buildSuiRegisterTx,
|
|
14
|
+
buildSwapAndRegisterTx,
|
|
15
|
+
prependGasSwapIfNeeded,
|
|
16
|
+
} from '../utils/swap-transactions'
|
|
17
|
+
import { relaySignedTransaction } from '../utils/transactions'
|
|
18
|
+
import { generateExtensionNoiseFilter, generateWalletKitJs } from '../utils/wallet-kit-js'
|
|
19
|
+
import { generateWalletSessionJs } from '../utils/wallet-session-js'
|
|
20
|
+
import { generateWalletTxJs } from '../utils/wallet-tx-js'
|
|
21
|
+
import { generateWalletUiCss, generateWalletUiJs } from '../utils/wallet-ui-js'
|
|
22
|
+
|
|
23
|
+
const CORS_HEADERS = {
|
|
24
|
+
'Access-Control-Allow-Origin': '*',
|
|
25
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
26
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SUI_COIN_TYPE = '0x2::sui::SUI'
|
|
30
|
+
const NS_COIN_TYPE = '0x5145494a5f5100e645e4b0aa950fa6b68f614e8c59e17bc5ded3495123a79178::ns::NS'
|
|
31
|
+
const USDC_COIN_TYPE =
|
|
32
|
+
'0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC'
|
|
33
|
+
const REGISTER_GAS_BUFFER_MIST = 80_000_000n
|
|
34
|
+
const REGISTER_PAYMENT_COIN_PAGE_LIMIT = 50
|
|
35
|
+
const REGISTER_PAYMENT_COIN_PAGE_MAX = 6
|
|
36
|
+
const STABLE_SYMBOL_PRIORITY = ['USDC', 'USDT', 'AUSD', 'USDY', 'FDUSD', 'DAI']
|
|
37
|
+
|
|
38
|
+
interface RegisterSession {
|
|
39
|
+
address: string | null
|
|
40
|
+
walletName: string | null
|
|
41
|
+
verified: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface RegistrationPageOptions {
|
|
45
|
+
flow?: 'register' | 'register2'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function generateRegistrationPage(
|
|
49
|
+
name: string,
|
|
50
|
+
env: Env,
|
|
51
|
+
session?: RegisterSession,
|
|
52
|
+
options: RegistrationPageOptions = {},
|
|
53
|
+
): string {
|
|
54
|
+
const cleanName = name.replace(/\.sui$/i, '').toLowerCase()
|
|
55
|
+
const network = env.SUI_NETWORK || 'mainnet'
|
|
56
|
+
const isRegisterable = cleanName.length >= 3
|
|
57
|
+
const registerFlow = options.flow === 'register2' ? 'register2' : 'register'
|
|
58
|
+
const registerBucket = registerFlow === 'register2' ? 'register-v2' : 'register-v1'
|
|
59
|
+
const serializeJson = (value: unknown) =>
|
|
60
|
+
JSON.stringify(value).replace(/</g, '\\u003c').replace(/-->/g, '--\\u003e')
|
|
61
|
+
const suiIconSvg =
|
|
62
|
+
'<svg class="price-sui-icon" viewBox="0 0 300 384" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M240.057 159.914C255.698 179.553 265.052 204.39 265.052 231.407C265.052 258.424 255.414 284.019 239.362 303.768L237.971 305.475L237.608 303.31C237.292 301.477 236.929 299.613 236.502 297.749C228.46 262.421 202.265 232.134 159.148 207.597C130.029 191.071 113.361 171.195 108.985 148.586C106.157 133.972 108.258 119.294 112.318 106.717C116.379 94.1569 122.414 83.6187 127.549 77.2831L144.328 56.7754C147.267 53.1731 152.781 53.1731 155.719 56.7754L240.073 159.914H240.057ZM266.584 139.422L154.155 1.96703C152.007 -0.655678 147.993 -0.655678 145.845 1.96703L33.4316 139.422L33.0683 139.881C12.3868 165.555 0 198.181 0 233.698C0 316.408 67.1635 383.461 150 383.461C232.837 383.461 300 316.408 300 233.698C300 198.181 287.613 165.555 266.932 139.896L266.568 139.438L266.584 139.422ZM60.3381 159.472L70.3866 147.164L70.6868 149.439C70.9237 151.24 71.2239 153.041 71.5715 154.858C78.0809 189.001 101.322 217.456 140.173 239.496C173.952 258.724 193.622 280.828 199.278 305.064C201.648 315.176 202.059 325.129 201.032 333.835L200.969 334.372L200.479 334.609C185.233 342.05 168.09 346.237 149.984 346.237C86.4546 346.237 34.9484 294.826 34.9484 231.391C34.9484 204.153 44.4439 179.142 60.3065 159.44L60.3381 159.472Z" fill="#4DA2FF"/></svg>'
|
|
63
|
+
const registrationCardHtml = isRegisterable
|
|
64
|
+
? `<div class="nft-card">
|
|
65
|
+
<div class="nft-top">
|
|
66
|
+
<div class="nft-name-block">
|
|
67
|
+
<div class="nft-name-line"><button type="button" class="primary-star" id="primary-star" aria-label="Set as primary" aria-pressed="false" title="Set as primary name">\u2606</button><span class="nft-name">${escapeHtml(cleanName)}<span class="nft-name-tld">.sui</span></span></div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="nft-body">
|
|
71
|
+
<div class="nft-qr-col">
|
|
72
|
+
<div class="nft-price-stack" id="price-value">
|
|
73
|
+
<div class="nft-price-main payment-choice" data-payment-mode="sui" role="button" tabindex="0" aria-label="Pay with SUI"><span class="price-amount">--</span></div>
|
|
74
|
+
<div class="price-rest">
|
|
75
|
+
<span class="price-rest-top payment-choice" data-payment-mode="sui" role="button" tabindex="0" aria-label="Pay with SUI">${suiIconSvg}</span>
|
|
76
|
+
<span class="price-usd payment-choice" data-payment-mode="coin" role="button" tabindex="0" aria-label="Pay with USD coin estimate">\u2248 $--</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<span class="nft-chip"><span class="nft-dot"></span>Available</span>
|
|
80
|
+
<canvas class="nft-qr" id="nft-qr" width="140" height="140" title="${escapeHtml(cleanName)}.sui"></canvas>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="nft-wallet-icons" id="nft-wallet-icons"></div>
|
|
83
|
+
<div class="nft-logo-col">
|
|
84
|
+
<div class="nft-logo-badge"><svg viewBox="0 0 512 560" fill="none" aria-hidden="true"><defs><linearGradient id="nlg" x1="0" y1="0" x2="0.5" y2="1"><stop offset="0%" stop-color="#ffffff"><animate attributeName="stop-color" values="#ffffff;#b8ffda;#49da91;#ffffff" dur="6s" repeatCount="indefinite"/></stop><stop offset="100%" stop-color="#b8ffda"><animate attributeName="stop-color" values="#b8ffda;#49da91;#ffffff;#b8ffda" dur="6s" repeatCount="indefinite"/></stop></linearGradient></defs><path d="M256 96L416 352H336L280 256L256 296L232 256L176 352H96Z" fill="url(#nlg)"/><path d="M128 384Q208 348 288 384Q368 420 432 384" stroke="url(#nlg)" stroke-width="24" fill="none" stroke-linecap="round"/><text x="256" y="480" font-family="Inter,system-ui,sans-serif" font-size="90" font-weight="800" fill="url(#nlg)" text-anchor="middle">sui.ski</text></svg></div>
|
|
85
|
+
<div class="nft-discount" id="price-savings"></div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="reg-form">
|
|
90
|
+
<div class="reg-action-row">
|
|
91
|
+
<button class="download-qr-btn" id="download-qr-btn" title="Download QR card"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></button>
|
|
92
|
+
<button class="button" id="register-btn">Connect Wallet</button>
|
|
93
|
+
<div class="year-stepper-h" role="group" aria-label="Registration duration" tabindex="0">
|
|
94
|
+
<button type="button" class="year-btn-h" id="years-decrease" aria-label="Decrease">\u2212</button>
|
|
95
|
+
<div class="year-display-h"><span id="years-value">1</span><span class="year-unit">yr</span></div>
|
|
96
|
+
<button type="button" class="year-btn-h" id="years-increase" aria-label="Increase">+</button>
|
|
97
|
+
<input id="years" type="hidden" value="1">
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="status" id="register-status"></div>
|
|
101
|
+
</div>`
|
|
102
|
+
: `<div class="nft-card" style="aspect-ratio:1;justify-content:center;align-items:center;text-align:center;"><span style="font-size:1.6rem;font-weight:800;color:#fff;">${escapeHtml(cleanName)}<span style="color:var(--ski-green);">.sui</span></span><span style="font-size:0.85rem;color:var(--muted);margin-top:8px;">Minimum length is 3 characters.</span></div>`
|
|
103
|
+
const discoveryColumnHtml = ''
|
|
104
|
+
|
|
105
|
+
return `<!DOCTYPE html>
|
|
106
|
+
<html lang="en">
|
|
107
|
+
<head>
|
|
108
|
+
${generateExtensionNoiseFilter()}
|
|
109
|
+
<meta charset="UTF-8">
|
|
110
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
111
|
+
<meta name="sui-ski-register-flow" content="${registerFlow}">
|
|
112
|
+
<meta name="sui-ski-register-bucket" content="${registerBucket}">
|
|
113
|
+
<title>${escapeHtml(cleanName)}.sui available | sui.ski</title>
|
|
114
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
115
|
+
<style>
|
|
116
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
|
117
|
+
:root {
|
|
118
|
+
--bg-0: #010201;
|
|
119
|
+
--bg-1: #030806;
|
|
120
|
+
--card: rgba(3, 10, 8, 0.9);
|
|
121
|
+
--line: rgba(130, 255, 190, 0.24);
|
|
122
|
+
--text: #ebfff4;
|
|
123
|
+
--muted: #8fb9a3;
|
|
124
|
+
--accent: #49da91;
|
|
125
|
+
--listing-purple: #a855f7;
|
|
126
|
+
--listing-purple-light: #d8b4fe;
|
|
127
|
+
--ski-green: #49da91;
|
|
128
|
+
--ski-green-dark: #27bd74;
|
|
129
|
+
--ski-green-light: #b8ffda;
|
|
130
|
+
--ski-green-soft: #ecfff4;
|
|
131
|
+
--ski-green-rgb: 73, 218, 145;
|
|
132
|
+
--snow-rgb: 228, 255, 242;
|
|
133
|
+
--ok: var(--ski-green);
|
|
134
|
+
--warn: #fbbf24;
|
|
135
|
+
--err: #f87171;
|
|
136
|
+
}
|
|
137
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
138
|
+
html {
|
|
139
|
+
scrollbar-color: rgba(var(--ski-green-rgb), 0.82) rgba(7, 20, 16, 0.9);
|
|
140
|
+
}
|
|
141
|
+
::-webkit-scrollbar {
|
|
142
|
+
width: 10px;
|
|
143
|
+
height: 10px;
|
|
144
|
+
}
|
|
145
|
+
::-webkit-scrollbar-track {
|
|
146
|
+
background: rgba(7, 20, 16, 0.88);
|
|
147
|
+
}
|
|
148
|
+
::-webkit-scrollbar-thumb {
|
|
149
|
+
background: linear-gradient(180deg, rgba(var(--ski-green-rgb), 0.95), rgba(var(--ski-green-rgb), 0.8));
|
|
150
|
+
border-radius: 999px;
|
|
151
|
+
border: 2px solid rgba(7, 20, 16, 0.88);
|
|
152
|
+
}
|
|
153
|
+
::-webkit-scrollbar-thumb:hover {
|
|
154
|
+
background: linear-gradient(180deg, rgba(var(--ski-green-rgb), 1), rgba(var(--ski-green-rgb), 0.88));
|
|
155
|
+
}
|
|
156
|
+
body {
|
|
157
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
158
|
+
min-height: 100vh;
|
|
159
|
+
position: relative;
|
|
160
|
+
background:
|
|
161
|
+
radial-gradient(45% 35% at 50% 32%, rgba(255, 255, 255, 0.04) 0%, transparent 70%),
|
|
162
|
+
radial-gradient(80% 60% at 50% -8%, rgba(106, 255, 185, 0.16) 0%, transparent 52%),
|
|
163
|
+
radial-gradient(75% 62% at 100% 100%, rgba(47, 186, 116, 0.16) 0%, transparent 62%),
|
|
164
|
+
linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
|
|
165
|
+
color: var(--text);
|
|
166
|
+
padding: 20px;
|
|
167
|
+
padding-bottom: 52px;
|
|
168
|
+
overflow-x: hidden;
|
|
169
|
+
}
|
|
170
|
+
body::before,
|
|
171
|
+
body::after {
|
|
172
|
+
content: '';
|
|
173
|
+
position: fixed;
|
|
174
|
+
inset: -20vh 0 0 0;
|
|
175
|
+
pointer-events: none;
|
|
176
|
+
z-index: 0;
|
|
177
|
+
background-repeat: repeat;
|
|
178
|
+
}
|
|
179
|
+
body::before {
|
|
180
|
+
opacity: calc(0.32 + var(--snow-boost, 0));
|
|
181
|
+
background-image:
|
|
182
|
+
radial-gradient(circle, rgba(var(--snow-rgb), 0.86) 0 1px, transparent 1.8px),
|
|
183
|
+
radial-gradient(circle, rgba(var(--snow-rgb), 0.6) 0 1.2px, transparent 2px);
|
|
184
|
+
background-size: 190px 190px, 260px 260px;
|
|
185
|
+
background-position: 0 0, 88px 120px;
|
|
186
|
+
animation: snow-fall-a 23s linear infinite;
|
|
187
|
+
}
|
|
188
|
+
body::after {
|
|
189
|
+
opacity: calc(0.2 + var(--snow-boost, 0) * 0.7);
|
|
190
|
+
background-image:
|
|
191
|
+
radial-gradient(circle, rgba(var(--snow-rgb), 0.64) 0 0.9px, transparent 1.6px),
|
|
192
|
+
radial-gradient(circle, rgba(var(--snow-rgb), 0.5) 0 0.8px, transparent 1.4px);
|
|
193
|
+
background-size: 120px 120px, 170px 170px;
|
|
194
|
+
background-position: 20px 40px, 72px 96px;
|
|
195
|
+
animation: snow-fall-b 31s linear infinite;
|
|
196
|
+
}
|
|
197
|
+
@keyframes snow-fall-a {
|
|
198
|
+
from { transform: translateY(-12%); }
|
|
199
|
+
to { transform: translateY(12%); }
|
|
200
|
+
}
|
|
201
|
+
@keyframes snow-fall-b {
|
|
202
|
+
from { transform: translateY(-8%); }
|
|
203
|
+
to { transform: translateY(10%); }
|
|
204
|
+
}
|
|
205
|
+
.container {
|
|
206
|
+
max-width: 600px;
|
|
207
|
+
margin: 0 auto;
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
gap: 12px;
|
|
211
|
+
position: relative;
|
|
212
|
+
z-index: 1;
|
|
213
|
+
}
|
|
214
|
+
.layout-grid {
|
|
215
|
+
display: grid;
|
|
216
|
+
grid-template-columns: minmax(0, 1fr);
|
|
217
|
+
gap: 12px;
|
|
218
|
+
align-items: start;
|
|
219
|
+
}
|
|
220
|
+
.nft-brand-link {
|
|
221
|
+
display: inline-flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
gap: 4px;
|
|
224
|
+
text-decoration: none;
|
|
225
|
+
flex-shrink: 0;
|
|
226
|
+
}
|
|
227
|
+
.nft-brand-name {
|
|
228
|
+
font-size: 0.84rem;
|
|
229
|
+
font-weight: 800;
|
|
230
|
+
color: var(--text);
|
|
231
|
+
}
|
|
232
|
+
.nft-brand-tagline {
|
|
233
|
+
font-size: 0.62rem;
|
|
234
|
+
font-weight: 500;
|
|
235
|
+
color: var(--muted);
|
|
236
|
+
line-height: 1.3;
|
|
237
|
+
opacity: 0.75;
|
|
238
|
+
white-space: nowrap;
|
|
239
|
+
}
|
|
240
|
+
@keyframes portal-ring {
|
|
241
|
+
0% { opacity: 0.5; transform: scale(0.97); }
|
|
242
|
+
50% { opacity: 1; transform: scale(1); }
|
|
243
|
+
100% { opacity: 0.5; transform: scale(0.97); }
|
|
244
|
+
}
|
|
245
|
+
.nft-card {
|
|
246
|
+
color-scheme: only light;
|
|
247
|
+
width: 100%;
|
|
248
|
+
max-width: 380px;
|
|
249
|
+
aspect-ratio: 1;
|
|
250
|
+
margin: 0 auto;
|
|
251
|
+
padding: 22px;
|
|
252
|
+
border-radius: 20px;
|
|
253
|
+
background: linear-gradient(155deg, #0a1a13 0%, #040d09 100%);
|
|
254
|
+
border: 1px solid rgba(var(--ski-green-rgb), 0.18);
|
|
255
|
+
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
|
|
256
|
+
display: flex;
|
|
257
|
+
flex-direction: column;
|
|
258
|
+
justify-content: space-between;
|
|
259
|
+
gap: 8px;
|
|
260
|
+
position: relative;
|
|
261
|
+
}
|
|
262
|
+
.nft-card::before {
|
|
263
|
+
content: '';
|
|
264
|
+
position: absolute;
|
|
265
|
+
inset: -2px;
|
|
266
|
+
border-radius: 21px;
|
|
267
|
+
background: conic-gradient(from 0deg, transparent 0%, rgba(var(--portal-r, 73), var(--portal-g, 218), var(--portal-b, 145), var(--portal-a, 0.35)) 25%, transparent 50%, rgba(var(--portal-r, 73), var(--portal-g, 218), var(--portal-b, 145), calc(var(--portal-a, 0.35) * 0.6)) 75%, transparent 100%);
|
|
268
|
+
pointer-events: none;
|
|
269
|
+
z-index: -1;
|
|
270
|
+
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
271
|
+
mask-composite: exclude;
|
|
272
|
+
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
273
|
+
-webkit-mask-composite: xor;
|
|
274
|
+
padding: 1px;
|
|
275
|
+
animation: portal-ring 4s ease-in-out infinite;
|
|
276
|
+
}
|
|
277
|
+
.nft-top {
|
|
278
|
+
display: flex;
|
|
279
|
+
justify-content: space-between;
|
|
280
|
+
align-items: flex-start;
|
|
281
|
+
}
|
|
282
|
+
.nft-wallet-icons {
|
|
283
|
+
display: none;
|
|
284
|
+
flex-direction: column;
|
|
285
|
+
align-items: flex-start;
|
|
286
|
+
gap: 5px;
|
|
287
|
+
align-self: flex-end;
|
|
288
|
+
margin-bottom: 2px;
|
|
289
|
+
flex-shrink: 0;
|
|
290
|
+
}
|
|
291
|
+
.nft-wallet-icons.show { display: flex; }
|
|
292
|
+
.nft-wallet-icon-row {
|
|
293
|
+
display: flex;
|
|
294
|
+
align-items: center;
|
|
295
|
+
gap: 5px;
|
|
296
|
+
}
|
|
297
|
+
.nft-wallet-icon-row img,
|
|
298
|
+
.nft-wallet-icon-row > svg {
|
|
299
|
+
width: 20px;
|
|
300
|
+
height: 20px;
|
|
301
|
+
border-radius: 4px;
|
|
302
|
+
display: block;
|
|
303
|
+
flex-shrink: 0;
|
|
304
|
+
filter: drop-shadow(0 1px 3px rgba(0,0,0,0.4));
|
|
305
|
+
}
|
|
306
|
+
.nft-wallet-icon-label {
|
|
307
|
+
font-size: 0.6rem;
|
|
308
|
+
font-weight: 700;
|
|
309
|
+
color: rgba(255, 255, 255, 0.6);
|
|
310
|
+
white-space: nowrap;
|
|
311
|
+
max-width: 60px;
|
|
312
|
+
overflow: hidden;
|
|
313
|
+
text-overflow: ellipsis;
|
|
314
|
+
}
|
|
315
|
+
.nft-name-block { flex: 1; min-width: 0; }
|
|
316
|
+
.nft-name-line {
|
|
317
|
+
display: flex;
|
|
318
|
+
align-items: baseline;
|
|
319
|
+
gap: 6px;
|
|
320
|
+
flex-wrap: wrap;
|
|
321
|
+
}
|
|
322
|
+
@keyframes tld-pulse {
|
|
323
|
+
0%, 100% { text-shadow: 0 0 8px rgba(73, 218, 145, 0.5), 0 0 20px rgba(73, 218, 145, 0.2); }
|
|
324
|
+
50% { text-shadow: 0 0 12px rgba(73, 218, 145, 0.7), 0 0 30px rgba(73, 218, 145, 0.3), 0 0 50px rgba(73, 218, 145, 0.1); }
|
|
325
|
+
}
|
|
326
|
+
.nft-name {
|
|
327
|
+
font-size: clamp(2.2rem, 10vw, 3.2rem);
|
|
328
|
+
font-weight: 900;
|
|
329
|
+
letter-spacing: -0.04em;
|
|
330
|
+
line-height: 1.05;
|
|
331
|
+
color: #f0f4f8;
|
|
332
|
+
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 0 12px rgba(200, 220, 240, 0.15);
|
|
333
|
+
transition: color 0.4s ease, text-shadow 0.4s ease;
|
|
334
|
+
}
|
|
335
|
+
.nft-name-tld {
|
|
336
|
+
color: var(--ski-green);
|
|
337
|
+
text-shadow: 0 0 8px rgba(73, 218, 145, 0.5), 0 0 20px rgba(73, 218, 145, 0.2);
|
|
338
|
+
animation: tld-pulse 3s ease-in-out infinite;
|
|
339
|
+
transition: color 0.4s ease;
|
|
340
|
+
}
|
|
341
|
+
.primary-star {
|
|
342
|
+
width: 28px;
|
|
343
|
+
height: 28px;
|
|
344
|
+
border-radius: 999px;
|
|
345
|
+
border: 1px solid rgba(180, 195, 210, 0.4);
|
|
346
|
+
background: rgba(180, 195, 210, 0.1);
|
|
347
|
+
color: rgba(200, 210, 225, 0.7);
|
|
348
|
+
font-size: 0.95rem;
|
|
349
|
+
line-height: 1;
|
|
350
|
+
display: inline-flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
cursor: pointer;
|
|
354
|
+
flex-shrink: 0;
|
|
355
|
+
transition: all 0.15s ease;
|
|
356
|
+
margin-top: 4px;
|
|
357
|
+
}
|
|
358
|
+
.primary-star:hover { background: rgba(180, 195, 210, 0.2); border-color: rgba(200, 210, 225, 0.6); color: rgba(220, 225, 235, 0.85); }
|
|
359
|
+
.primary-star.active {
|
|
360
|
+
color: #c8d0e0;
|
|
361
|
+
background: rgba(120, 130, 155, 0.18);
|
|
362
|
+
border-color: rgba(120, 130, 155, 0.7);
|
|
363
|
+
box-shadow: 0 0 12px rgba(120, 130, 155, 0.2);
|
|
364
|
+
}
|
|
365
|
+
.nft-qr-col {
|
|
366
|
+
display: flex;
|
|
367
|
+
flex-direction: column;
|
|
368
|
+
align-items: stretch;
|
|
369
|
+
gap: 6px;
|
|
370
|
+
width: 120px;
|
|
371
|
+
flex-shrink: 0;
|
|
372
|
+
}
|
|
373
|
+
.nft-chip {
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
justify-content: center;
|
|
377
|
+
gap: 6px;
|
|
378
|
+
padding: 5px 0;
|
|
379
|
+
border-radius: 6px;
|
|
380
|
+
background: rgba(13, 82, 48, 0.4);
|
|
381
|
+
border: 1px solid rgba(20, 130, 72, 0.5);
|
|
382
|
+
font-size: 0.78rem;
|
|
383
|
+
font-weight: 700;
|
|
384
|
+
color: #ffffff;
|
|
385
|
+
letter-spacing: 0.05em;
|
|
386
|
+
text-transform: uppercase;
|
|
387
|
+
transition: background 0.4s ease, border-color 0.4s ease;
|
|
388
|
+
}
|
|
389
|
+
.nft-dot {
|
|
390
|
+
width: 9px;
|
|
391
|
+
height: 9px;
|
|
392
|
+
border-radius: 50%;
|
|
393
|
+
background: #16a34a;
|
|
394
|
+
box-shadow: 0 0 10px rgba(22, 163, 74, 0.9);
|
|
395
|
+
transition: background 0.4s ease, box-shadow 0.4s ease;
|
|
396
|
+
}
|
|
397
|
+
.nft-body {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: flex-end;
|
|
400
|
+
gap: 8px;
|
|
401
|
+
}
|
|
402
|
+
.nft-qr {
|
|
403
|
+
width: 100%;
|
|
404
|
+
height: auto;
|
|
405
|
+
aspect-ratio: 1;
|
|
406
|
+
border-radius: 10px;
|
|
407
|
+
}
|
|
408
|
+
.nft-logo-col {
|
|
409
|
+
display: flex;
|
|
410
|
+
flex-direction: column;
|
|
411
|
+
align-items: center;
|
|
412
|
+
gap: 0;
|
|
413
|
+
align-self: flex-end;
|
|
414
|
+
flex-shrink: 0;
|
|
415
|
+
margin-left: auto;
|
|
416
|
+
margin-right: -22px;
|
|
417
|
+
margin-bottom: -18px;
|
|
418
|
+
}
|
|
419
|
+
.nft-logo-badge {
|
|
420
|
+
width: 130px;
|
|
421
|
+
filter: drop-shadow(0 2px 12px rgba(var(--ski-green-rgb), 0.35));
|
|
422
|
+
}
|
|
423
|
+
.nft-logo-badge svg {
|
|
424
|
+
width: 100%;
|
|
425
|
+
height: auto;
|
|
426
|
+
display: block;
|
|
427
|
+
}
|
|
428
|
+
.nft-discount {
|
|
429
|
+
display: flex;
|
|
430
|
+
flex-direction: row;
|
|
431
|
+
align-items: center;
|
|
432
|
+
gap: 4px;
|
|
433
|
+
font-size: 0.72rem;
|
|
434
|
+
font-weight: 700;
|
|
435
|
+
color: rgba(255, 255, 255, 0.9);
|
|
436
|
+
line-height: 1.3;
|
|
437
|
+
margin-top: -14px;
|
|
438
|
+
position: relative;
|
|
439
|
+
z-index: 1;
|
|
440
|
+
}
|
|
441
|
+
.nft-discount .discount-sui-icon {
|
|
442
|
+
width: 0.65em;
|
|
443
|
+
height: 0.85em;
|
|
444
|
+
display: inline-block;
|
|
445
|
+
vertical-align: -0.1em;
|
|
446
|
+
margin-left: 1px;
|
|
447
|
+
margin-right: 0;
|
|
448
|
+
}
|
|
449
|
+
.nft-price-stack {
|
|
450
|
+
display: flex;
|
|
451
|
+
align-items: stretch;
|
|
452
|
+
gap: 0;
|
|
453
|
+
margin-bottom: -4px;
|
|
454
|
+
}
|
|
455
|
+
.nft-price-main {
|
|
456
|
+
display: flex;
|
|
457
|
+
align-items: center;
|
|
458
|
+
}
|
|
459
|
+
.nft-price-stack .price-amount {
|
|
460
|
+
font-size: clamp(2.4rem, 7vw, 3rem);
|
|
461
|
+
font-weight: 800;
|
|
462
|
+
color: #e9fff3;
|
|
463
|
+
line-height: 1;
|
|
464
|
+
}
|
|
465
|
+
.nft-price-stack .price-rest {
|
|
466
|
+
display: flex;
|
|
467
|
+
flex-direction: column;
|
|
468
|
+
justify-content: center;
|
|
469
|
+
gap: 1px;
|
|
470
|
+
margin-left: 2px;
|
|
471
|
+
}
|
|
472
|
+
.nft-price-stack .price-rest-top {
|
|
473
|
+
display: flex;
|
|
474
|
+
align-items: baseline;
|
|
475
|
+
gap: 4px;
|
|
476
|
+
}
|
|
477
|
+
.nft-price-stack .price-decimals {
|
|
478
|
+
font-size: 1.05rem;
|
|
479
|
+
font-weight: 700;
|
|
480
|
+
color: rgba(233, 255, 243, 0.75);
|
|
481
|
+
}
|
|
482
|
+
.nft-price-stack .price-sui-icon {
|
|
483
|
+
width: 14px;
|
|
484
|
+
height: 18px;
|
|
485
|
+
display: inline-block;
|
|
486
|
+
vertical-align: baseline;
|
|
487
|
+
fill: #4DA2FF;
|
|
488
|
+
}
|
|
489
|
+
.nft-price-stack .price-usd {
|
|
490
|
+
color: var(--muted);
|
|
491
|
+
font-size: 0.88rem;
|
|
492
|
+
font-weight: 700;
|
|
493
|
+
white-space: nowrap;
|
|
494
|
+
}
|
|
495
|
+
.nft-price-stack .payment-choice {
|
|
496
|
+
border-radius: 8px;
|
|
497
|
+
cursor: pointer;
|
|
498
|
+
transition: background 0.16s ease, box-shadow 0.16s ease, color 0.16s ease;
|
|
499
|
+
}
|
|
500
|
+
.nft-price-stack .payment-choice:hover {
|
|
501
|
+
background: rgba(var(--ski-green-rgb), 0.09);
|
|
502
|
+
}
|
|
503
|
+
.nft-price-stack .payment-choice.active {
|
|
504
|
+
background: rgba(var(--ski-green-rgb), 0.17);
|
|
505
|
+
box-shadow: 0 0 0 1px rgba(var(--ski-green-rgb), 0.42) inset;
|
|
506
|
+
}
|
|
507
|
+
.reg-form {
|
|
508
|
+
max-width: 440px;
|
|
509
|
+
margin: 8px auto 0;
|
|
510
|
+
display: grid;
|
|
511
|
+
gap: 10px;
|
|
512
|
+
width: 100%;
|
|
513
|
+
}
|
|
514
|
+
.reg-action-row {
|
|
515
|
+
display: flex;
|
|
516
|
+
align-items: stretch;
|
|
517
|
+
gap: 6px;
|
|
518
|
+
}
|
|
519
|
+
.reg-action-row .button {
|
|
520
|
+
flex: 1;
|
|
521
|
+
white-space: nowrap;
|
|
522
|
+
}
|
|
523
|
+
.download-qr-btn {
|
|
524
|
+
width: 38px;
|
|
525
|
+
flex-shrink: 0;
|
|
526
|
+
display: flex;
|
|
527
|
+
align-items: center;
|
|
528
|
+
justify-content: center;
|
|
529
|
+
padding: 0;
|
|
530
|
+
border-radius: 10px;
|
|
531
|
+
border: 1px solid rgba(var(--ski-green-rgb), 0.18);
|
|
532
|
+
background: rgba(var(--ski-green-rgb), 0.05);
|
|
533
|
+
color: var(--muted);
|
|
534
|
+
cursor: pointer;
|
|
535
|
+
opacity: 0.45;
|
|
536
|
+
transition: opacity 0.2s ease, border-color 0.2s ease, background 0.2s ease;
|
|
537
|
+
}
|
|
538
|
+
.download-qr-btn:hover { opacity: 1; border-color: rgba(var(--ski-green-rgb), 0.45); color: var(--ski-green-light); background: rgba(var(--ski-green-rgb), 0.1); }
|
|
539
|
+
.download-qr-btn.active { opacity: 0.9; border-color: rgba(var(--ski-green-rgb), 0.5); color: var(--ski-green); background: rgba(var(--ski-green-rgb), 0.12); }
|
|
540
|
+
.download-qr-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
|
|
541
|
+
.reg-sui-icon {
|
|
542
|
+
width: 0.85em;
|
|
543
|
+
height: 1.1em;
|
|
544
|
+
display: inline-block;
|
|
545
|
+
vertical-align: -0.15em;
|
|
546
|
+
margin-left: 1px;
|
|
547
|
+
}
|
|
548
|
+
.price-amount { color: #e9fff3; }
|
|
549
|
+
.price-sui-icon {
|
|
550
|
+
width: 0.6em;
|
|
551
|
+
height: 0.77em;
|
|
552
|
+
display: inline-block;
|
|
553
|
+
vertical-align: -0.05em;
|
|
554
|
+
fill: #4DA2FF;
|
|
555
|
+
}
|
|
556
|
+
.price-usd {
|
|
557
|
+
color: var(--muted);
|
|
558
|
+
font-size: 0.44em;
|
|
559
|
+
font-weight: 600;
|
|
560
|
+
white-space: nowrap;
|
|
561
|
+
}
|
|
562
|
+
.price-decimals {
|
|
563
|
+
font-size: 0.56em;
|
|
564
|
+
font-weight: 700;
|
|
565
|
+
opacity: 0.9;
|
|
566
|
+
margin-left: 2px;
|
|
567
|
+
}
|
|
568
|
+
.year-stepper-h {
|
|
569
|
+
display: flex;
|
|
570
|
+
align-items: center;
|
|
571
|
+
gap: 0;
|
|
572
|
+
border-radius: 10px;
|
|
573
|
+
border: 1px solid rgba(var(--ski-green-rgb), 0.28);
|
|
574
|
+
background: rgba(10, 34, 22, 0.55);
|
|
575
|
+
flex-shrink: 0;
|
|
576
|
+
transition: border-color 0.4s ease;
|
|
577
|
+
outline: none;
|
|
578
|
+
}
|
|
579
|
+
.year-stepper-h:focus-within { border-color: rgba(var(--ski-green-rgb), 0.5); }
|
|
580
|
+
.year-btn-h {
|
|
581
|
+
width: 34px;
|
|
582
|
+
height: 38px;
|
|
583
|
+
border: none;
|
|
584
|
+
background: rgba(var(--ski-green-rgb), 0.1);
|
|
585
|
+
color: var(--ski-green-light);
|
|
586
|
+
font-size: 1rem;
|
|
587
|
+
font-weight: 800;
|
|
588
|
+
line-height: 1;
|
|
589
|
+
cursor: pointer;
|
|
590
|
+
display: flex;
|
|
591
|
+
align-items: center;
|
|
592
|
+
justify-content: center;
|
|
593
|
+
}
|
|
594
|
+
.year-btn-h:first-child { border-radius: 9px 0 0 9px; }
|
|
595
|
+
.year-btn-h:last-of-type { border-radius: 0 9px 9px 0; }
|
|
596
|
+
.year-btn-h:hover { background: rgba(var(--ski-green-rgb), 0.22); }
|
|
597
|
+
.year-btn-h:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
598
|
+
.year-display-h {
|
|
599
|
+
display: flex;
|
|
600
|
+
align-items: baseline;
|
|
601
|
+
gap: 2px;
|
|
602
|
+
font-size: 1rem;
|
|
603
|
+
font-weight: 800;
|
|
604
|
+
color: var(--text);
|
|
605
|
+
padding: 0 6px;
|
|
606
|
+
white-space: nowrap;
|
|
607
|
+
user-select: none;
|
|
608
|
+
}
|
|
609
|
+
.year-unit {
|
|
610
|
+
font-size: 0.6rem;
|
|
611
|
+
font-weight: 600;
|
|
612
|
+
color: var(--muted);
|
|
613
|
+
text-transform: uppercase;
|
|
614
|
+
letter-spacing: 0.06em;
|
|
615
|
+
}
|
|
616
|
+
.button {
|
|
617
|
+
width: 100%;
|
|
618
|
+
padding: 14px 18px;
|
|
619
|
+
border-radius: 14px;
|
|
620
|
+
border: 1px solid rgba(var(--ski-green-rgb), 0.5);
|
|
621
|
+
background: linear-gradient(135deg, rgba(14, 56, 36, 0.95), rgba(8, 42, 27, 0.96));
|
|
622
|
+
box-shadow: inset 0 1px 0 rgba(var(--ski-green-rgb), 0.2);
|
|
623
|
+
color: var(--ski-green-light);
|
|
624
|
+
font-weight: 800;
|
|
625
|
+
font-size: 0.92rem;
|
|
626
|
+
cursor: pointer;
|
|
627
|
+
}
|
|
628
|
+
.button:hover { background: linear-gradient(135deg, rgba(16, 67, 42, 0.96), rgba(9, 50, 31, 0.98)); }
|
|
629
|
+
.button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
630
|
+
.status {
|
|
631
|
+
display: none;
|
|
632
|
+
padding: 8px 10px;
|
|
633
|
+
border-radius: 8px;
|
|
634
|
+
font-size: 0.8rem;
|
|
635
|
+
}
|
|
636
|
+
.status.show { display: block; }
|
|
637
|
+
.status.info {
|
|
638
|
+
background: rgba(var(--ski-green-rgb), 0.12);
|
|
639
|
+
border: 1px solid rgba(var(--ski-green-rgb), 0.3);
|
|
640
|
+
color: var(--ski-green-light);
|
|
641
|
+
}
|
|
642
|
+
.status.ok { background: rgba(var(--ski-green-rgb),0.15); border: 1px solid rgba(var(--ski-green-rgb),0.36); color: var(--ski-green-light); }
|
|
643
|
+
.status.err { background: rgba(248,113,113,0.12); border: 1px solid rgba(248,113,113,0.3); color: #ffb0b0; }
|
|
644
|
+
.home-float-btn {
|
|
645
|
+
position: fixed;
|
|
646
|
+
top: calc(16px + env(safe-area-inset-top));
|
|
647
|
+
left: calc(16px + env(safe-area-inset-left));
|
|
648
|
+
z-index: 10035;
|
|
649
|
+
display: flex;
|
|
650
|
+
flex-direction: column;
|
|
651
|
+
align-items: center;
|
|
652
|
+
justify-content: center;
|
|
653
|
+
gap: 3px;
|
|
654
|
+
width: 52px;
|
|
655
|
+
height: 52px;
|
|
656
|
+
padding: 0;
|
|
657
|
+
border-radius: 12px;
|
|
658
|
+
background: rgba(7, 14, 28, 0.72);
|
|
659
|
+
border: 1px solid rgba(255, 255, 255, 0.62);
|
|
660
|
+
text-decoration: none;
|
|
661
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
|
662
|
+
backdrop-filter: blur(8px);
|
|
663
|
+
-webkit-backdrop-filter: blur(8px);
|
|
664
|
+
transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
|
|
665
|
+
}
|
|
666
|
+
.home-float-btn:hover {
|
|
667
|
+
background: rgba(12, 22, 40, 0.88);
|
|
668
|
+
border-color: rgba(255, 255, 255, 0.92);
|
|
669
|
+
transform: translateY(-1px);
|
|
670
|
+
}
|
|
671
|
+
.home-float-sail {
|
|
672
|
+
width: 28px;
|
|
673
|
+
height: 10px;
|
|
674
|
+
display: block;
|
|
675
|
+
}
|
|
676
|
+
.home-float-sail svg {
|
|
677
|
+
width: 100%;
|
|
678
|
+
height: 100%;
|
|
679
|
+
display: block;
|
|
680
|
+
}
|
|
681
|
+
.home-float-wordmark {
|
|
682
|
+
display: flex;
|
|
683
|
+
align-items: center;
|
|
684
|
+
justify-content: center;
|
|
685
|
+
}
|
|
686
|
+
.home-float-dot {
|
|
687
|
+
width: 12px;
|
|
688
|
+
height: 12px;
|
|
689
|
+
border-radius: 999px;
|
|
690
|
+
background: #40c463;
|
|
691
|
+
border: 2px solid #ffffff;
|
|
692
|
+
box-shadow: 0 0 12px rgba(64, 196, 99, 0.48);
|
|
693
|
+
flex-shrink: 0;
|
|
694
|
+
}
|
|
695
|
+
.wallet-widget {
|
|
696
|
+
position: fixed;
|
|
697
|
+
top: calc(16px + env(safe-area-inset-top));
|
|
698
|
+
right: calc(16px + env(safe-area-inset-right));
|
|
699
|
+
z-index: 10040;
|
|
700
|
+
display: flex;
|
|
701
|
+
align-items: center;
|
|
702
|
+
gap: 10px;
|
|
703
|
+
}
|
|
704
|
+
.wallet-profile-btn {
|
|
705
|
+
min-width: 74px;
|
|
706
|
+
height: 40px;
|
|
707
|
+
border-radius: 10px;
|
|
708
|
+
display: none;
|
|
709
|
+
align-items: center;
|
|
710
|
+
justify-content: center;
|
|
711
|
+
gap: 7px;
|
|
712
|
+
background: rgba(7, 14, 28, 0.72);
|
|
713
|
+
border: 1px solid rgba(255, 255, 255, 0.55);
|
|
714
|
+
padding: 0 10px;
|
|
715
|
+
cursor: pointer;
|
|
716
|
+
}
|
|
717
|
+
.wallet-profile-mark {
|
|
718
|
+
width: 14px;
|
|
719
|
+
height: 14px;
|
|
720
|
+
background: #177ec7;
|
|
721
|
+
border: 1.5px solid #ffffff;
|
|
722
|
+
border-radius: 3px;
|
|
723
|
+
flex-shrink: 0;
|
|
724
|
+
}
|
|
725
|
+
.wallet-profile-text {
|
|
726
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
727
|
+
font-size: 0.96rem;
|
|
728
|
+
font-weight: 700;
|
|
729
|
+
letter-spacing: 0.04em;
|
|
730
|
+
color: #ffffff;
|
|
731
|
+
line-height: 1;
|
|
732
|
+
}
|
|
733
|
+
.wallet-profile-btn.visible { display: inline-flex; }
|
|
734
|
+
.wallet-profile-btn:hover { background: rgba(12, 23, 44, 0.85); border-color: rgba(255, 255, 255, 0.82); }
|
|
735
|
+
.wallet-widget.has-black-diamond .wallet-profile-btn {
|
|
736
|
+
background: linear-gradient(135deg, rgba(10, 10, 18, 0.6), rgba(18, 18, 28, 0.7));
|
|
737
|
+
border-color: rgba(120, 130, 155, 0.42);
|
|
738
|
+
box-shadow: 0 0 24px rgba(0, 0, 0, 0.4);
|
|
739
|
+
}
|
|
740
|
+
.wallet-widget.has-black-diamond .wallet-profile-btn:hover {
|
|
741
|
+
background: linear-gradient(135deg, rgba(16, 16, 26, 0.7), rgba(24, 24, 36, 0.8));
|
|
742
|
+
border-color: rgba(140, 150, 175, 0.62);
|
|
743
|
+
box-shadow: 0 0 28px rgba(0, 0, 0, 0.5);
|
|
744
|
+
}
|
|
745
|
+
.wallet-widget.has-black-diamond #wk-widget .wk-widget-btn.connected,
|
|
746
|
+
.wallet-widget.has-black-diamond #wk-widget > div > button.connected {
|
|
747
|
+
background: linear-gradient(135deg, rgba(8, 8, 16, 0.95), rgba(16, 16, 30, 0.94));
|
|
748
|
+
border-color: rgba(198, 170, 98, 0.62);
|
|
749
|
+
color: #d0d4e0;
|
|
750
|
+
box-shadow:
|
|
751
|
+
0 0 0 1px rgba(160, 120, 56, 0.24) inset,
|
|
752
|
+
0 10px 24px rgba(0, 0, 0, 0.58),
|
|
753
|
+
0 0 18px rgba(194, 145, 72, 0.26);
|
|
754
|
+
}
|
|
755
|
+
.wallet-widget.has-black-diamond #wk-widget .wk-widget-btn.connected:hover,
|
|
756
|
+
.wallet-widget.has-black-diamond #wk-widget > div > button.connected:hover {
|
|
757
|
+
border-color: rgba(234, 206, 128, 0.88);
|
|
758
|
+
box-shadow:
|
|
759
|
+
0 0 0 1px rgba(196, 154, 76, 0.34) inset,
|
|
760
|
+
0 12px 28px rgba(0, 0, 0, 0.62),
|
|
761
|
+
0 0 24px rgba(234, 179, 8, 0.28);
|
|
762
|
+
}
|
|
763
|
+
.wallet-widget:not(.has-black-diamond) #wk-widget .wk-widget-btn,
|
|
764
|
+
.wallet-widget:not(.has-black-diamond) #wk-widget > div > button {
|
|
765
|
+
border-color: rgba(255, 255, 255, 0.78);
|
|
766
|
+
}
|
|
767
|
+
.wallet-widget:not(.has-black-diamond) #wk-widget .wk-widget-btn:hover,
|
|
768
|
+
.wallet-widget:not(.has-black-diamond) #wk-widget > div > button:hover {
|
|
769
|
+
border-color: rgba(255, 255, 255, 0.96);
|
|
770
|
+
}
|
|
771
|
+
.tracker-footer {
|
|
772
|
+
position: fixed;
|
|
773
|
+
left: 0;
|
|
774
|
+
right: 0;
|
|
775
|
+
bottom: 0;
|
|
776
|
+
z-index: 900;
|
|
777
|
+
background: rgba(2, 9, 6, 0.94);
|
|
778
|
+
backdrop-filter: blur(10px);
|
|
779
|
+
border-top: 1px solid rgba(var(--ski-green-rgb), 0.35);
|
|
780
|
+
display: flex;
|
|
781
|
+
flex-direction: column;
|
|
782
|
+
align-items: center;
|
|
783
|
+
gap: 0;
|
|
784
|
+
}
|
|
785
|
+
.tracker-brand {
|
|
786
|
+
display: flex;
|
|
787
|
+
align-items: center;
|
|
788
|
+
justify-content: center;
|
|
789
|
+
gap: 8px;
|
|
790
|
+
padding: 7px 16px 0;
|
|
791
|
+
}
|
|
792
|
+
.tracker-line {
|
|
793
|
+
display: flex;
|
|
794
|
+
align-items: center;
|
|
795
|
+
gap: 10px;
|
|
796
|
+
font-size: 0.84rem;
|
|
797
|
+
color: #8cae9a;
|
|
798
|
+
white-space: nowrap;
|
|
799
|
+
overflow-x: auto;
|
|
800
|
+
max-width: 100%;
|
|
801
|
+
scrollbar-width: none;
|
|
802
|
+
padding: 5px 16px 9px;
|
|
803
|
+
}
|
|
804
|
+
.tracker-line::-webkit-scrollbar { display: none; }
|
|
805
|
+
.tracker-price-label {
|
|
806
|
+
color: #deffec;
|
|
807
|
+
font-weight: 600;
|
|
808
|
+
display: inline-flex;
|
|
809
|
+
align-items: center;
|
|
810
|
+
gap: 6px;
|
|
811
|
+
}
|
|
812
|
+
.tracker-sui-icon {
|
|
813
|
+
width: 0.8em;
|
|
814
|
+
height: 1em;
|
|
815
|
+
display: inline-block;
|
|
816
|
+
vertical-align: middle;
|
|
817
|
+
}
|
|
818
|
+
#sui-price { color: var(--ski-green-light); font-weight: 700; }
|
|
819
|
+
.tracker-sep { color: rgba(var(--ski-green-rgb), 0.4); }
|
|
820
|
+
.tracker-built-on { color: #8cae9a; }
|
|
821
|
+
.tracker-built-on a {
|
|
822
|
+
color: var(--ski-green-light);
|
|
823
|
+
text-decoration: none;
|
|
824
|
+
font-weight: 600;
|
|
825
|
+
}
|
|
826
|
+
.tracker-built-on a:hover { color: var(--ski-green-soft); }
|
|
827
|
+
@media (max-width: 760px) {
|
|
828
|
+
body { padding: 14px; padding-top: 54px; padding-bottom: 74px; }
|
|
829
|
+
.layout-grid { grid-template-columns: 1fr; }
|
|
830
|
+
.home-float-btn { top: 12px; left: 12px; width: 44px; height: 44px; border-radius: 10px; }
|
|
831
|
+
.home-float-sail { width: 24px; height: 8px; }
|
|
832
|
+
.home-float-dot { width: 10px; height: 10px; border-width: 1.5px; }
|
|
833
|
+
.wallet-widget { top: 12px; right: 12px; }
|
|
834
|
+
.tracker-footer { padding: 10px 8px; }
|
|
835
|
+
.tracker-line { font-size: 0.78rem; }
|
|
836
|
+
.nft-card { padding: 16px; gap: 8px; }
|
|
837
|
+
.nft-qr-col { width: 90px; }
|
|
838
|
+
.nft-logo-badge { width: 100px; }
|
|
839
|
+
.year-btn-h { width: 28px; height: 34px; font-size: 0.9rem; }
|
|
840
|
+
.year-display-h { font-size: 0.85rem; padding: 0 4px; }
|
|
841
|
+
}
|
|
842
|
+
@media (max-width: 1020px) {
|
|
843
|
+
.layout-grid { grid-template-columns: 1fr; }
|
|
844
|
+
}
|
|
845
|
+
${generateWalletUiCss()}
|
|
846
|
+
</style>
|
|
847
|
+
</head>
|
|
848
|
+
<body data-register-flow="${registerFlow}">
|
|
849
|
+
<div id="wk-modal"></div>
|
|
850
|
+
<a class="home-float-btn" href="https://sui.ski" title="Go to sui.ski" aria-label="Go to sui.ski home">
|
|
851
|
+
<span class="home-float-sail" aria-hidden="true">
|
|
852
|
+
<svg viewBox="0 0 244 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
853
|
+
<path d="M6 66Q92 66 146 30Q181 8 238 6L226 24L220 66H6Z" fill="#ffffff"/>
|
|
854
|
+
</svg>
|
|
855
|
+
</span>
|
|
856
|
+
<span class="home-float-wordmark">
|
|
857
|
+
<span class="home-float-dot" aria-hidden="true"></span>
|
|
858
|
+
</span>
|
|
859
|
+
</a>
|
|
860
|
+
<div class="wallet-widget" id="wallet-widget">
|
|
861
|
+
<div id="wk-widget"></div>
|
|
862
|
+
<button class="wallet-profile-btn" id="wallet-profile-btn" title="Go to sui.ski" aria-label="Open wallet profile">
|
|
863
|
+
<span class="wallet-profile-mark" aria-hidden="true"></span>
|
|
864
|
+
<span class="wallet-profile-text">SKI</span>
|
|
865
|
+
</button>
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
<div class="container">
|
|
869
|
+
<div class="layout-grid">
|
|
870
|
+
${registrationCardHtml}
|
|
871
|
+
${discoveryColumnHtml}
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
|
|
875
|
+
<div class="tracker-footer">
|
|
876
|
+
<div class="tracker-brand">
|
|
877
|
+
<a class="nft-brand-link" href="https://sui.ski">${generateLogoSvg(14)}<span class="nft-brand-name">.ski</span></a>
|
|
878
|
+
<span class="nft-brand-tagline">Lift every 0xAddr to human-readability at scale</span>
|
|
879
|
+
</div>
|
|
880
|
+
<span class="tracker-line">
|
|
881
|
+
<span class="tracker-price-label"><img class="tracker-sui-icon" src="/media-pack/SuiIcon.svg" alt="SUI"><span id="sui-price">$--</span></span>
|
|
882
|
+
<span class="tracker-sep">\u00b7</span>
|
|
883
|
+
<span class="tracker-built-on">
|
|
884
|
+
Built on
|
|
885
|
+
<a href="https://docs.sui.io" target="_blank" rel="noopener">Sui</a>
|
|
886
|
+
<span class="tracker-sep">\u00b7</span>
|
|
887
|
+
<a href="https://docs.suins.io" target="_blank" rel="noopener">SuiNS</a>
|
|
888
|
+
<span class="tracker-sep">\u00b7</span>
|
|
889
|
+
<a href="https://moveregistry.com/docs" target="_blank" rel="noopener">MVR</a>
|
|
890
|
+
<span class="tracker-sep">\u00b7</span>
|
|
891
|
+
<a href="https://docs.sui.io/standards/deepbook" target="_blank" rel="noopener">DeepBook</a>
|
|
892
|
+
<span class="tracker-sep">\u00b7</span>
|
|
893
|
+
<a href="https://www.tradeport.xyz/docs" target="_blank" rel="noopener">Tradeport</a>
|
|
894
|
+
<span class="tracker-sep">\u00b7</span>
|
|
895
|
+
<a href="https://docs.wal.app" target="_blank" rel="noopener">Walrus</a>
|
|
896
|
+
<span class="tracker-sep">\u00b7</span>
|
|
897
|
+
<a href="https://seal-docs.wal.app" target="_blank" rel="noopener">Seal</a>
|
|
898
|
+
<span class="tracker-sep">\u00b7</span>
|
|
899
|
+
<a href="https://docs.waap.xyz/category/guides-sui" target="_blank" rel="noopener">WaaP</a>
|
|
900
|
+
</span>
|
|
901
|
+
</span>
|
|
902
|
+
</div>
|
|
903
|
+
|
|
904
|
+
<script type="module">
|
|
905
|
+
let SuiJsonRpcClient, Transaction, SuinsClient, SuinsTransaction
|
|
906
|
+
{
|
|
907
|
+
const pickExport = (mod, name) => {
|
|
908
|
+
if (!mod || typeof mod !== 'object') return undefined
|
|
909
|
+
if (name in mod) return mod[name]
|
|
910
|
+
if (mod.default && typeof mod.default === 'object' && name in mod.default) return mod.default[name]
|
|
911
|
+
return undefined
|
|
912
|
+
}
|
|
913
|
+
const SDK_TIMEOUT = 15000
|
|
914
|
+
const timedImport = (url) => Promise.race([
|
|
915
|
+
import(url),
|
|
916
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout: ' + url)), SDK_TIMEOUT)),
|
|
917
|
+
])
|
|
918
|
+
const results = await Promise.allSettled([
|
|
919
|
+
timedImport('https://esm.sh/@wallet-standard/app@1.1.0'),
|
|
920
|
+
timedImport('https://esm.sh/@mysten/sui@2.4.0/jsonRpc?bundle'),
|
|
921
|
+
timedImport('https://esm.sh/@mysten/sui@2.4.0/transactions?bundle'),
|
|
922
|
+
timedImport('https://esm.sh/@mysten/suins@1.0.0?bundle'),
|
|
923
|
+
])
|
|
924
|
+
if (results[1].status === 'fulfilled') ({ SuiJsonRpcClient } = results[1].value)
|
|
925
|
+
if (results[2].status === 'fulfilled') ({ Transaction } = results[2].value)
|
|
926
|
+
if (results[3].status === 'fulfilled') {
|
|
927
|
+
const suinsModule = results[3].value
|
|
928
|
+
SuinsClient = pickExport(suinsModule, 'SuinsClient')
|
|
929
|
+
SuinsTransaction = pickExport(suinsModule, 'SuinsTransaction')
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (typeof SuiJsonRpcClient === 'function' && typeof window !== 'undefined') {
|
|
933
|
+
window.SuiJsonRpcClient = SuiJsonRpcClient
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
${generateWalletSessionJs()}
|
|
937
|
+
${generateWalletKitJs({ network: env.SUI_NETWORK, autoConnect: true })}
|
|
938
|
+
${generateWalletTxJs()}
|
|
939
|
+
${generateWalletUiJs({
|
|
940
|
+
showPrimaryName: true,
|
|
941
|
+
onConnect: 'onRegisterWalletConnected',
|
|
942
|
+
onDisconnect: 'onRegisterWalletDisconnected',
|
|
943
|
+
})}
|
|
944
|
+
|
|
945
|
+
const NAME = ${serializeJson(cleanName)}
|
|
946
|
+
const NETWORK = ${serializeJson(network)}
|
|
947
|
+
const REGISTER_FLOW = ${serializeJson(registerFlow)}
|
|
948
|
+
const REGISTER_BUCKET = ${serializeJson(registerBucket)}
|
|
949
|
+
const IS_REGISTERABLE = ${isRegisterable ? 'true' : 'false'}
|
|
950
|
+
const WAAP_REFERRAL_ADDRESS = '0x53f1e3d5f1e3f5aefa47fd3d5a47c9b8cc87e26a2c7bf39e26c870ded4eca7df'
|
|
951
|
+
|
|
952
|
+
function isValidSuiAddress(addr) {
|
|
953
|
+
return typeof addr === 'string' && /^0x[0-9a-fA-F]{64}$/.test(addr)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
957
|
+
const referrerAddress = isValidSuiAddress(urlParams.get('ref')) ? urlParams.get('ref') : null
|
|
958
|
+
const waapReferralAddress = isValidSuiAddress(urlParams.get('rw')) ? urlParams.get('rw') : WAAP_REFERRAL_ADDRESS
|
|
959
|
+
let referralFeeMist = 0n
|
|
960
|
+
let waapFeeMist = 0n
|
|
961
|
+
|
|
962
|
+
const yearsEl = document.getElementById('years')
|
|
963
|
+
const yearsValueEl = document.getElementById('years-value')
|
|
964
|
+
const yearsDecreaseBtn = document.getElementById('years-decrease')
|
|
965
|
+
const yearsIncreaseBtn = document.getElementById('years-increase')
|
|
966
|
+
const primaryStarEl = document.getElementById('primary-star')
|
|
967
|
+
const registerBtn = document.getElementById('register-btn')
|
|
968
|
+
const registerStatus = document.getElementById('register-status')
|
|
969
|
+
const priceValue = document.getElementById('price-value')
|
|
970
|
+
const suiPriceEl = document.getElementById('sui-price')
|
|
971
|
+
const walletWidget = document.getElementById('wallet-widget')
|
|
972
|
+
const walletProfileBtn = document.getElementById('wallet-profile-btn')
|
|
973
|
+
const downloadQrBtn = document.getElementById('download-qr-btn')
|
|
974
|
+
const x402PriceEl = document.getElementById('x402-price')
|
|
975
|
+
const x402LinkEl = document.getElementById('x402-link')
|
|
976
|
+
const suggestionsGrid = document.getElementById('suggestions-grid')
|
|
977
|
+
const refreshSuggestionsBtn = document.getElementById('refresh-suggestions')
|
|
978
|
+
|
|
979
|
+
let pricingData = null
|
|
980
|
+
let selectedPaymentMode = 'auto'
|
|
981
|
+
let paymentModeManualOverride = false
|
|
982
|
+
const suggestionStatusCache = new Map()
|
|
983
|
+
let wantsPrimaryName = false
|
|
984
|
+
let primaryStarManualOverride = null
|
|
985
|
+
let primaryStarAddress = ''
|
|
986
|
+
let primaryStarSyncNonce = 0
|
|
987
|
+
const MIN_YEARS = 1
|
|
988
|
+
const MAX_YEARS = 5
|
|
989
|
+
const RPC_URLS = {
|
|
990
|
+
mainnet: 'https://fullnode.mainnet.sui.io:443',
|
|
991
|
+
testnet: 'https://fullnode.testnet.sui.io:443',
|
|
992
|
+
devnet: 'https://fullnode.devnet.sui.io:443',
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
let cachedSuiClient = null
|
|
996
|
+
const SUI_TYPE_FULL = '0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'
|
|
997
|
+
const NS_TYPE_FULL = '0x5145494a5f5100e645e4b0aa950fa6b68f614e8c59e17bc5ded3495123a79178::ns::NS'
|
|
998
|
+
const USDC_TYPE_FULL = '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC'
|
|
999
|
+
const USD_SYMBOL_PRIORITY = ['USDC', 'USDT', 'AUSD', 'USDY', 'FDUSD', 'DAI']
|
|
1000
|
+
const COIN_PAGE_LIMIT = 50
|
|
1001
|
+
const COIN_PAGE_MAX = 6
|
|
1002
|
+
function getSuiClient() {
|
|
1003
|
+
if (!cachedSuiClient && typeof SuiJsonRpcClient === 'function') {
|
|
1004
|
+
cachedSuiClient = new SuiJsonRpcClient({ url: RPC_URLS[NETWORK] || RPC_URLS.mainnet })
|
|
1005
|
+
}
|
|
1006
|
+
return cachedSuiClient
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function parseMistToBigInt(value) {
|
|
1010
|
+
if (typeof value === 'bigint') return value
|
|
1011
|
+
if (typeof value === 'number' && Number.isFinite(value)) return BigInt(Math.floor(value))
|
|
1012
|
+
if (typeof value === 'string' && value.trim()) {
|
|
1013
|
+
try { return BigInt(value) } catch {}
|
|
1014
|
+
}
|
|
1015
|
+
return 0n
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function usdSymbolRank(name) {
|
|
1019
|
+
const symbol = String(name || '').toUpperCase()
|
|
1020
|
+
for (let i = 0; i < USD_SYMBOL_PRIORITY.length; i++) {
|
|
1021
|
+
if (symbol === USD_SYMBOL_PRIORITY[i]) return i
|
|
1022
|
+
}
|
|
1023
|
+
return 999
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function isUsdStableName(name) {
|
|
1027
|
+
const symbol = String(name || '').toUpperCase()
|
|
1028
|
+
if (!symbol) return false
|
|
1029
|
+
return symbol.includes('USD') || usdSymbolRank(symbol) !== 999
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function estimateTokenMistNeeded(requiredSuiMist, suiPerToken, decimals) {
|
|
1033
|
+
if (!Number.isFinite(suiPerToken) || suiPerToken <= 0) return 0n
|
|
1034
|
+
const suiNeeded = Number(requiredSuiMist) / 1e9
|
|
1035
|
+
if (!Number.isFinite(suiNeeded) || suiNeeded <= 0) return 0n
|
|
1036
|
+
const tokensNeeded = suiNeeded / suiPerToken
|
|
1037
|
+
if (!Number.isFinite(tokensNeeded) || tokensNeeded <= 0) return 0n
|
|
1038
|
+
const bufferedTokens = tokensNeeded * 1.12
|
|
1039
|
+
const scaled = Math.ceil(bufferedTokens * Math.pow(10, Number(decimals || 0)))
|
|
1040
|
+
if (!Number.isFinite(scaled) || scaled <= 0) return 0n
|
|
1041
|
+
return BigInt(scaled)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
async function collectCoinObjectIdsForAmountClient(owner, coinType, targetAmount) {
|
|
1045
|
+
const client = getSuiClient()
|
|
1046
|
+
if (!client) return { coinObjectIds: [], total: 0n }
|
|
1047
|
+
const coinObjectIds = []
|
|
1048
|
+
let total = 0n
|
|
1049
|
+
let cursor = null
|
|
1050
|
+
for (let i = 0; i < COIN_PAGE_MAX; i++) {
|
|
1051
|
+
const page = await client.getCoins({
|
|
1052
|
+
owner,
|
|
1053
|
+
coinType,
|
|
1054
|
+
cursor: cursor || undefined,
|
|
1055
|
+
limit: COIN_PAGE_LIMIT,
|
|
1056
|
+
})
|
|
1057
|
+
const rows = Array.isArray(page && page.data) ? page.data : []
|
|
1058
|
+
for (const coin of rows) {
|
|
1059
|
+
if (!coin || typeof coin.coinObjectId !== 'string') continue
|
|
1060
|
+
const coinBalance = parseMistToBigInt(coin.balance)
|
|
1061
|
+
if (coinBalance <= 0n) continue
|
|
1062
|
+
coinObjectIds.push(coin.coinObjectId)
|
|
1063
|
+
total += coinBalance
|
|
1064
|
+
if (total >= targetAmount) return { coinObjectIds, total }
|
|
1065
|
+
}
|
|
1066
|
+
if (!page || !page.hasNextPage || !page.nextCursor) break
|
|
1067
|
+
cursor = page.nextCursor
|
|
1068
|
+
}
|
|
1069
|
+
return { coinObjectIds, total }
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function resolveRegisterUsdCoinPayment(address, years) {
|
|
1073
|
+
const client = getSuiClient()
|
|
1074
|
+
if (!client) throw new Error('Wallet RPC client unavailable')
|
|
1075
|
+
let requiredSuiMist = parseMistToBigInt(pricingData && (pricingData.discountedSuiMist || pricingData.directSuiMist || 0))
|
|
1076
|
+
if (requiredSuiMist <= 0n) {
|
|
1077
|
+
const pricingRes = await fetch('/api/pricing?domain=' + encodeURIComponent(NAME) + '&years=' + years)
|
|
1078
|
+
const nextPricing = await pricingRes.json().catch(() => null)
|
|
1079
|
+
if (pricingRes.ok && nextPricing) pricingData = nextPricing
|
|
1080
|
+
requiredSuiMist = parseMistToBigInt(nextPricing && (nextPricing.discountedSuiMist || nextPricing.directSuiMist || 0))
|
|
1081
|
+
}
|
|
1082
|
+
if (requiredSuiMist <= 0n) throw new Error('Unable to price registration for USD payment')
|
|
1083
|
+
|
|
1084
|
+
const pools = await fetch('/api/deepbook-pools').then((r) => r.json()).catch(() => [])
|
|
1085
|
+
if (!Array.isArray(pools) || pools.length === 0) {
|
|
1086
|
+
throw new Error('DeepBook pools unavailable')
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const usdcPool = pools.find((pool) => pool && pool.coinType === USDC_TYPE_FULL)
|
|
1090
|
+
let requiredUsdcMist = 0n
|
|
1091
|
+
if (usdcPool) {
|
|
1092
|
+
requiredUsdcMist = estimateTokenMistNeeded(
|
|
1093
|
+
requiredSuiMist,
|
|
1094
|
+
Number(usdcPool.suiPerToken || 0),
|
|
1095
|
+
Number(usdcPool.decimals || 0),
|
|
1096
|
+
)
|
|
1097
|
+
}
|
|
1098
|
+
if (requiredUsdcMist <= 0n) {
|
|
1099
|
+
const discountedUsd = Number(pricingData?.breakdown?.discountedPriceUsd || 0)
|
|
1100
|
+
if (Number.isFinite(discountedUsd) && discountedUsd > 0) {
|
|
1101
|
+
requiredUsdcMist = BigInt(Math.ceil(discountedUsd * 1_000_000 * 1.12))
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (requiredUsdcMist <= 0n) throw new Error('Unable to estimate required USDC amount')
|
|
1105
|
+
|
|
1106
|
+
const usdcBalance = await client
|
|
1107
|
+
.getBalance({ owner: address, coinType: USDC_TYPE_FULL })
|
|
1108
|
+
.catch(() => ({ totalBalance: '0' }))
|
|
1109
|
+
const totalUsdcMist = parseMistToBigInt(usdcBalance && usdcBalance.totalBalance)
|
|
1110
|
+
if (totalUsdcMist < requiredUsdcMist) return null
|
|
1111
|
+
|
|
1112
|
+
const picked = await collectCoinObjectIdsForAmountClient(address, USDC_TYPE_FULL, requiredUsdcMist)
|
|
1113
|
+
if (!picked.coinObjectIds.length || picked.total < requiredUsdcMist) return null
|
|
1114
|
+
|
|
1115
|
+
return {
|
|
1116
|
+
sourceCoinType: USDC_TYPE_FULL,
|
|
1117
|
+
coinObjectIds: picked.coinObjectIds,
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function formatPrimaryPriceParts(sui) {
|
|
1122
|
+
if (!Number.isFinite(sui) || sui <= 0) return { whole: '--', decimals: '' }
|
|
1123
|
+
const whole = Math.trunc(sui)
|
|
1124
|
+
const fraction = sui - whole
|
|
1125
|
+
if (fraction > 0.95) {
|
|
1126
|
+
return { whole: String(whole + 1), decimals: '' }
|
|
1127
|
+
}
|
|
1128
|
+
const dec = Math.floor(fraction * 100)
|
|
1129
|
+
if (dec < 5) {
|
|
1130
|
+
return { whole: String(whole), decimals: '' }
|
|
1131
|
+
}
|
|
1132
|
+
const decText = String(dec).padStart(2, '0')
|
|
1133
|
+
const normalized = decText.endsWith('0') ? decText.slice(0, 1) : decText
|
|
1134
|
+
return { whole: String(whole), decimals: '.' + normalized }
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function formatUsdAmount(usdValue) {
|
|
1138
|
+
if (!Number.isFinite(usdValue) || usdValue <= 0) return null
|
|
1139
|
+
return new Intl.NumberFormat('en-US', {
|
|
1140
|
+
minimumFractionDigits: 0,
|
|
1141
|
+
maximumFractionDigits: 0,
|
|
1142
|
+
}).format(Math.round(usdValue))
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function normalizePaymentMode(value) {
|
|
1146
|
+
if (value === 'coin' || value === 'sui' || value === 'auto') return value
|
|
1147
|
+
return 'auto'
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function updatePaymentChoiceUi() {
|
|
1151
|
+
if (!priceValue) return
|
|
1152
|
+
const activeMode = normalizePaymentMode(selectedPaymentMode)
|
|
1153
|
+
const choices = priceValue.querySelectorAll('[data-payment-mode]')
|
|
1154
|
+
for (const choice of choices) {
|
|
1155
|
+
const mode = choice.getAttribute('data-payment-mode')
|
|
1156
|
+
const isActive = activeMode !== 'auto' && mode === activeMode
|
|
1157
|
+
choice.classList.toggle('active', isActive)
|
|
1158
|
+
choice.setAttribute('aria-pressed', isActive ? 'true' : 'false')
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function setSelectedPaymentMode(mode) {
|
|
1163
|
+
selectedPaymentMode = normalizePaymentMode(mode)
|
|
1164
|
+
updatePaymentChoiceUi()
|
|
1165
|
+
updateRegisterButton()
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function getWalletRouteLabel() {
|
|
1169
|
+
const walletName = getConnectedWalletName().trim()
|
|
1170
|
+
if (!walletName) return 'Connected wallet'
|
|
1171
|
+
return walletName === 'WaaP' ? 'WaaP wallet' : walletName + ' wallet'
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function updatePrimaryStarUi() {
|
|
1175
|
+
if (!primaryStarEl) return
|
|
1176
|
+
primaryStarEl.classList.toggle('active', wantsPrimaryName)
|
|
1177
|
+
primaryStarEl.textContent = wantsPrimaryName ? '★' : '☆'
|
|
1178
|
+
primaryStarEl.setAttribute('aria-pressed', wantsPrimaryName ? 'true' : 'false')
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function showStatus(message, type = 'info', html = false) {
|
|
1182
|
+
if (!registerStatus) return
|
|
1183
|
+
registerStatus.className = 'status show ' + type
|
|
1184
|
+
if (html) registerStatus.innerHTML = message
|
|
1185
|
+
else registerStatus.textContent = message
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function hideStatus() {
|
|
1189
|
+
if (!registerStatus) return
|
|
1190
|
+
registerStatus.className = 'status'
|
|
1191
|
+
registerStatus.textContent = ''
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function getSelectedYears() {
|
|
1195
|
+
const years = Number(yearsEl && yearsEl.value ? yearsEl.value : '1')
|
|
1196
|
+
if (!Number.isFinite(years)) return MIN_YEARS
|
|
1197
|
+
return Math.min(MAX_YEARS, Math.max(MIN_YEARS, Math.floor(years)))
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function yearProgress(years) {
|
|
1201
|
+
return Math.min(1, Math.max(0, (years - MIN_YEARS) / (MAX_YEARS - MIN_YEARS)))
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function lerpRgb(r0, g0, b0, r1, g1, b1, t) {
|
|
1205
|
+
return 'rgb(' + Math.round(r0 + (r1 - r0) * t) + ',' + Math.round(g0 + (g1 - g0) * t) + ',' + Math.round(b0 + (b1 - b0) * t) + ')'
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function yearColor(years) {
|
|
1209
|
+
return lerpRgb(26, 153, 96, 255, 255, 255, yearProgress(years))
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function updateYearTheme(years) {
|
|
1213
|
+
const t = yearProgress(years)
|
|
1214
|
+
|
|
1215
|
+
const nameEl = document.querySelector('.nft-name')
|
|
1216
|
+
if (nameEl) {
|
|
1217
|
+
nameEl.style.color = lerpRgb(240, 244, 248, 255, 255, 255, t)
|
|
1218
|
+
const ng = (0.15 + t * 0.25).toFixed(2)
|
|
1219
|
+
nameEl.style.textShadow = '0 1px 3px rgba(0,0,0,' + (0.4 - t * 0.2).toFixed(2) + '), 0 0 ' + (12 + t * 18) + 'px rgba(200,220,240,' + ng + ')'
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const tldEl = document.querySelector('.nft-name-tld')
|
|
1223
|
+
if (tldEl) {
|
|
1224
|
+
tldEl.style.color = lerpRgb(73, 218, 145, 240, 248, 255, t)
|
|
1225
|
+
tldEl.style.animation = 'none'
|
|
1226
|
+
const ga = (0.5 + t * 0.4).toFixed(2)
|
|
1227
|
+
const gb = (0.2 + t * 0.4).toFixed(2)
|
|
1228
|
+
const gc = (t * 0.25).toFixed(2)
|
|
1229
|
+
tldEl.style.textShadow = '0 0 ' + (8 + t * 12) + 'px rgba(73,218,145,' + ga + '), 0 0 ' + (20 + t * 30) + 'px rgba(73,218,145,' + gb + '), 0 0 ' + (50 + t * 30) + 'px rgba(73,218,145,' + gc + ')'
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const borderColor = lerpRgb(73, 218, 145, 200, 210, 225, t)
|
|
1233
|
+
const borderA = (0.18 + t * 0.3).toFixed(2)
|
|
1234
|
+
const cardEl = document.querySelector('.nft-card')
|
|
1235
|
+
if (cardEl) {
|
|
1236
|
+
cardEl.style.borderColor = borderColor.replace('rgb(', 'rgba(').replace(')', ',' + borderA + ')')
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const pr = Math.round(73 + 127 * t)
|
|
1240
|
+
const pg = Math.round(218 - 8 * t)
|
|
1241
|
+
const pb = Math.round(145 + 80 * t)
|
|
1242
|
+
document.documentElement.style.setProperty('--portal-r', String(pr))
|
|
1243
|
+
document.documentElement.style.setProperty('--portal-g', String(pg))
|
|
1244
|
+
document.documentElement.style.setProperty('--portal-b', String(pb))
|
|
1245
|
+
document.documentElement.style.setProperty('--portal-a', (0.35 + t * 0.5).toFixed(2))
|
|
1246
|
+
|
|
1247
|
+
const chipEl = document.querySelector('.nft-chip')
|
|
1248
|
+
if (chipEl) {
|
|
1249
|
+
chipEl.style.color = '#ffffff'
|
|
1250
|
+
const cbg = lerpRgb(13, 82, 48, 80, 90, 100, t)
|
|
1251
|
+
chipEl.style.background = cbg.replace('rgb(', 'rgba(').replace(')', ',' + (0.4 - t * 0.1).toFixed(2) + ')')
|
|
1252
|
+
const cbd = lerpRgb(20, 130, 72, 180, 195, 210, t)
|
|
1253
|
+
chipEl.style.borderColor = cbd.replace('rgb(', 'rgba(').replace(')', ',' + (0.5 + t * 0.2).toFixed(2) + ')')
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const dotEl = document.querySelector('.nft-dot')
|
|
1257
|
+
if (dotEl) {
|
|
1258
|
+
const dotColor = lerpRgb(22, 163, 74, 220, 235, 245, t)
|
|
1259
|
+
dotEl.style.background = dotColor
|
|
1260
|
+
dotEl.style.boxShadow = '0 0 ' + (8 + t * 6) + 'px ' + dotColor
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const stepperEl = document.querySelector('.year-stepper-h')
|
|
1264
|
+
if (stepperEl) {
|
|
1265
|
+
const sb = lerpRgb(73, 218, 145, 200, 210, 225, t)
|
|
1266
|
+
stepperEl.style.borderColor = sb.replace('rgb(', 'rgba(').replace(')', ',' + (0.28 + t * 0.3).toFixed(2) + ')')
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
document.body.style.setProperty('--snow-boost', (t * 0.5).toFixed(3))
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function setSelectedYears(nextYears) {
|
|
1273
|
+
const normalized = Math.min(MAX_YEARS, Math.max(MIN_YEARS, Math.floor(Number(nextYears) || MIN_YEARS)))
|
|
1274
|
+
if (yearsEl) yearsEl.value = String(normalized)
|
|
1275
|
+
if (yearsValueEl) {
|
|
1276
|
+
yearsValueEl.textContent = String(normalized)
|
|
1277
|
+
yearsValueEl.style.color = yearColor(normalized)
|
|
1278
|
+
}
|
|
1279
|
+
if (yearsDecreaseBtn) yearsDecreaseBtn.disabled = normalized <= MIN_YEARS
|
|
1280
|
+
if (yearsIncreaseBtn) yearsIncreaseBtn.disabled = normalized >= MAX_YEARS
|
|
1281
|
+
updateYearTheme(normalized)
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function getConnectedAddress() {
|
|
1285
|
+
const conn = SuiWalletKit.$connection.value
|
|
1286
|
+
if (!conn) return null
|
|
1287
|
+
if (conn.status !== 'connected' && conn.status !== 'session') return null
|
|
1288
|
+
return conn.address || null
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function shouldForceSignBridge() {
|
|
1292
|
+
const conn = SuiWalletKit.$connection.value || {}
|
|
1293
|
+
return !(conn.status === 'session' && !conn.wallet)
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function getConnectedPrimaryName() {
|
|
1297
|
+
const conn = SuiWalletKit.$connection.value
|
|
1298
|
+
if (!conn) return null
|
|
1299
|
+
if (conn.status !== 'connected' && conn.status !== 'session') return null
|
|
1300
|
+
if (!conn.primaryName || typeof conn.primaryName !== 'string') return null
|
|
1301
|
+
const normalized = conn.primaryName.trim().replace(/\\.sui$/i, '')
|
|
1302
|
+
return normalized || null
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function getRpcUrlForNetwork() {
|
|
1306
|
+
return RPC_URLS[NETWORK] || RPC_URLS.mainnet
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
async function fetchPrimaryNameForAddress(address) {
|
|
1310
|
+
if (!address || typeof SuiJsonRpcClient !== 'function') return { resolved: false, name: null }
|
|
1311
|
+
try {
|
|
1312
|
+
const client = new SuiJsonRpcClient({ url: getRpcUrlForNetwork() })
|
|
1313
|
+
const result = await client.resolveNameServiceNames({ address })
|
|
1314
|
+
const name = result?.data?.[0]
|
|
1315
|
+
if (!name || typeof name !== 'string') return { resolved: true, name: null }
|
|
1316
|
+
const normalized = name.replace(/\\.sui$/i, '')
|
|
1317
|
+
return { resolved: true, name: normalized || null }
|
|
1318
|
+
} catch {
|
|
1319
|
+
return { resolved: false, name: null }
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
async function syncPrimaryStarState() {
|
|
1324
|
+
const address = getConnectedAddress()
|
|
1325
|
+
if (!address) {
|
|
1326
|
+
primaryStarAddress = ''
|
|
1327
|
+
primaryStarManualOverride = null
|
|
1328
|
+
wantsPrimaryName = false
|
|
1329
|
+
updatePrimaryStarUi()
|
|
1330
|
+
return
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if (primaryStarAddress !== address) {
|
|
1334
|
+
primaryStarAddress = address
|
|
1335
|
+
primaryStarManualOverride = null
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (typeof primaryStarManualOverride === 'boolean') {
|
|
1339
|
+
wantsPrimaryName = primaryStarManualOverride
|
|
1340
|
+
updatePrimaryStarUi()
|
|
1341
|
+
return
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const connectedPrimaryName = getConnectedPrimaryName()
|
|
1345
|
+
if (connectedPrimaryName) {
|
|
1346
|
+
wantsPrimaryName = false
|
|
1347
|
+
updatePrimaryStarUi()
|
|
1348
|
+
return
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const syncNonce = ++primaryStarSyncNonce
|
|
1352
|
+
const primaryResolution = await fetchPrimaryNameForAddress(address)
|
|
1353
|
+
if (syncNonce !== primaryStarSyncNonce) return
|
|
1354
|
+
if (getConnectedAddress() !== address) return
|
|
1355
|
+
if (!primaryResolution.resolved) return
|
|
1356
|
+
wantsPrimaryName = primaryResolution.name === null
|
|
1357
|
+
updatePrimaryStarUi()
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function parsePriceMist(value) {
|
|
1361
|
+
if (typeof value === 'bigint') return value
|
|
1362
|
+
if (typeof value === 'number' && Number.isFinite(value)) return BigInt(Math.floor(value))
|
|
1363
|
+
if (typeof value === 'string' && value.trim()) {
|
|
1364
|
+
try {
|
|
1365
|
+
return BigInt(value)
|
|
1366
|
+
} catch {
|
|
1367
|
+
return null
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return null
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function getSuiCoinConfig(suinsClient) {
|
|
1374
|
+
const coins = suinsClient?.config?.coins || {}
|
|
1375
|
+
if (coins.SUI) return coins.SUI
|
|
1376
|
+
if (coins.sui) return coins.sui
|
|
1377
|
+
const values = Object.values(coins)
|
|
1378
|
+
for (const cfg of values) {
|
|
1379
|
+
const coinType = String(cfg?.type || cfg?.coinType || '')
|
|
1380
|
+
if (coinType.endsWith('::sui::SUI')) return cfg
|
|
1381
|
+
}
|
|
1382
|
+
return null
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function updateWalletProfileButton() {
|
|
1386
|
+
if (!walletProfileBtn) return
|
|
1387
|
+
const address = getConnectedAddress()
|
|
1388
|
+
const primaryName = getConnectedPrimaryName()
|
|
1389
|
+
walletProfileBtn.classList.toggle('visible', !!address)
|
|
1390
|
+
if (walletWidget) {
|
|
1391
|
+
walletWidget.classList.toggle('has-black-diamond', !!primaryName)
|
|
1392
|
+
}
|
|
1393
|
+
if (primaryName) {
|
|
1394
|
+
walletProfileBtn.dataset.href = 'https://' + encodeURIComponent(primaryName) + '.sui.ski'
|
|
1395
|
+
walletProfileBtn.title = 'Go to ' + primaryName + '.sui.ski'
|
|
1396
|
+
} else {
|
|
1397
|
+
walletProfileBtn.dataset.href = 'https://sui.ski'
|
|
1398
|
+
walletProfileBtn.title = 'Go to sui.ski'
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const REG_SUI_ICON = '<svg class="reg-sui-icon" viewBox="0 0 300 384" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M240.057 159.914C255.698 179.553 265.052 204.39 265.052 231.407C265.052 258.424 255.414 284.019 239.362 303.768L237.971 305.475L237.608 303.31C237.292 301.477 236.929 299.613 236.502 297.749C228.46 262.421 202.265 232.134 159.148 207.597C130.029 191.071 113.361 171.195 108.985 148.586C106.157 133.972 108.258 119.294 112.318 106.717C116.379 94.1569 122.414 83.6187 127.549 77.2831L144.328 56.7754C147.267 53.1731 152.781 53.1731 155.719 56.7754L240.073 159.914H240.057ZM266.584 139.422L154.155 1.96703C152.007 -0.655678 147.993 -0.655678 145.845 1.96703L33.4316 139.422L33.0683 139.881C12.3868 165.555 0 198.181 0 233.698C0 316.408 67.1635 383.461 150 383.461C232.837 383.461 300 316.408 300 233.698C300 198.181 287.613 165.555 266.932 139.896L266.568 139.438L266.584 139.422ZM60.3381 159.472L70.3866 147.164L70.6868 149.439C70.9237 151.24 71.2239 153.041 71.5715 154.858C78.0809 189.001 101.322 217.456 140.173 239.496C173.952 258.724 193.622 280.828 199.278 305.064C201.648 315.176 202.059 325.129 201.032 333.835L200.969 334.372L200.479 334.609C185.233 342.05 168.09 346.237 149.984 346.237C86.4546 346.237 34.9484 294.826 34.9484 231.391C34.9484 204.153 44.4439 179.142 60.3065 159.44L60.3381 159.472Z" fill="#4DA2FF"/></svg>'
|
|
1403
|
+
|
|
1404
|
+
function updateRegisterButton() {
|
|
1405
|
+
if (!registerBtn) return
|
|
1406
|
+
const address = getConnectedAddress()
|
|
1407
|
+
updateWalletProfileButton()
|
|
1408
|
+
const stepperEl = document.querySelector('.year-stepper-h')
|
|
1409
|
+
if (!address) {
|
|
1410
|
+
registerBtn.style.display = 'none'
|
|
1411
|
+
if (downloadQrBtn) downloadQrBtn.style.display = 'none'
|
|
1412
|
+
if (stepperEl) stepperEl.style.display = 'none'
|
|
1413
|
+
return
|
|
1414
|
+
}
|
|
1415
|
+
registerBtn.style.display = ''
|
|
1416
|
+
if (downloadQrBtn) downloadQrBtn.style.display = ''
|
|
1417
|
+
if (stepperEl) stepperEl.style.display = ''
|
|
1418
|
+
if (selectedPaymentMode === 'coin') {
|
|
1419
|
+
const usd = Number(pricingData?.breakdown?.discountedPriceUsd || 0)
|
|
1420
|
+
const usdText = formatUsdAmount(usd)
|
|
1421
|
+
registerBtn.textContent = usdText ? 'Accept $' + usdText + ' USDC' : 'Accept via USDC'
|
|
1422
|
+
return
|
|
1423
|
+
}
|
|
1424
|
+
if (pricingData && pricingData.discountedSuiMist) {
|
|
1425
|
+
const sui = Number(pricingData.discountedSuiMist) / 1e9
|
|
1426
|
+
if (Number.isFinite(sui) && sui > 0) {
|
|
1427
|
+
registerBtn.innerHTML = 'Accept ' + Math.ceil(sui) + ' ' + REG_SUI_ICON
|
|
1428
|
+
return
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
registerBtn.innerHTML = 'Accept ' + NAME + '.sui'
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function fetchPricing() {
|
|
1435
|
+
if (!IS_REGISTERABLE) return
|
|
1436
|
+
const years = getSelectedYears()
|
|
1437
|
+
try {
|
|
1438
|
+
const response = await fetch('/api/pricing?domain=' + encodeURIComponent(NAME) + '&years=' + years)
|
|
1439
|
+
if (!response.ok) throw new Error('Pricing request failed')
|
|
1440
|
+
pricingData = await response.json()
|
|
1441
|
+
const mist = Number(pricingData?.discountedSuiMist || pricingData?.directSuiMist || 0)
|
|
1442
|
+
const sui = mist / 1e9
|
|
1443
|
+
const discountedUsdRaw = Number(pricingData?.breakdown?.discountedPriceUsd || 0)
|
|
1444
|
+
const discountedUsdText = formatUsdAmount(discountedUsdRaw)
|
|
1445
|
+
const savingsMist = Number(pricingData?.savingsMist || 0)
|
|
1446
|
+
const savingsSui = savingsMist / 1e9
|
|
1447
|
+
const directUsd = Number(pricingData?.breakdown?.basePriceUsd || 0)
|
|
1448
|
+
const savingsUsd = directUsd - discountedUsdRaw
|
|
1449
|
+
if (referrerAddress && savingsMist > 0) {
|
|
1450
|
+
referralFeeMist = BigInt(Math.floor(savingsMist * 10 / 100))
|
|
1451
|
+
waapFeeMist = BigInt(Math.floor(savingsMist * 5 / 100))
|
|
1452
|
+
} else {
|
|
1453
|
+
referralFeeMist = 0n
|
|
1454
|
+
waapFeeMist = 0n
|
|
1455
|
+
}
|
|
1456
|
+
if (priceValue) {
|
|
1457
|
+
const priceParts = formatPrimaryPriceParts(sui)
|
|
1458
|
+
const perYearUsd = years > 0 ? discountedUsdRaw / years : discountedUsdRaw
|
|
1459
|
+
const perYearUsdText = formatUsdAmount(perYearUsd)
|
|
1460
|
+
const usdLabel = perYearUsdText ? '= $' + perYearUsdText + '/yr' : '= $--'
|
|
1461
|
+
const suiIcon = '<svg class="price-sui-icon" viewBox="0 0 300 384" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M240.057 159.914C255.698 179.553 265.052 204.39 265.052 231.407C265.052 258.424 255.414 284.019 239.362 303.768L237.971 305.475L237.608 303.31C237.292 301.477 236.929 299.613 236.502 297.749C228.46 262.421 202.265 232.134 159.148 207.597C130.029 191.071 113.361 171.195 108.985 148.586C106.157 133.972 108.258 119.294 112.318 106.717C116.379 94.1569 122.414 83.6187 127.549 77.2831L144.328 56.7754C147.267 53.1731 152.781 53.1731 155.719 56.7754L240.073 159.914H240.057ZM266.584 139.422L154.155 1.96703C152.007 -0.655678 147.993 -0.655678 145.845 1.96703L33.4316 139.422L33.0683 139.881C12.3868 165.555 0 198.181 0 233.698C0 316.408 67.1635 383.461 150 383.461C232.837 383.461 300 316.408 300 233.698C300 198.181 287.613 165.555 266.932 139.896L266.568 139.438L266.584 139.422ZM60.3381 159.472L70.3866 147.164L70.6868 149.439C70.9237 151.24 71.2239 153.041 71.5715 154.858C78.0809 189.001 101.322 217.456 140.173 239.496C173.952 258.724 193.622 280.828 199.278 305.064C201.648 315.176 202.059 325.129 201.032 333.835L200.969 334.372L200.479 334.609C185.233 342.05 168.09 346.237 149.984 346.237C86.4546 346.237 34.9484 294.826 34.9484 231.391C34.9484 204.153 44.4439 179.142 60.3065 159.44L60.3381 159.472Z" fill="#4DA2FF"/></svg>'
|
|
1462
|
+
const discountEl = document.getElementById('price-savings')
|
|
1463
|
+
if (savingsSui > 0.5 && savingsUsd > 0.5 && discountEl) {
|
|
1464
|
+
const savingsUsdText = formatUsdAmount(savingsUsd)
|
|
1465
|
+
const discountSuiIcon = '<svg class="discount-sui-icon" viewBox="0 0 300 384" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M240.057 159.914C255.698 179.553 265.052 204.39 265.052 231.407C265.052 258.424 255.414 284.019 239.362 303.768L237.971 305.475L237.608 303.31C237.292 301.477 236.929 299.613 236.502 297.749C228.46 262.421 202.265 232.134 159.148 207.597C130.029 191.071 113.361 171.195 108.985 148.586C106.157 133.972 108.258 119.294 112.318 106.717C116.379 94.1569 122.414 83.6187 127.549 77.2831L144.328 56.7754C147.267 53.1731 152.781 53.1731 155.719 56.7754L240.073 159.914H240.057ZM266.584 139.422L154.155 1.96703C152.007 -0.655678 147.993 -0.655678 145.845 1.96703L33.4316 139.422L33.0683 139.881C12.3868 165.555 0 198.181 0 233.698C0 316.408 67.1635 383.461 150 383.461C232.837 383.461 300 316.408 300 233.698C300 198.181 287.613 165.555 266.932 139.896L266.568 139.438L266.584 139.422ZM60.3381 159.472L70.3866 147.164L70.6868 149.439C70.9237 151.24 71.2239 153.041 71.5715 154.858C78.0809 189.001 101.322 217.456 140.173 239.496C173.952 258.724 193.622 280.828 199.278 305.064C201.648 315.176 202.059 325.129 201.032 333.835L200.969 334.372L200.479 334.609C185.233 342.05 168.09 346.237 149.984 346.237C86.4546 346.237 34.9484 294.826 34.9484 231.391C34.9484 204.153 44.4439 179.142 60.3065 159.44L60.3381 159.472Z" fill="#4DA2FF"/></svg>'
|
|
1466
|
+
discountEl.innerHTML =
|
|
1467
|
+
'<span>-' + Math.round(savingsSui) + discountSuiIcon + '</span>' +
|
|
1468
|
+
'<span>($' + (savingsUsdText || '--') + ')</span>'
|
|
1469
|
+
}
|
|
1470
|
+
const usdColor = yearColor(years)
|
|
1471
|
+
const decimalsHtml = priceParts.decimals ? '<span class="price-decimals">' + priceParts.decimals + '</span>' : ''
|
|
1472
|
+
priceValue.innerHTML =
|
|
1473
|
+
'<div class="nft-price-main payment-choice" data-payment-mode="sui" role="button" tabindex="0" aria-label="Pay with SUI"><span class="price-amount">' + priceParts.whole + '</span></div>' +
|
|
1474
|
+
'<div class="price-rest">' +
|
|
1475
|
+
'<span class="price-rest-top payment-choice" data-payment-mode="sui" role="button" tabindex="0" aria-label="Pay with SUI">' + decimalsHtml + suiIcon + '</span>' +
|
|
1476
|
+
'<span class="price-usd payment-choice" data-payment-mode="coin" role="button" tabindex="0" aria-label="Pay with USD coin estimate" style="color:' + usdColor + '">' + usdLabel + '</span>' +
|
|
1477
|
+
'</div>'
|
|
1478
|
+
updatePaymentChoiceUi()
|
|
1479
|
+
}
|
|
1480
|
+
updateRegisterButton()
|
|
1481
|
+
checkAutoPaymentRoute()
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
if (priceValue) {
|
|
1484
|
+
priceValue.innerHTML =
|
|
1485
|
+
'<div class="nft-price-main payment-choice" data-payment-mode="sui" role="button" tabindex="0" aria-label="Pay with SUI"><span class="price-amount">--</span></div>' +
|
|
1486
|
+
'<div class="price-rest">' +
|
|
1487
|
+
'<span class="price-rest-top payment-choice" data-payment-mode="sui" role="button" tabindex="0" aria-label="Pay with SUI"><svg class="price-sui-icon" viewBox="0 0 300 384" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M240.057 159.914C255.698 179.553 265.052 204.39 265.052 231.407C265.052 258.424 255.414 284.019 239.362 303.768L237.971 305.475L237.608 303.31C237.292 301.477 236.929 299.613 236.502 297.749C228.46 262.421 202.265 232.134 159.148 207.597C130.029 191.071 113.361 171.195 108.985 148.586C106.157 133.972 108.258 119.294 112.318 106.717C116.379 94.1569 122.414 83.6187 127.549 77.2831L144.328 56.7754C147.267 53.1731 152.781 53.1731 155.719 56.7754L240.073 159.914H240.057ZM266.584 139.422L154.155 1.96703C152.007 -0.655678 147.993 -0.655678 145.845 1.96703L33.4316 139.422L33.0683 139.881C12.3868 165.555 0 198.181 0 233.698C0 316.408 67.1635 383.461 150 383.461C232.837 383.461 300 316.408 300 233.698C300 198.181 287.613 165.555 266.932 139.896L266.568 139.438L266.584 139.422ZM60.3381 159.472L70.3866 147.164L70.6868 149.439C70.9237 151.24 71.2239 153.041 71.5715 154.858C78.0809 189.001 101.322 217.456 140.173 239.496C173.952 258.724 193.622 280.828 199.278 305.064C201.648 315.176 202.059 325.129 201.032 333.835L200.969 334.372L200.479 334.609C185.233 342.05 168.09 346.237 149.984 346.237C86.4546 346.237 34.9484 294.826 34.9484 231.391C34.9484 204.153 44.4439 179.142 60.3065 159.44L60.3381 159.472Z" fill="#4DA2FF"/></svg></span>' +
|
|
1488
|
+
'<span class="price-usd payment-choice" data-payment-mode="coin" role="button" tabindex="0" aria-label="Pay with USD coin estimate">\u2248 $--</span>' +
|
|
1489
|
+
'</div>'
|
|
1490
|
+
updatePaymentChoiceUi()
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
async function updateSuiPrice() {
|
|
1496
|
+
if (!suiPriceEl) return
|
|
1497
|
+
try {
|
|
1498
|
+
const res = await fetch('/api/sui-price')
|
|
1499
|
+
if (!res.ok) throw new Error('Price request failed')
|
|
1500
|
+
const data = await res.json()
|
|
1501
|
+
if (data && typeof data.price === 'number' && Number.isFinite(data.price)) {
|
|
1502
|
+
suiPriceEl.textContent = '$' + data.price.toFixed(2)
|
|
1503
|
+
return
|
|
1504
|
+
}
|
|
1505
|
+
suiPriceEl.textContent = '$--'
|
|
1506
|
+
} catch {
|
|
1507
|
+
suiPriceEl.textContent = '$--'
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function getConnectedWalletName() {
|
|
1512
|
+
const conn = SuiWalletKit.$connection.value
|
|
1513
|
+
if (conn && conn.wallet && conn.wallet.name) return String(conn.wallet.name)
|
|
1514
|
+
const session = typeof getWalletSession === 'function' ? getWalletSession() : null
|
|
1515
|
+
return session && session.walletName ? String(session.walletName) : ''
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
async function checkAutoPaymentRoute() {
|
|
1519
|
+
if (!IS_REGISTERABLE || paymentModeManualOverride) return
|
|
1520
|
+
const address = getConnectedAddress()
|
|
1521
|
+
if (!address) return
|
|
1522
|
+
const client = getSuiClient()
|
|
1523
|
+
if (!client) return
|
|
1524
|
+
const requiredSuiMist = parseMistToBigInt(
|
|
1525
|
+
pricingData && (pricingData.discountedSuiMist || pricingData.directSuiMist || 0)
|
|
1526
|
+
)
|
|
1527
|
+
if (requiredSuiMist <= 0n) return
|
|
1528
|
+
try {
|
|
1529
|
+
const suiBal = await client.getBalance({ owner: address, coinType: SUI_TYPE_FULL })
|
|
1530
|
+
const totalSuiMist = parseMistToBigInt(suiBal && suiBal.totalBalance)
|
|
1531
|
+
const overhead = (requiredSuiMist * 50n) / 100n + 80000000n
|
|
1532
|
+
if (totalSuiMist >= requiredSuiMist + overhead) return
|
|
1533
|
+
const usdcBal = await client.getBalance({ owner: address, coinType: USDC_TYPE_FULL })
|
|
1534
|
+
const totalUsdcMist = parseMistToBigInt(usdcBal && usdcBal.totalBalance)
|
|
1535
|
+
const discountedUsd = Number(pricingData?.breakdown?.discountedPriceUsd || 0)
|
|
1536
|
+
if (!Number.isFinite(discountedUsd) || discountedUsd <= 0) return
|
|
1537
|
+
const requiredUsdcMist = BigInt(Math.ceil(discountedUsd * 1_000_000 * 1.12))
|
|
1538
|
+
if (totalUsdcMist >= requiredUsdcMist) {
|
|
1539
|
+
setSelectedPaymentMode('coin')
|
|
1540
|
+
}
|
|
1541
|
+
} catch {}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function getConnectedWalletIcon() {
|
|
1545
|
+
const conn = SuiWalletKit.$connection.value
|
|
1546
|
+
if (conn && conn.wallet && conn.wallet.icon) return String(conn.wallet.icon)
|
|
1547
|
+
return ''
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function getConnectedAccountLabel() {
|
|
1551
|
+
const conn = SuiWalletKit.$connection.value
|
|
1552
|
+
if (!conn || !conn.account) return ''
|
|
1553
|
+
return typeof conn.account.label === 'string' ? conn.account.label.trim() : ''
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function updateCardWalletInfo() {
|
|
1557
|
+
const el = document.getElementById('nft-wallet-icons')
|
|
1558
|
+
if (!el) return
|
|
1559
|
+
const address = getConnectedAddress()
|
|
1560
|
+
if (!address) {
|
|
1561
|
+
el.className = 'nft-wallet-icons'
|
|
1562
|
+
el.innerHTML = ''
|
|
1563
|
+
return
|
|
1564
|
+
}
|
|
1565
|
+
const walletName = getConnectedWalletName()
|
|
1566
|
+
const isWaaP = walletName.toLowerCase() === 'waap'
|
|
1567
|
+
let accountLabel = getConnectedAccountLabel()
|
|
1568
|
+
if (!accountLabel && isWaaP && typeof __wkGetWaaPLabelForAddress === 'function') {
|
|
1569
|
+
accountLabel = __wkGetWaaPLabelForAddress(address)
|
|
1570
|
+
}
|
|
1571
|
+
if (!accountLabel && isWaaP) {
|
|
1572
|
+
const pName = getConnectedPrimaryName()
|
|
1573
|
+
if (pName) accountLabel = pName
|
|
1574
|
+
}
|
|
1575
|
+
let html = ''
|
|
1576
|
+
if (isWaaP && typeof __wkWaaPIcon === 'string' && __wkWaaPIcon) {
|
|
1577
|
+
html += '<div class="nft-wallet-icon-row">' +
|
|
1578
|
+
'<img src="' + __wkWaaPIcon + '" alt="WaaP" onerror="this.style.display=\\'none\\'">' +
|
|
1579
|
+
'<span class="nft-wallet-icon-label">WaaP</span>' +
|
|
1580
|
+
'</div>'
|
|
1581
|
+
}
|
|
1582
|
+
const waapMethod = isWaaP && typeof __wkGetWaaPMethodForAddress === 'function'
|
|
1583
|
+
? __wkGetWaaPMethodForAddress(address)
|
|
1584
|
+
: (isWaaP && typeof __wkPendingWaaPMethod === 'string' ? __wkPendingWaaPMethod : '')
|
|
1585
|
+
if (waapMethod && typeof __wkSocialIcons === 'object' && __wkSocialIcons[waapMethod]) {
|
|
1586
|
+
const methodSvg = __wkSocialIcons[waapMethod]
|
|
1587
|
+
.replace(/width="\\d+"/, 'width="20"')
|
|
1588
|
+
.replace(/height="\\d+"/, 'height="20"')
|
|
1589
|
+
.replace(/fill="[^"]*"/, 'fill="#e2e8f0"')
|
|
1590
|
+
let methodLabel = accountLabel || ''
|
|
1591
|
+
if (!methodLabel) {
|
|
1592
|
+
const fallbackLabels = { x: 'X', google: 'Google', discord: 'Discord', apple: 'Apple', email: 'Email', phone: 'Phone' }
|
|
1593
|
+
methodLabel = fallbackLabels[waapMethod] || ''
|
|
1594
|
+
}
|
|
1595
|
+
html += '<div class="nft-wallet-icon-row">' + methodSvg +
|
|
1596
|
+
(methodLabel ? '<span class="nft-wallet-icon-label">' + methodLabel + '</span>' : '') +
|
|
1597
|
+
'</div>'
|
|
1598
|
+
}
|
|
1599
|
+
if (!isWaaP && walletName) {
|
|
1600
|
+
html += '<div class="nft-wallet-icon-row"><span class="nft-wallet-icon-label">' + walletName + '</span></div>'
|
|
1601
|
+
}
|
|
1602
|
+
if (html) {
|
|
1603
|
+
el.className = 'nft-wallet-icons show'
|
|
1604
|
+
el.innerHTML = html
|
|
1605
|
+
} else {
|
|
1606
|
+
el.className = 'nft-wallet-icons'
|
|
1607
|
+
el.innerHTML = ''
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function formatCompactSuiPrice(suiAmount) {
|
|
1612
|
+
if (!Number.isFinite(suiAmount) || suiAmount <= 0) return null
|
|
1613
|
+
const units = [
|
|
1614
|
+
{ value: 1e9, suffix: 'B' },
|
|
1615
|
+
{ value: 1e6, suffix: 'M' },
|
|
1616
|
+
{ value: 1e3, suffix: 'K' },
|
|
1617
|
+
]
|
|
1618
|
+
let scaled = suiAmount
|
|
1619
|
+
let suffix = ''
|
|
1620
|
+
for (const unit of units) {
|
|
1621
|
+
if (suiAmount >= unit.value) {
|
|
1622
|
+
scaled = suiAmount / unit.value
|
|
1623
|
+
suffix = unit.suffix
|
|
1624
|
+
break
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
if (scaled >= 100) return String(Math.trunc(scaled)) + suffix + ' SUI'
|
|
1628
|
+
const truncatedTenths = Math.floor(scaled * 10) / 10
|
|
1629
|
+
return truncatedTenths.toFixed(1) + suffix + ' SUI'
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
async function updateX402Listing() {
|
|
1633
|
+
if (!x402PriceEl) return
|
|
1634
|
+
x402PriceEl.textContent = 'Loading...'
|
|
1635
|
+
x402PriceEl.classList.remove('listed')
|
|
1636
|
+
try {
|
|
1637
|
+
const response = await fetch('/api/marketplace/x402')
|
|
1638
|
+
if (!response.ok) throw new Error('Listing request failed')
|
|
1639
|
+
const data = await response.json()
|
|
1640
|
+
const listingMist = Number(data?.bestListing?.price || 0)
|
|
1641
|
+
const tradeportUrl = typeof data?.bestListing?.tradeportUrl === 'string' && data.bestListing.tradeportUrl
|
|
1642
|
+
? data.bestListing.tradeportUrl
|
|
1643
|
+
: 'https://www.tradeport.xyz/sui/collection/suins?search=x402'
|
|
1644
|
+
if (x402LinkEl) x402LinkEl.href = tradeportUrl
|
|
1645
|
+
if (Number.isFinite(listingMist) && listingMist > 0) {
|
|
1646
|
+
const listingSui = listingMist / 1e9
|
|
1647
|
+
const compactPrice = formatCompactSuiPrice(listingSui)
|
|
1648
|
+
x402PriceEl.textContent = compactPrice || 'No listing'
|
|
1649
|
+
x402PriceEl.classList.add('listed')
|
|
1650
|
+
return
|
|
1651
|
+
}
|
|
1652
|
+
x402PriceEl.textContent = 'No listing'
|
|
1653
|
+
} catch {
|
|
1654
|
+
x402PriceEl.textContent = 'Unavailable'
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function trackEvent(eventName, payload) {
|
|
1659
|
+
try {
|
|
1660
|
+
window.dispatchEvent(new CustomEvent(eventName, { detail: payload }))
|
|
1661
|
+
if (Array.isArray(window.dataLayer)) {
|
|
1662
|
+
window.dataLayer.push({ event: eventName, ...payload })
|
|
1663
|
+
}
|
|
1664
|
+
if (typeof window.plausible === 'function') {
|
|
1665
|
+
window.plausible(eventName, { props: payload })
|
|
1666
|
+
}
|
|
1667
|
+
} catch {}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function markRegisterFlowImpression() {
|
|
1671
|
+
trackEvent('sui_ski_register_impression', {
|
|
1672
|
+
registerFlow: REGISTER_FLOW,
|
|
1673
|
+
registerBucket: REGISTER_BUCKET,
|
|
1674
|
+
registerName: NAME,
|
|
1675
|
+
registerNetwork: NETWORK,
|
|
1676
|
+
})
|
|
1677
|
+
if (REGISTER_FLOW === 'register2') {
|
|
1678
|
+
const currentUrl = new URL(window.location.href)
|
|
1679
|
+
if (!currentUrl.searchParams.has('rf')) {
|
|
1680
|
+
currentUrl.searchParams.set('rf', '2')
|
|
1681
|
+
window.history.replaceState(window.history.state, '', currentUrl.toString())
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function sanitizeCandidate(value) {
|
|
1687
|
+
const cleaned = String(value || '').toLowerCase().replace(/\\.sui$/i, '').replace(/[^a-z0-9-]/g, '')
|
|
1688
|
+
if (!cleaned || cleaned.length < 3 || cleaned === NAME) return null
|
|
1689
|
+
return cleaned
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function buildCandidates(baseName, suggestions) {
|
|
1693
|
+
const suffixes = ['ai', 'app', 'hub', 'pro', 'xyz', 'dao', 'labs', 'agent']
|
|
1694
|
+
const avoidX402 = !String(baseName || '').toLowerCase().includes('x402')
|
|
1695
|
+
const seen = new Set()
|
|
1696
|
+
const list = []
|
|
1697
|
+
const push = (candidate) => {
|
|
1698
|
+
const clean = sanitizeCandidate(candidate)
|
|
1699
|
+
if (!clean || seen.has(clean)) return
|
|
1700
|
+
if (avoidX402 && clean.includes('x402')) return
|
|
1701
|
+
seen.add(clean)
|
|
1702
|
+
list.push(clean)
|
|
1703
|
+
}
|
|
1704
|
+
for (const suggestion of suggestions || []) push(suggestion)
|
|
1705
|
+
for (const suffix of suffixes) push(baseName + suffix)
|
|
1706
|
+
return list.slice(0, 12)
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
async function fetchSuggestions(baseName) {
|
|
1710
|
+
try {
|
|
1711
|
+
const response = await fetch('/api/suggest-names?q=' + encodeURIComponent(baseName) + '&mode=related')
|
|
1712
|
+
if (!response.ok) return [baseName]
|
|
1713
|
+
const body = await response.json()
|
|
1714
|
+
return Array.isArray(body?.suggestions) ? body.suggestions : [baseName]
|
|
1715
|
+
} catch {
|
|
1716
|
+
return [baseName]
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
async function checkCandidate(name) {
|
|
1721
|
+
if (suggestionStatusCache.has(name)) return suggestionStatusCache.get(name)
|
|
1722
|
+
try {
|
|
1723
|
+
const response = await fetch('/api/resolve?name=' + encodeURIComponent(name))
|
|
1724
|
+
if (!response.ok) throw new Error('resolve failed')
|
|
1725
|
+
const body = await response.json()
|
|
1726
|
+
const status = body?.found ? 'taken' : 'available'
|
|
1727
|
+
suggestionStatusCache.set(name, status)
|
|
1728
|
+
return status
|
|
1729
|
+
} catch {
|
|
1730
|
+
suggestionStatusCache.set(name, 'error')
|
|
1731
|
+
return 'error'
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function suggestionAction(name, status) {
|
|
1736
|
+
const href = 'https://' + encodeURIComponent(name) + '.sui.ski'
|
|
1737
|
+
if (status === 'available') return '<a class="suggestion-link available" href="' + href + '">Register</a>'
|
|
1738
|
+
return '<a class="suggestion-link" href="' + href + '">View</a>'
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
async function loadSuggestions(force = false) {
|
|
1742
|
+
if (!suggestionsGrid || !IS_REGISTERABLE) return
|
|
1743
|
+
if (force) suggestionStatusCache.clear()
|
|
1744
|
+
suggestionsGrid.innerHTML = '<div class="empty">Generating suggestions...</div>'
|
|
1745
|
+
const suggested = await fetchSuggestions(NAME)
|
|
1746
|
+
const candidates = buildCandidates(NAME, suggested)
|
|
1747
|
+
if (candidates.length === 0) {
|
|
1748
|
+
suggestionsGrid.innerHTML = '<div class="empty">No suggestions right now.</div>'
|
|
1749
|
+
return
|
|
1750
|
+
}
|
|
1751
|
+
const states = await Promise.all(candidates.map((candidate) => checkCandidate(candidate)))
|
|
1752
|
+
let html = ''
|
|
1753
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
1754
|
+
const candidate = candidates[i]
|
|
1755
|
+
const state = states[i]
|
|
1756
|
+
const stateLabel = state === 'available' ? 'available' : state === 'taken' ? 'registered' : 'check failed'
|
|
1757
|
+
html += '<div class="suggestion">' +
|
|
1758
|
+
'<div class="suggestion-name">' + candidate + '.sui</div>' +
|
|
1759
|
+
'<div class="suggestion-row">' +
|
|
1760
|
+
'<span class="suggestion-state ' + state + '">' + stateLabel + '</span>' +
|
|
1761
|
+
suggestionAction(candidate, state) +
|
|
1762
|
+
'</div>' +
|
|
1763
|
+
'</div>'
|
|
1764
|
+
}
|
|
1765
|
+
suggestionsGrid.innerHTML = html
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
async function registerName() {
|
|
1769
|
+
if (!IS_REGISTERABLE || !registerBtn) return
|
|
1770
|
+
const address = getConnectedAddress()
|
|
1771
|
+
if (!address) {
|
|
1772
|
+
SuiWalletKit.openModal()
|
|
1773
|
+
return
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
registerBtn.disabled = true
|
|
1777
|
+
hideStatus()
|
|
1778
|
+
showStatus('Building transaction...', 'info')
|
|
1779
|
+
|
|
1780
|
+
try {
|
|
1781
|
+
const years = getSelectedYears()
|
|
1782
|
+
const buildPayload = {
|
|
1783
|
+
domain: NAME,
|
|
1784
|
+
years,
|
|
1785
|
+
sender: address,
|
|
1786
|
+
paymentMode: selectedPaymentMode,
|
|
1787
|
+
referrer: referrerAddress || undefined,
|
|
1788
|
+
waapReferral: waapReferralAddress || undefined,
|
|
1789
|
+
wantsPrimary: wantsPrimaryName,
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
if (selectedPaymentMode === 'coin') {
|
|
1793
|
+
showStatus('Resolving USD route...', 'info')
|
|
1794
|
+
const coinPayment = await resolveRegisterUsdCoinPayment(address, years).catch(() => null)
|
|
1795
|
+
if (coinPayment && coinPayment.sourceCoinType && coinPayment.coinObjectIds.length) {
|
|
1796
|
+
buildPayload.paymentMethod = 'coin'
|
|
1797
|
+
buildPayload.sourceCoinType = coinPayment.sourceCoinType
|
|
1798
|
+
buildPayload.coinObjectIds = coinPayment.coinObjectIds
|
|
1799
|
+
} else {
|
|
1800
|
+
showStatus('Building USD route with live pool balances...', 'info')
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
const buildRes = await fetch('/api/register/build-tx', {
|
|
1805
|
+
method: 'POST',
|
|
1806
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1807
|
+
body: JSON.stringify(buildPayload),
|
|
1808
|
+
})
|
|
1809
|
+
if (!buildRes.ok) {
|
|
1810
|
+
const errBody = await buildRes.json().catch(() => ({}))
|
|
1811
|
+
throw new Error(errBody.error || 'Failed to build transaction')
|
|
1812
|
+
}
|
|
1813
|
+
const buildBody = await buildRes.json()
|
|
1814
|
+
const txBytesBase64 = buildBody?.txBytes
|
|
1815
|
+
const method = typeof buildBody?.method === 'string' ? buildBody.method : ''
|
|
1816
|
+
if (!txBytesBase64 || typeof txBytesBase64 !== 'string') {
|
|
1817
|
+
throw new Error('Build transaction response missing tx bytes')
|
|
1818
|
+
}
|
|
1819
|
+
const raw = atob(txBytesBase64)
|
|
1820
|
+
const txBytes = new Uint8Array(raw.length)
|
|
1821
|
+
for (let i = 0; i < raw.length; i++) txBytes[i] = raw.charCodeAt(i)
|
|
1822
|
+
|
|
1823
|
+
if (method === 'coin-swap') {
|
|
1824
|
+
showStatus('USDC route ready. Approve in ' + getWalletRouteLabel() + '...', 'info')
|
|
1825
|
+
} else {
|
|
1826
|
+
showStatus('Approve in ' + getWalletRouteLabel() + '...', 'info')
|
|
1827
|
+
}
|
|
1828
|
+
const signingChain = NETWORK === 'testnet'
|
|
1829
|
+
? 'sui:testnet'
|
|
1830
|
+
: NETWORK === 'devnet'
|
|
1831
|
+
? 'sui:devnet'
|
|
1832
|
+
: 'sui:mainnet'
|
|
1833
|
+
const result = await SuiWalletKit.signAndExecute(txBytes, {
|
|
1834
|
+
account: { address, chains: [signingChain] },
|
|
1835
|
+
chain: signingChain,
|
|
1836
|
+
txOptions: { showEffects: true, showObjectChanges: true },
|
|
1837
|
+
preferTransactionBlock: true,
|
|
1838
|
+
singleAttempt: true,
|
|
1839
|
+
forceSignBridge: shouldForceSignBridge(),
|
|
1840
|
+
})
|
|
1841
|
+
const digest = result?.digest ? String(result.digest) : ''
|
|
1842
|
+
|
|
1843
|
+
const effectsStatus = result?.effects?.status
|
|
1844
|
+
if (effectsStatus?.status === 'failure') {
|
|
1845
|
+
throw new Error(effectsStatus.error || 'Transaction failed on-chain')
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
let createdNft = false
|
|
1849
|
+
if (result?.effects?.created) {
|
|
1850
|
+
for (const c of result.effects.created) {
|
|
1851
|
+
const ref = c.reference || c
|
|
1852
|
+
if (ref?.objectId) { createdNft = true; break }
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (!createdNft && digest) {
|
|
1856
|
+
showStatus('Verifying registration...', 'info')
|
|
1857
|
+
try {
|
|
1858
|
+
const txCheck = await client.getTransactionBlock({ digest, options: { showEffects: true, showObjectChanges: true } })
|
|
1859
|
+
if (txCheck?.effects?.status?.status === 'failure') {
|
|
1860
|
+
throw new Error(txCheck.effects.status.error || 'Transaction failed on-chain')
|
|
1861
|
+
}
|
|
1862
|
+
if (txCheck?.objectChanges) {
|
|
1863
|
+
for (const oc of txCheck.objectChanges) {
|
|
1864
|
+
if (oc.type === 'created') { createdNft = true; break }
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
} catch (verifyErr) {
|
|
1868
|
+
if (verifyErr?.message?.includes('failed on-chain')) throw verifyErr
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
if (!createdNft) {
|
|
1873
|
+
throw new Error('Registration transaction did not create a SuiNS NFT. Check your balance and try again.')
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
trackEvent('sui_ski_register_success', {
|
|
1877
|
+
registerFlow: REGISTER_FLOW,
|
|
1878
|
+
registerBucket: REGISTER_BUCKET,
|
|
1879
|
+
registerName: NAME,
|
|
1880
|
+
registerNetwork: NETWORK,
|
|
1881
|
+
txDigest: digest,
|
|
1882
|
+
method: method || 'unknown',
|
|
1883
|
+
referrer: referrerAddress || '',
|
|
1884
|
+
})
|
|
1885
|
+
|
|
1886
|
+
const profileUrl = 'https://' + NAME + '.sui.ski?nocache'
|
|
1887
|
+
const links = digest
|
|
1888
|
+
? '<a href="https://suiscan.xyz/' + NETWORK + '/tx/' + encodeURIComponent(digest) + '" target="_blank" rel="noopener noreferrer">Suiscan</a> · ' +
|
|
1889
|
+
'<a href="https://suiexplorer.com/txblock/' + encodeURIComponent(digest) + '?network=' + NETWORK + '" target="_blank" rel="noopener noreferrer">Explorer</a>'
|
|
1890
|
+
: ''
|
|
1891
|
+
showStatus(
|
|
1892
|
+
'<strong>Registered!</strong> Redirecting to profile...' +
|
|
1893
|
+
(digest ? ' · ' + links : ''),
|
|
1894
|
+
'ok',
|
|
1895
|
+
true,
|
|
1896
|
+
)
|
|
1897
|
+
if (wantsPrimaryName && typeof SuiWalletKit.setPrimaryName === 'function') {
|
|
1898
|
+
SuiWalletKit.setPrimaryName(NAME)
|
|
1899
|
+
}
|
|
1900
|
+
registerBtn.textContent = 'Registered'
|
|
1901
|
+
registerBtn.disabled = true
|
|
1902
|
+
|
|
1903
|
+
setTimeout(() => { window.location.href = profileUrl }, 2000)
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
const msg = error && error.message ? error.message : 'Registration failed'
|
|
1906
|
+
trackEvent('sui_ski_register_error', {
|
|
1907
|
+
registerFlow: REGISTER_FLOW,
|
|
1908
|
+
registerBucket: REGISTER_BUCKET,
|
|
1909
|
+
registerName: NAME,
|
|
1910
|
+
registerNetwork: NETWORK,
|
|
1911
|
+
error: String(msg),
|
|
1912
|
+
})
|
|
1913
|
+
const normalizedMsg = String(msg).toLowerCase()
|
|
1914
|
+
if (normalizedMsg.includes('no usable usdc balance was found for this wallet')) {
|
|
1915
|
+
showStatus(
|
|
1916
|
+
'No usable USDC balance was found for this wallet. <a href="https://sui.ski" style="color:#b8ffda;text-decoration:underline;">Open .ski</a> and swap to SUI, then try again.',
|
|
1917
|
+
'err',
|
|
1918
|
+
true,
|
|
1919
|
+
)
|
|
1920
|
+
} else {
|
|
1921
|
+
showStatus(msg, 'err')
|
|
1922
|
+
}
|
|
1923
|
+
registerBtn.disabled = false
|
|
1924
|
+
updateRegisterButton()
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
window.onRegisterWalletConnected = function() {
|
|
1929
|
+
updateRegisterButton()
|
|
1930
|
+
updateWalletProfileButton()
|
|
1931
|
+
syncPrimaryStarState()
|
|
1932
|
+
updateCardWalletInfo()
|
|
1933
|
+
renderNftQr()
|
|
1934
|
+
checkAutoPaymentRoute()
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
window.onRegisterWalletDisconnected = function() {
|
|
1938
|
+
selectedPaymentMode = 'auto'
|
|
1939
|
+
paymentModeManualOverride = false
|
|
1940
|
+
updateRegisterButton()
|
|
1941
|
+
updateWalletProfileButton()
|
|
1942
|
+
syncPrimaryStarState()
|
|
1943
|
+
updateCardWalletInfo()
|
|
1944
|
+
renderNftQr()
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
${generateSharedWalletMountJs({
|
|
1948
|
+
network: env.SUI_NETWORK,
|
|
1949
|
+
session,
|
|
1950
|
+
onConnect: 'onRegisterWalletConnected',
|
|
1951
|
+
onDisconnect: 'onRegisterWalletDisconnected',
|
|
1952
|
+
profileButtonId: 'wallet-profile-btn',
|
|
1953
|
+
profileFallbackHref: 'https://sui.ski',
|
|
1954
|
+
})}
|
|
1955
|
+
|
|
1956
|
+
;(function fitNameSize() {
|
|
1957
|
+
const nameEl = document.querySelector('.nft-name')
|
|
1958
|
+
if (!nameEl) return
|
|
1959
|
+
const fullName = NAME + '.sui'
|
|
1960
|
+
const len = fullName.length
|
|
1961
|
+
if (len > 12) {
|
|
1962
|
+
nameEl.style.fontSize = 'clamp(1.4rem, 6vw, 2rem)'
|
|
1963
|
+
} else if (len > 9) {
|
|
1964
|
+
nameEl.style.fontSize = 'clamp(1.7rem, 7.5vw, 2.4rem)'
|
|
1965
|
+
} else if (len > 7) {
|
|
1966
|
+
nameEl.style.fontSize = 'clamp(2rem, 8.5vw, 2.8rem)'
|
|
1967
|
+
}
|
|
1968
|
+
})()
|
|
1969
|
+
|
|
1970
|
+
markRegisterFlowImpression()
|
|
1971
|
+
updatePrimaryStarUi()
|
|
1972
|
+
syncPrimaryStarState()
|
|
1973
|
+
setSelectedYears(getSelectedYears())
|
|
1974
|
+
updatePaymentChoiceUi()
|
|
1975
|
+
updateRegisterButton()
|
|
1976
|
+
fetchPricing()
|
|
1977
|
+
updateSuiPrice()
|
|
1978
|
+
updateX402Listing()
|
|
1979
|
+
updateCardWalletInfo()
|
|
1980
|
+
setInterval(updateSuiPrice, 60000)
|
|
1981
|
+
setInterval(updateX402Listing, 120000)
|
|
1982
|
+
loadSuggestions()
|
|
1983
|
+
|
|
1984
|
+
let __qrCreatorMod = null
|
|
1985
|
+
async function renderNftQr() {
|
|
1986
|
+
const canvas = document.getElementById('nft-qr')
|
|
1987
|
+
if (!canvas || !canvas.getContext) return
|
|
1988
|
+
try {
|
|
1989
|
+
if (!__qrCreatorMod) __qrCreatorMod = await import('https://esm.sh/qr-creator@1.0.0')
|
|
1990
|
+
const QrCreator = __qrCreatorMod.default || __qrCreatorMod
|
|
1991
|
+
const qrUrl = 'https://' + NAME + '.sui.ski'
|
|
1992
|
+
const ctx = canvas.getContext('2d')
|
|
1993
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
1994
|
+
QrCreator.render({
|
|
1995
|
+
text: qrUrl,
|
|
1996
|
+
radius: 0.4,
|
|
1997
|
+
ecLevel: 'M',
|
|
1998
|
+
fill: '#49da91',
|
|
1999
|
+
background: 'transparent',
|
|
2000
|
+
size: 200,
|
|
2001
|
+
}, canvas)
|
|
2002
|
+
} catch {
|
|
2003
|
+
canvas.style.display = 'none'
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
renderNftQr()
|
|
2007
|
+
|
|
2008
|
+
if (yearsDecreaseBtn) {
|
|
2009
|
+
yearsDecreaseBtn.addEventListener('click', () => {
|
|
2010
|
+
setSelectedYears(getSelectedYears() - 1)
|
|
2011
|
+
fetchPricing()
|
|
2012
|
+
})
|
|
2013
|
+
}
|
|
2014
|
+
if (yearsIncreaseBtn) {
|
|
2015
|
+
yearsIncreaseBtn.addEventListener('click', () => {
|
|
2016
|
+
setSelectedYears(getSelectedYears() + 1)
|
|
2017
|
+
fetchPricing()
|
|
2018
|
+
})
|
|
2019
|
+
}
|
|
2020
|
+
if (walletProfileBtn && !walletProfileBtn.dataset.registerBound) {
|
|
2021
|
+
walletProfileBtn.dataset.registerBound = '1'
|
|
2022
|
+
walletProfileBtn.addEventListener('click', (e) => {
|
|
2023
|
+
e.stopPropagation()
|
|
2024
|
+
window.location.href = walletProfileBtn.dataset.href || 'https://sui.ski'
|
|
2025
|
+
})
|
|
2026
|
+
}
|
|
2027
|
+
if (primaryStarEl) {
|
|
2028
|
+
primaryStarEl.addEventListener('click', () => {
|
|
2029
|
+
wantsPrimaryName = !wantsPrimaryName
|
|
2030
|
+
primaryStarManualOverride = wantsPrimaryName
|
|
2031
|
+
updatePrimaryStarUi()
|
|
2032
|
+
})
|
|
2033
|
+
}
|
|
2034
|
+
document.addEventListener('keydown', (e) => {
|
|
2035
|
+
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) return
|
|
2036
|
+
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
|
|
2037
|
+
e.preventDefault()
|
|
2038
|
+
setSelectedYears(getSelectedYears() + 1)
|
|
2039
|
+
fetchPricing()
|
|
2040
|
+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
|
|
2041
|
+
e.preventDefault()
|
|
2042
|
+
setSelectedYears(getSelectedYears() - 1)
|
|
2043
|
+
fetchPricing()
|
|
2044
|
+
}
|
|
2045
|
+
})
|
|
2046
|
+
if (refreshSuggestionsBtn) refreshSuggestionsBtn.addEventListener('click', () => loadSuggestions(true))
|
|
2047
|
+
function getPaymentModeFromEventTarget(target) {
|
|
2048
|
+
if (!target) return ''
|
|
2049
|
+
const element = typeof target.closest === 'function' ? target : target.parentElement
|
|
2050
|
+
if (!element || typeof element.closest !== 'function') return ''
|
|
2051
|
+
const trigger = element.closest('[data-payment-mode]')
|
|
2052
|
+
if (!trigger || !priceValue || !priceValue.contains(trigger)) return ''
|
|
2053
|
+
const mode = trigger.getAttribute('data-payment-mode')
|
|
2054
|
+
return mode === 'coin' || mode === 'sui' ? mode : ''
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
if (priceValue) {
|
|
2058
|
+
priceValue.addEventListener('click', function(event) {
|
|
2059
|
+
const mode = getPaymentModeFromEventTarget(event.target)
|
|
2060
|
+
if (mode) {
|
|
2061
|
+
paymentModeManualOverride = true
|
|
2062
|
+
setSelectedPaymentMode(mode)
|
|
2063
|
+
}
|
|
2064
|
+
})
|
|
2065
|
+
priceValue.addEventListener('keydown', function(event) {
|
|
2066
|
+
const key = event && event.key ? event.key : ''
|
|
2067
|
+
if (key !== 'Enter' && key !== ' ') return
|
|
2068
|
+
const mode = getPaymentModeFromEventTarget(event.target)
|
|
2069
|
+
if (!mode) return
|
|
2070
|
+
event.preventDefault()
|
|
2071
|
+
paymentModeManualOverride = true
|
|
2072
|
+
setSelectedPaymentMode(mode)
|
|
2073
|
+
})
|
|
2074
|
+
}
|
|
2075
|
+
if (registerBtn) registerBtn.addEventListener('click', registerName)
|
|
2076
|
+
let __html2canvasMod = null
|
|
2077
|
+
if (downloadQrBtn) {
|
|
2078
|
+
downloadQrBtn.addEventListener('click', async function() {
|
|
2079
|
+
const card = document.querySelector('body > div.container > div > div.nft-card')
|
|
2080
|
+
if (!card) return
|
|
2081
|
+
try {
|
|
2082
|
+
if (!__html2canvasMod) __html2canvasMod = await import('https://esm.sh/html2canvas@1.4.1')
|
|
2083
|
+
const html2canvas = __html2canvasMod.default || __html2canvasMod
|
|
2084
|
+
const canvas = await html2canvas(card, {
|
|
2085
|
+
backgroundColor: '#040d09',
|
|
2086
|
+
scale: 2,
|
|
2087
|
+
useCORS: true,
|
|
2088
|
+
logging: false,
|
|
2089
|
+
})
|
|
2090
|
+
const link = document.createElement('a')
|
|
2091
|
+
link.download = NAME + '-sui-ski.png'
|
|
2092
|
+
link.href = canvas.toDataURL('image/png')
|
|
2093
|
+
link.click()
|
|
2094
|
+
} catch {}
|
|
2095
|
+
})
|
|
2096
|
+
}
|
|
2097
|
+
</script>
|
|
2098
|
+
</body>
|
|
2099
|
+
</html>`
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function parseMistAmount(value: unknown): bigint {
|
|
2103
|
+
if (typeof value === 'bigint') return value
|
|
2104
|
+
if (typeof value === 'number' && Number.isFinite(value)) return BigInt(Math.floor(value))
|
|
2105
|
+
if (typeof value === 'string' && value.trim()) {
|
|
2106
|
+
try {
|
|
2107
|
+
return BigInt(value)
|
|
2108
|
+
} catch {
|
|
2109
|
+
return 0n
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return 0n
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
function createRpcClient(env: Env): SuiJsonRpcClient {
|
|
2116
|
+
return new SuiJsonRpcClient({
|
|
2117
|
+
url: env.SUI_RPC_URL || getDefaultRpcUrl(env.SUI_NETWORK),
|
|
2118
|
+
network: env.SUI_NETWORK,
|
|
2119
|
+
})
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
async function getRpcClientWithFallback(env: Env, probeAddress: string): Promise<SuiJsonRpcClient> {
|
|
2123
|
+
const primaryClient = createRpcClient(env)
|
|
2124
|
+
try {
|
|
2125
|
+
await primaryClient.getBalance({ owner: probeAddress, coinType: SUI_COIN_TYPE })
|
|
2126
|
+
return primaryClient
|
|
2127
|
+
} catch (primaryError) {
|
|
2128
|
+
const fallbackUrl = getDefaultRpcUrl(env.SUI_NETWORK)
|
|
2129
|
+
const configuredUrl = (env.SUI_RPC_URL || '').trim()
|
|
2130
|
+
if (!configuredUrl || configuredUrl === fallbackUrl) throw primaryError
|
|
2131
|
+
const fallbackClient = new SuiJsonRpcClient({
|
|
2132
|
+
url: fallbackUrl,
|
|
2133
|
+
network: env.SUI_NETWORK,
|
|
2134
|
+
})
|
|
2135
|
+
await fallbackClient.getBalance({ owner: probeAddress, coinType: SUI_COIN_TYPE })
|
|
2136
|
+
return fallbackClient
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
async function collectCoinObjectIdsForAmount(
|
|
2141
|
+
client: SuiJsonRpcClient,
|
|
2142
|
+
owner: string,
|
|
2143
|
+
coinType: string,
|
|
2144
|
+
targetAmount: bigint,
|
|
2145
|
+
): Promise<{ coinObjectIds: string[]; total: bigint }> {
|
|
2146
|
+
const coinObjectIds: string[] = []
|
|
2147
|
+
let total = 0n
|
|
2148
|
+
let cursor: string | null = null
|
|
2149
|
+
for (let i = 0; i < REGISTER_PAYMENT_COIN_PAGE_MAX; i++) {
|
|
2150
|
+
const page = await client.getCoins({
|
|
2151
|
+
owner,
|
|
2152
|
+
coinType,
|
|
2153
|
+
cursor: cursor || undefined,
|
|
2154
|
+
limit: REGISTER_PAYMENT_COIN_PAGE_LIMIT,
|
|
2155
|
+
})
|
|
2156
|
+
const rows = Array.isArray(page?.data) ? page.data : []
|
|
2157
|
+
for (const coin of rows) {
|
|
2158
|
+
if (!coin || typeof coin.coinObjectId !== 'string') continue
|
|
2159
|
+
const coinBalance = parseMistAmount(coin.balance)
|
|
2160
|
+
if (coinBalance <= 0n) continue
|
|
2161
|
+
coinObjectIds.push(coin.coinObjectId)
|
|
2162
|
+
total += coinBalance
|
|
2163
|
+
if (total >= targetAmount) return { coinObjectIds, total }
|
|
2164
|
+
}
|
|
2165
|
+
if (!page?.hasNextPage || !page?.nextCursor) break
|
|
2166
|
+
cursor = page.nextCursor
|
|
2167
|
+
}
|
|
2168
|
+
return { coinObjectIds, total }
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function stableSymbolRank(name: string): number {
|
|
2172
|
+
const symbol = String(name || '').toUpperCase()
|
|
2173
|
+
for (let i = 0; i < STABLE_SYMBOL_PRIORITY.length; i++) {
|
|
2174
|
+
if (symbol === STABLE_SYMBOL_PRIORITY[i]) return i
|
|
2175
|
+
}
|
|
2176
|
+
return 999
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function isStableSymbol(name: string): boolean {
|
|
2180
|
+
const symbol = String(name || '').toUpperCase()
|
|
2181
|
+
if (!symbol) return false
|
|
2182
|
+
return symbol.includes('USD') || stableSymbolRank(symbol) !== 999
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function estimateTokenMistNeeded(
|
|
2186
|
+
requiredSuiMist: bigint,
|
|
2187
|
+
suiPerToken: number,
|
|
2188
|
+
decimals: number,
|
|
2189
|
+
): bigint {
|
|
2190
|
+
if (!Number.isFinite(suiPerToken) || suiPerToken <= 0) return 0n
|
|
2191
|
+
const suiNeeded = Number(requiredSuiMist) / 1e9
|
|
2192
|
+
if (!Number.isFinite(suiNeeded) || suiNeeded <= 0) return 0n
|
|
2193
|
+
const tokensNeeded = suiNeeded / suiPerToken
|
|
2194
|
+
if (!Number.isFinite(tokensNeeded) || tokensNeeded <= 0) return 0n
|
|
2195
|
+
const bufferedTokens = tokensNeeded * 1.12
|
|
2196
|
+
const scaled = Math.ceil(bufferedTokens * 10 ** decimals)
|
|
2197
|
+
if (!Number.isFinite(scaled) || scaled <= 0) return 0n
|
|
2198
|
+
return BigInt(scaled)
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
async function resolveCoinPayment(
|
|
2202
|
+
env: Env,
|
|
2203
|
+
sender: string,
|
|
2204
|
+
domain: string,
|
|
2205
|
+
years: number,
|
|
2206
|
+
forceCoin = false,
|
|
2207
|
+
usdcOnly = false,
|
|
2208
|
+
): Promise<{ sourceCoinType: string; coinObjectIds: string[] } | null> {
|
|
2209
|
+
const pricing = await calculateRegistrationPrice({ domain, years, env })
|
|
2210
|
+
const requiredSuiMist =
|
|
2211
|
+
pricing.discountedSuiMist > 0n ? pricing.discountedSuiMist : pricing.directSuiMist
|
|
2212
|
+
if (requiredSuiMist <= 0n) return null
|
|
2213
|
+
|
|
2214
|
+
const client = await getRpcClientWithFallback(env, sender)
|
|
2215
|
+
const [suiBalance, pools] = await Promise.all([
|
|
2216
|
+
client.getBalance({ owner: sender, coinType: SUI_COIN_TYPE }),
|
|
2217
|
+
getDeepBookSuiPools(env),
|
|
2218
|
+
])
|
|
2219
|
+
|
|
2220
|
+
const totalSuiMist = parseMistAmount(suiBalance?.totalBalance)
|
|
2221
|
+
const swapOverheadMist = (requiredSuiMist * 50n) / 100n + REGISTER_GAS_BUFFER_MIST
|
|
2222
|
+
if (!forceCoin && totalSuiMist >= requiredSuiMist + swapOverheadMist) return null
|
|
2223
|
+
|
|
2224
|
+
let usdcRequiredMist = 0n
|
|
2225
|
+
for (const pool of pools) {
|
|
2226
|
+
if (!pool || pool.coinType !== USDC_COIN_TYPE) continue
|
|
2227
|
+
const estimate = estimateTokenMistNeeded(requiredSuiMist, pool.suiPerToken, pool.decimals)
|
|
2228
|
+
if (estimate <= 0n) continue
|
|
2229
|
+
if (usdcRequiredMist === 0n || estimate < usdcRequiredMist) {
|
|
2230
|
+
usdcRequiredMist = estimate
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (usdcRequiredMist <= 0n) {
|
|
2234
|
+
const discountedUsd = Number(pricing.breakdown?.discountedPriceUsd || 0)
|
|
2235
|
+
if (Number.isFinite(discountedUsd) && discountedUsd > 0) {
|
|
2236
|
+
usdcRequiredMist = BigInt(Math.ceil(discountedUsd * 1_000_000 * 1.12))
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
if (usdcRequiredMist > 0n) {
|
|
2241
|
+
const usdcBalance = await client
|
|
2242
|
+
.getBalance({ owner: sender, coinType: USDC_COIN_TYPE })
|
|
2243
|
+
.catch(() => ({ totalBalance: '0' }))
|
|
2244
|
+
const totalUsdcMist = parseMistAmount(usdcBalance?.totalBalance)
|
|
2245
|
+
if (usdcOnly && totalUsdcMist > 0n) {
|
|
2246
|
+
try {
|
|
2247
|
+
const allUsdcCoins = await collectCoinObjectIdsForAmount(
|
|
2248
|
+
client,
|
|
2249
|
+
sender,
|
|
2250
|
+
USDC_COIN_TYPE,
|
|
2251
|
+
totalUsdcMist,
|
|
2252
|
+
)
|
|
2253
|
+
if (allUsdcCoins.coinObjectIds.length > 0) {
|
|
2254
|
+
return {
|
|
2255
|
+
sourceCoinType: USDC_COIN_TYPE,
|
|
2256
|
+
coinObjectIds: allUsdcCoins.coinObjectIds,
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
} catch {
|
|
2260
|
+
// Continue to estimated-amount selection.
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
try {
|
|
2264
|
+
const usdcCoins = await collectCoinObjectIdsForAmount(
|
|
2265
|
+
client,
|
|
2266
|
+
sender,
|
|
2267
|
+
USDC_COIN_TYPE,
|
|
2268
|
+
usdcRequiredMist,
|
|
2269
|
+
)
|
|
2270
|
+
if (usdcOnly && usdcCoins.coinObjectIds.length > 0) {
|
|
2271
|
+
return {
|
|
2272
|
+
sourceCoinType: USDC_COIN_TYPE,
|
|
2273
|
+
coinObjectIds: usdcCoins.coinObjectIds,
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (
|
|
2277
|
+
totalUsdcMist >= usdcRequiredMist &&
|
|
2278
|
+
usdcCoins.total >= usdcRequiredMist &&
|
|
2279
|
+
usdcCoins.coinObjectIds.length > 0
|
|
2280
|
+
) {
|
|
2281
|
+
return {
|
|
2282
|
+
sourceCoinType: USDC_COIN_TYPE,
|
|
2283
|
+
coinObjectIds: usdcCoins.coinObjectIds,
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
} catch {
|
|
2287
|
+
// Fall through to generic pool selection.
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (usdcOnly) {
|
|
2291
|
+
const usdcBalance = await client
|
|
2292
|
+
.getBalance({ owner: sender, coinType: USDC_COIN_TYPE })
|
|
2293
|
+
.catch(() => ({ totalBalance: '0' }))
|
|
2294
|
+
const totalUsdcMist = parseMistAmount(usdcBalance?.totalBalance)
|
|
2295
|
+
if (totalUsdcMist <= 0n) return null
|
|
2296
|
+
const allUsdcCoins = await collectCoinObjectIdsForAmount(
|
|
2297
|
+
client,
|
|
2298
|
+
sender,
|
|
2299
|
+
USDC_COIN_TYPE,
|
|
2300
|
+
totalUsdcMist,
|
|
2301
|
+
).catch(() => ({ coinObjectIds: [] as string[], total: 0n }))
|
|
2302
|
+
if (allUsdcCoins.coinObjectIds.length === 0) return null
|
|
2303
|
+
return {
|
|
2304
|
+
sourceCoinType: USDC_COIN_TYPE,
|
|
2305
|
+
coinObjectIds: allUsdcCoins.coinObjectIds,
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
const poolByCoin = new Map<string, (typeof pools)[number]>()
|
|
2310
|
+
for (const pool of pools) {
|
|
2311
|
+
if (!pool || !pool.coinType) continue
|
|
2312
|
+
if (pool.coinType === SUI_COIN_TYPE || pool.coinType === NS_COIN_TYPE) continue
|
|
2313
|
+
const existing = poolByCoin.get(pool.coinType)
|
|
2314
|
+
if (!existing) {
|
|
2315
|
+
poolByCoin.set(pool.coinType, pool)
|
|
2316
|
+
continue
|
|
2317
|
+
}
|
|
2318
|
+
if (!existing.isDirect && pool.isDirect) {
|
|
2319
|
+
poolByCoin.set(pool.coinType, pool)
|
|
2320
|
+
continue
|
|
2321
|
+
}
|
|
2322
|
+
if (pool.suiPerToken > existing.suiPerToken) {
|
|
2323
|
+
poolByCoin.set(pool.coinType, pool)
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
const uniquePools = Array.from(poolByCoin.values())
|
|
2328
|
+
if (!uniquePools.length) return null
|
|
2329
|
+
|
|
2330
|
+
const balances = await Promise.all(
|
|
2331
|
+
uniquePools.map((pool) =>
|
|
2332
|
+
client
|
|
2333
|
+
.getBalance({ owner: sender, coinType: pool.coinType })
|
|
2334
|
+
.catch(() => ({ totalBalance: '0' })),
|
|
2335
|
+
),
|
|
2336
|
+
)
|
|
2337
|
+
|
|
2338
|
+
const candidates: Array<{
|
|
2339
|
+
sourceCoinType: string
|
|
2340
|
+
coinObjectIds: string[]
|
|
2341
|
+
isUsdc: boolean
|
|
2342
|
+
isStable: boolean
|
|
2343
|
+
stableRank: number
|
|
2344
|
+
isDirect: boolean
|
|
2345
|
+
}> = []
|
|
2346
|
+
|
|
2347
|
+
for (let i = 0; i < uniquePools.length; i++) {
|
|
2348
|
+
const pool = uniquePools[i]
|
|
2349
|
+
const totalBalance = parseMistAmount(balances[i]?.totalBalance)
|
|
2350
|
+
if (totalBalance <= 0n) continue
|
|
2351
|
+
|
|
2352
|
+
const requiredTokenMist = estimateTokenMistNeeded(
|
|
2353
|
+
requiredSuiMist,
|
|
2354
|
+
pool.suiPerToken,
|
|
2355
|
+
pool.decimals,
|
|
2356
|
+
)
|
|
2357
|
+
if (requiredTokenMist <= 0n || totalBalance < requiredTokenMist) continue
|
|
2358
|
+
|
|
2359
|
+
const selected = await collectCoinObjectIdsForAmount(
|
|
2360
|
+
client,
|
|
2361
|
+
sender,
|
|
2362
|
+
pool.coinType,
|
|
2363
|
+
requiredTokenMist,
|
|
2364
|
+
).catch(() => ({ coinObjectIds: [] as string[], total: 0n }))
|
|
2365
|
+
if (selected.total < requiredTokenMist || selected.coinObjectIds.length === 0) continue
|
|
2366
|
+
|
|
2367
|
+
const rank = stableSymbolRank(pool.name)
|
|
2368
|
+
candidates.push({
|
|
2369
|
+
sourceCoinType: pool.coinType,
|
|
2370
|
+
coinObjectIds: selected.coinObjectIds,
|
|
2371
|
+
isUsdc: pool.coinType === USDC_COIN_TYPE,
|
|
2372
|
+
isStable: isStableSymbol(pool.name),
|
|
2373
|
+
stableRank: rank,
|
|
2374
|
+
isDirect: !!pool.isDirect,
|
|
2375
|
+
})
|
|
2376
|
+
}
|
|
2377
|
+
if (!candidates.length) return null
|
|
2378
|
+
|
|
2379
|
+
candidates.sort((a, b) => {
|
|
2380
|
+
if (a.isUsdc && !b.isUsdc) return -1
|
|
2381
|
+
if (!a.isUsdc && b.isUsdc) return 1
|
|
2382
|
+
if (a.isStable && !b.isStable) return -1
|
|
2383
|
+
if (!a.isStable && b.isStable) return 1
|
|
2384
|
+
if (a.stableRank !== b.stableRank) return a.stableRank - b.stableRank
|
|
2385
|
+
if (a.isDirect && !b.isDirect) return -1
|
|
2386
|
+
if (!a.isDirect && b.isDirect) return 1
|
|
2387
|
+
return 0
|
|
2388
|
+
})
|
|
2389
|
+
|
|
2390
|
+
return {
|
|
2391
|
+
sourceCoinType: candidates[0].sourceCoinType,
|
|
2392
|
+
coinObjectIds: candidates[0].coinObjectIds,
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
export async function handleBuildRegisterTx(request: Request, env: Env): Promise<Response> {
|
|
2397
|
+
if (request.method === 'OPTIONS') {
|
|
2398
|
+
return new Response(null, { headers: CORS_HEADERS })
|
|
2399
|
+
}
|
|
2400
|
+
if (request.method !== 'POST') {
|
|
2401
|
+
return jsonResponse({ error: 'Method not allowed' }, 405, CORS_HEADERS)
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
let body: {
|
|
2405
|
+
domain?: string
|
|
2406
|
+
years?: number
|
|
2407
|
+
sender?: string
|
|
2408
|
+
referrer?: string
|
|
2409
|
+
waapReferral?: string
|
|
2410
|
+
wantsPrimary?: boolean
|
|
2411
|
+
paymentMode?: 'auto' | 'coin' | 'sui'
|
|
2412
|
+
paymentMethod?: 'coin' | 'ns'
|
|
2413
|
+
sourceCoinType?: string
|
|
2414
|
+
coinObjectIds?: string[]
|
|
2415
|
+
}
|
|
2416
|
+
try {
|
|
2417
|
+
body = (await request.json()) as typeof body
|
|
2418
|
+
} catch {
|
|
2419
|
+
return jsonResponse({ error: 'Invalid JSON body' }, 400, CORS_HEADERS)
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
const domain =
|
|
2423
|
+
typeof body.domain === 'string'
|
|
2424
|
+
? body.domain
|
|
2425
|
+
.trim()
|
|
2426
|
+
.toLowerCase()
|
|
2427
|
+
.replace(/\.sui$/i, '')
|
|
2428
|
+
: ''
|
|
2429
|
+
const years =
|
|
2430
|
+
typeof body.years === 'number' && Number.isFinite(body.years)
|
|
2431
|
+
? Math.max(1, Math.min(5, Math.floor(body.years)))
|
|
2432
|
+
: 1
|
|
2433
|
+
const sender = typeof body.sender === 'string' ? body.sender.trim() : ''
|
|
2434
|
+
const referrer =
|
|
2435
|
+
typeof body.referrer === 'string' && /^0x[0-9a-fA-F]{64}$/.test(body.referrer)
|
|
2436
|
+
? body.referrer
|
|
2437
|
+
: null
|
|
2438
|
+
const waapReferral =
|
|
2439
|
+
typeof body.waapReferral === 'string' && /^0x[0-9a-fA-F]{64}$/.test(body.waapReferral)
|
|
2440
|
+
? body.waapReferral
|
|
2441
|
+
: null
|
|
2442
|
+
const wantsPrimary = body.wantsPrimary === true
|
|
2443
|
+
const paymentMode =
|
|
2444
|
+
body.paymentMode === 'coin' || body.paymentMode === 'sui' ? body.paymentMode : 'auto'
|
|
2445
|
+
const paymentMethod = body.paymentMethod === 'coin' ? 'coin' : 'ns'
|
|
2446
|
+
const sourceCoinType = typeof body.sourceCoinType === 'string' ? body.sourceCoinType.trim() : ''
|
|
2447
|
+
const coinObjectIds = Array.isArray(body.coinObjectIds)
|
|
2448
|
+
? body.coinObjectIds.filter(
|
|
2449
|
+
(id): id is string => typeof id === 'string' && /^0x[0-9a-fA-F]+$/.test(id),
|
|
2450
|
+
)
|
|
2451
|
+
: []
|
|
2452
|
+
|
|
2453
|
+
if (!domain || domain.length < 3) {
|
|
2454
|
+
return jsonResponse({ error: 'Invalid domain (minimum 3 characters)' }, 400, CORS_HEADERS)
|
|
2455
|
+
}
|
|
2456
|
+
if (!sender || !/^0x[0-9a-fA-F]{64}$/.test(sender)) {
|
|
2457
|
+
return jsonResponse({ error: 'Invalid sender address' }, 400, CORS_HEADERS)
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
const fullDomain = `${domain}.sui`
|
|
2461
|
+
|
|
2462
|
+
try {
|
|
2463
|
+
let tx: Transaction | null = null
|
|
2464
|
+
let method: 'ns-swap' | 'sui-direct' | 'coin-swap' | null = null
|
|
2465
|
+
let breakdownInfo: Record<string, unknown> = {}
|
|
2466
|
+
let selectedPaymentMethod: 'coin' | 'ns' =
|
|
2467
|
+
paymentMode === 'coin' ? 'coin' : paymentMode === 'sui' ? 'ns' : paymentMethod
|
|
2468
|
+
let selectedSourceCoinType = sourceCoinType
|
|
2469
|
+
let selectedCoinObjectIds = coinObjectIds
|
|
2470
|
+
|
|
2471
|
+
let feePricing: { savingsMist: bigint } | null = null
|
|
2472
|
+
let extraSuiForFeesMist = 0n
|
|
2473
|
+
if (referrer || waapReferral) {
|
|
2474
|
+
try {
|
|
2475
|
+
feePricing = await calculateRegistrationPrice({ domain: fullDomain, years, env })
|
|
2476
|
+
if (feePricing?.savingsMist > 0n) {
|
|
2477
|
+
if (referrer) extraSuiForFeesMist += (feePricing.savingsMist * 10n) / 100n
|
|
2478
|
+
if (waapReferral) extraSuiForFeesMist += (feePricing.savingsMist * 5n) / 100n
|
|
2479
|
+
}
|
|
2480
|
+
} catch {
|
|
2481
|
+
// Ignore pricing errors; fees will be skipped if pricing unavailable
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
if (selectedPaymentMethod !== 'coin' && paymentMode === 'auto') {
|
|
2486
|
+
try {
|
|
2487
|
+
const coinPayment = await resolveCoinPayment(env, sender, fullDomain, years)
|
|
2488
|
+
if (coinPayment) {
|
|
2489
|
+
selectedPaymentMethod = 'coin'
|
|
2490
|
+
selectedSourceCoinType = coinPayment.sourceCoinType
|
|
2491
|
+
selectedCoinObjectIds = coinPayment.coinObjectIds
|
|
2492
|
+
}
|
|
2493
|
+
} catch {
|
|
2494
|
+
// Fall back to NS/SUI paths when automatic coin payment resolution is unavailable.
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
if (selectedPaymentMethod === 'coin') {
|
|
2499
|
+
if (
|
|
2500
|
+
paymentMode === 'coin' &&
|
|
2501
|
+
selectedSourceCoinType &&
|
|
2502
|
+
selectedSourceCoinType !== USDC_COIN_TYPE
|
|
2503
|
+
) {
|
|
2504
|
+
return jsonResponse(
|
|
2505
|
+
{
|
|
2506
|
+
error:
|
|
2507
|
+
'USD mode currently supports USDC only. Click the SUI price to build a SUI transaction.',
|
|
2508
|
+
},
|
|
2509
|
+
400,
|
|
2510
|
+
CORS_HEADERS,
|
|
2511
|
+
)
|
|
2512
|
+
}
|
|
2513
|
+
if (paymentMode === 'coin') {
|
|
2514
|
+
try {
|
|
2515
|
+
const freshUsdcPayment = await resolveCoinPayment(
|
|
2516
|
+
env,
|
|
2517
|
+
sender,
|
|
2518
|
+
fullDomain,
|
|
2519
|
+
years,
|
|
2520
|
+
true,
|
|
2521
|
+
true,
|
|
2522
|
+
)
|
|
2523
|
+
if (freshUsdcPayment) {
|
|
2524
|
+
selectedSourceCoinType = freshUsdcPayment.sourceCoinType
|
|
2525
|
+
selectedCoinObjectIds = freshUsdcPayment.coinObjectIds
|
|
2526
|
+
}
|
|
2527
|
+
} catch {}
|
|
2528
|
+
}
|
|
2529
|
+
if (!selectedSourceCoinType || selectedCoinObjectIds.length === 0) {
|
|
2530
|
+
try {
|
|
2531
|
+
const coinPayment = await resolveCoinPayment(
|
|
2532
|
+
env,
|
|
2533
|
+
sender,
|
|
2534
|
+
fullDomain,
|
|
2535
|
+
years,
|
|
2536
|
+
true,
|
|
2537
|
+
paymentMode === 'coin',
|
|
2538
|
+
)
|
|
2539
|
+
if (coinPayment) {
|
|
2540
|
+
selectedSourceCoinType = coinPayment.sourceCoinType
|
|
2541
|
+
selectedCoinObjectIds = coinPayment.coinObjectIds
|
|
2542
|
+
}
|
|
2543
|
+
} catch {}
|
|
2544
|
+
}
|
|
2545
|
+
if (!selectedSourceCoinType || selectedCoinObjectIds.length === 0) {
|
|
2546
|
+
return jsonResponse(
|
|
2547
|
+
{
|
|
2548
|
+
error:
|
|
2549
|
+
paymentMode === 'coin'
|
|
2550
|
+
? 'No usable USDC balance was found for this wallet. Click the SUI price to build a SUI transaction.'
|
|
2551
|
+
: 'paymentMethod "coin" requires sourceCoinType and coinObjectIds',
|
|
2552
|
+
},
|
|
2553
|
+
400,
|
|
2554
|
+
CORS_HEADERS,
|
|
2555
|
+
)
|
|
2556
|
+
}
|
|
2557
|
+
try {
|
|
2558
|
+
const swapResult = await buildMultiCoinRegisterTx(
|
|
2559
|
+
{
|
|
2560
|
+
domain: fullDomain,
|
|
2561
|
+
years,
|
|
2562
|
+
senderAddress: sender,
|
|
2563
|
+
sourceCoinType: selectedSourceCoinType,
|
|
2564
|
+
coinObjectIds: selectedCoinObjectIds,
|
|
2565
|
+
slippageBps: 100,
|
|
2566
|
+
extraSuiForFeesMist: extraSuiForFeesMist > 0n ? extraSuiForFeesMist : undefined,
|
|
2567
|
+
},
|
|
2568
|
+
env,
|
|
2569
|
+
)
|
|
2570
|
+
tx = swapResult.tx
|
|
2571
|
+
method = 'coin-swap'
|
|
2572
|
+
breakdownInfo = {
|
|
2573
|
+
suiInputMist: String(swapResult.breakdown.suiInputMist),
|
|
2574
|
+
nsOutputEstimate: String(swapResult.breakdown.nsOutputEstimate),
|
|
2575
|
+
registrationCostNsMist: String(swapResult.breakdown.registrationCostNsMist),
|
|
2576
|
+
slippageBps: swapResult.breakdown.slippageBps,
|
|
2577
|
+
source: swapResult.breakdown.source,
|
|
2578
|
+
priceImpactBps: swapResult.breakdown.priceImpactBps,
|
|
2579
|
+
sourceCoinType: swapResult.breakdown.sourceCoinType,
|
|
2580
|
+
sourceTokensNeeded: swapResult.breakdown.sourceTokensNeeded,
|
|
2581
|
+
}
|
|
2582
|
+
} catch (coinBuildError) {
|
|
2583
|
+
const message =
|
|
2584
|
+
coinBuildError instanceof Error && coinBuildError.message
|
|
2585
|
+
? coinBuildError.message
|
|
2586
|
+
: 'Failed to build coin-swap registration transaction'
|
|
2587
|
+
if (paymentMode === 'coin') {
|
|
2588
|
+
return jsonResponse({ error: `USD payment build failed: ${message}` }, 400, CORS_HEADERS)
|
|
2589
|
+
}
|
|
2590
|
+
selectedPaymentMethod = 'ns'
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
if (selectedPaymentMethod !== 'coin') {
|
|
2595
|
+
try {
|
|
2596
|
+
const swapResult = await buildSwapAndRegisterTx(
|
|
2597
|
+
{ domain: fullDomain, years, senderAddress: sender },
|
|
2598
|
+
env,
|
|
2599
|
+
)
|
|
2600
|
+
tx = swapResult.tx
|
|
2601
|
+
method = 'ns-swap'
|
|
2602
|
+
breakdownInfo = {
|
|
2603
|
+
suiInputMist: String(swapResult.breakdown.suiInputMist),
|
|
2604
|
+
nsOutputEstimate: String(swapResult.breakdown.nsOutputEstimate),
|
|
2605
|
+
registrationCostNsMist: String(swapResult.breakdown.registrationCostNsMist),
|
|
2606
|
+
slippageBps: swapResult.breakdown.slippageBps,
|
|
2607
|
+
source: swapResult.breakdown.source,
|
|
2608
|
+
}
|
|
2609
|
+
} catch {
|
|
2610
|
+
const coinFallback = await resolveCoinPayment(env, sender, fullDomain, years, true).catch(
|
|
2611
|
+
() => null,
|
|
2612
|
+
)
|
|
2613
|
+
if (coinFallback) {
|
|
2614
|
+
try {
|
|
2615
|
+
const coinResult = await buildMultiCoinRegisterTx(
|
|
2616
|
+
{
|
|
2617
|
+
domain: fullDomain,
|
|
2618
|
+
years,
|
|
2619
|
+
senderAddress: sender,
|
|
2620
|
+
sourceCoinType: coinFallback.sourceCoinType,
|
|
2621
|
+
coinObjectIds: coinFallback.coinObjectIds,
|
|
2622
|
+
slippageBps: 100,
|
|
2623
|
+
extraSuiForFeesMist: extraSuiForFeesMist > 0n ? extraSuiForFeesMist : undefined,
|
|
2624
|
+
},
|
|
2625
|
+
env,
|
|
2626
|
+
)
|
|
2627
|
+
tx = coinResult.tx
|
|
2628
|
+
method = 'coin-swap'
|
|
2629
|
+
breakdownInfo = {
|
|
2630
|
+
suiInputMist: String(coinResult.breakdown.suiInputMist),
|
|
2631
|
+
nsOutputEstimate: String(coinResult.breakdown.nsOutputEstimate),
|
|
2632
|
+
registrationCostNsMist: String(coinResult.breakdown.registrationCostNsMist),
|
|
2633
|
+
slippageBps: coinResult.breakdown.slippageBps,
|
|
2634
|
+
source: coinResult.breakdown.source,
|
|
2635
|
+
priceImpactBps: coinResult.breakdown.priceImpactBps,
|
|
2636
|
+
sourceCoinType: coinResult.breakdown.sourceCoinType,
|
|
2637
|
+
sourceTokensNeeded: coinResult.breakdown.sourceTokensNeeded,
|
|
2638
|
+
}
|
|
2639
|
+
} catch {
|
|
2640
|
+
tx = await buildSuiRegisterTx({ domain: fullDomain, years, senderAddress: sender }, env)
|
|
2641
|
+
method = 'sui-direct'
|
|
2642
|
+
}
|
|
2643
|
+
} else {
|
|
2644
|
+
tx = await buildSuiRegisterTx({ domain: fullDomain, years, senderAddress: sender }, env)
|
|
2645
|
+
method = 'sui-direct'
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
if (!tx || !method) {
|
|
2651
|
+
throw new Error('Unable to prepare registration transaction')
|
|
2652
|
+
}
|
|
2653
|
+
const buildClient = await getRpcClientWithFallback(env, sender)
|
|
2654
|
+
|
|
2655
|
+
if (wantsPrimary) {
|
|
2656
|
+
const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
|
|
2657
|
+
const suinsClient = new SuinsClient({ client: buildClient as never, network })
|
|
2658
|
+
const suinsTx = new SuinsTransaction(suinsClient, tx)
|
|
2659
|
+
suinsTx.setDefault(fullDomain)
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
const GAS_BUDGET_MIST = 100_000_000n
|
|
2663
|
+
let totalSuiNeededMist = GAS_BUDGET_MIST
|
|
2664
|
+
if (feePricing && feePricing.savingsMist > 0n) {
|
|
2665
|
+
if (referrer) {
|
|
2666
|
+
const refFeeMist = (feePricing.savingsMist * 10n) / 100n
|
|
2667
|
+
if (refFeeMist > 0n) {
|
|
2668
|
+
totalSuiNeededMist += refFeeMist
|
|
2669
|
+
const [refCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(refFeeMist)])
|
|
2670
|
+
tx.transferObjects([refCoin], referrer)
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
if (waapReferral) {
|
|
2674
|
+
const waapFeeMist = (feePricing.savingsMist * 5n) / 100n
|
|
2675
|
+
if (waapFeeMist > 0n) {
|
|
2676
|
+
totalSuiNeededMist += waapFeeMist
|
|
2677
|
+
const [waapCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(waapFeeMist)])
|
|
2678
|
+
tx.transferObjects([waapCoin], waapReferral)
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
let txToBuild = tx
|
|
2684
|
+
if (method === 'coin-swap') {
|
|
2685
|
+
const suiBal = await buildClient.getBalance({ owner: sender, coinType: SUI_COIN_TYPE })
|
|
2686
|
+
const availSuiMist = parseMistAmount(suiBal?.totalBalance)
|
|
2687
|
+
const MIN_COIN_SWAP_GAS_MIST = 20_000_000n
|
|
2688
|
+
if (availSuiMist < GAS_BUDGET_MIST) {
|
|
2689
|
+
if (availSuiMist < MIN_COIN_SWAP_GAS_MIST) {
|
|
2690
|
+
throw new Error(
|
|
2691
|
+
'Insufficient SUI for gas. Send at least 0.02 SUI to this wallet, or use a wallet with SUI.',
|
|
2692
|
+
)
|
|
2693
|
+
}
|
|
2694
|
+
tx.setGasBudget(Number(availSuiMist))
|
|
2695
|
+
}
|
|
2696
|
+
} else {
|
|
2697
|
+
txToBuild = await prependGasSwapIfNeeded(tx, buildClient, sender, totalSuiNeededMist, env)
|
|
2698
|
+
}
|
|
2699
|
+
const txBytes = await txToBuild.build({ client: buildClient })
|
|
2700
|
+
|
|
2701
|
+
const txBytesBase64 = uint8ArrayToBase64(txBytes)
|
|
2702
|
+
|
|
2703
|
+
return jsonResponse(
|
|
2704
|
+
{
|
|
2705
|
+
txBytes: txBytesBase64,
|
|
2706
|
+
method,
|
|
2707
|
+
breakdown: breakdownInfo,
|
|
2708
|
+
payment: {
|
|
2709
|
+
mode: paymentMode,
|
|
2710
|
+
method: selectedPaymentMethod,
|
|
2711
|
+
sourceCoinType:
|
|
2712
|
+
selectedSourceCoinType ||
|
|
2713
|
+
(typeof (breakdownInfo as { sourceCoinType?: unknown }).sourceCoinType === 'string'
|
|
2714
|
+
? String((breakdownInfo as { sourceCoinType?: unknown }).sourceCoinType)
|
|
2715
|
+
: undefined),
|
|
2716
|
+
},
|
|
2717
|
+
},
|
|
2718
|
+
200,
|
|
2719
|
+
CORS_HEADERS,
|
|
2720
|
+
)
|
|
2721
|
+
} catch (error) {
|
|
2722
|
+
const message =
|
|
2723
|
+
error instanceof Error && error.message
|
|
2724
|
+
? error.message
|
|
2725
|
+
: 'Failed to build registration transaction'
|
|
2726
|
+
return jsonResponse({ error: message }, 400, CORS_HEADERS)
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
2731
|
+
let binary = ''
|
|
2732
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2733
|
+
binary += String.fromCharCode(bytes[i])
|
|
2734
|
+
}
|
|
2735
|
+
return btoa(binary)
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
export async function handleRegistrationSubmission(request: Request, env: Env): Promise<Response> {
|
|
2739
|
+
if (request.method === 'OPTIONS') {
|
|
2740
|
+
return new Response(null, { headers: CORS_HEADERS })
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
if (request.method !== 'POST') {
|
|
2744
|
+
return jsonResponse({ error: 'Method not allowed' }, 405, CORS_HEADERS)
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
let payload: {
|
|
2748
|
+
txBytes?: string
|
|
2749
|
+
signatures?: unknown
|
|
2750
|
+
options?: Record<string, unknown>
|
|
2751
|
+
requestType?: string
|
|
2752
|
+
}
|
|
2753
|
+
try {
|
|
2754
|
+
payload = (await request.json()) as typeof payload
|
|
2755
|
+
} catch {
|
|
2756
|
+
return jsonResponse({ error: 'Invalid JSON body' }, 400, CORS_HEADERS)
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
const txBytes = typeof payload.txBytes === 'string' ? payload.txBytes.trim() : ''
|
|
2760
|
+
const rawSignatures = Array.isArray(payload.signatures) ? payload.signatures : []
|
|
2761
|
+
const signatures = rawSignatures
|
|
2762
|
+
.filter((sig): sig is string => typeof sig === 'string' && sig.trim().length > 0)
|
|
2763
|
+
.map((sig) => sig.trim())
|
|
2764
|
+
|
|
2765
|
+
if (!txBytes) {
|
|
2766
|
+
return jsonResponse({ error: 'txBytes is required' }, 400, CORS_HEADERS)
|
|
2767
|
+
}
|
|
2768
|
+
if (signatures.length === 0) {
|
|
2769
|
+
return jsonResponse({ error: 'At least one signature is required' }, 400, CORS_HEADERS)
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
const relay = await relaySignedTransaction(
|
|
2773
|
+
env,
|
|
2774
|
+
txBytes,
|
|
2775
|
+
signatures,
|
|
2776
|
+
(payload.options as Record<string, unknown>) || {},
|
|
2777
|
+
payload.requestType || 'WaitForLocalExecution',
|
|
2778
|
+
)
|
|
2779
|
+
const status = relay.ok ? 200 : relay.status || 502
|
|
2780
|
+
const body =
|
|
2781
|
+
typeof relay.response === 'undefined'
|
|
2782
|
+
? relay.ok
|
|
2783
|
+
? { ok: true }
|
|
2784
|
+
: { error: relay.error || 'Relay failed' }
|
|
2785
|
+
: relay.response
|
|
2786
|
+
|
|
2787
|
+
if (!relay.ok && relay.error && body && typeof body === 'object' && !('error' in body)) {
|
|
2788
|
+
Object.assign(body as Record<string, unknown>, { error: relay.error })
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
return jsonResponse(body, status, CORS_HEADERS)
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
function escapeHtml(value: string): string {
|
|
2795
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
2796
|
+
switch (char) {
|
|
2797
|
+
case '&':
|
|
2798
|
+
return '&'
|
|
2799
|
+
case '<':
|
|
2800
|
+
return '<'
|
|
2801
|
+
case '>':
|
|
2802
|
+
return '>'
|
|
2803
|
+
case '"':
|
|
2804
|
+
return '"'
|
|
2805
|
+
case "'":
|
|
2806
|
+
return '''
|
|
2807
|
+
default:
|
|
2808
|
+
return char
|
|
2809
|
+
}
|
|
2810
|
+
})
|
|
2811
|
+
}
|