sui.ski 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AGENTS.md +311 -0
  2. package/CLAUDE.md +292 -0
  3. package/CODEBASE_GUIDE.md +217 -0
  4. package/README.md +77 -0
  5. package/biome.json +28 -0
  6. package/package.json +73 -0
  7. package/scripts/deploy-messaging-mainnet.sh +184 -0
  8. package/scripts/extract-suins-object.ts +180 -0
  9. package/scripts/full-deploy.sh +26 -0
  10. package/scripts/obsidian.ts +243 -0
  11. package/scripts/set-suins-contenthash.ts +130 -0
  12. package/scripts/setup-ika-dwallet.ts +338 -0
  13. package/scripts/transfer-upgrade-cap-from-nft.ts +86 -0
  14. package/src/durable-objects/wallet-session.ts +333 -0
  15. package/src/handlers/app.ts +1430 -0
  16. package/src/handlers/authenticated-events.ts +267 -0
  17. package/src/handlers/dashboard.ts +1659 -0
  18. package/src/handlers/landing.ts +6751 -0
  19. package/src/handlers/mcp.ts +556 -0
  20. package/src/handlers/messaging-sdk.ts +220 -0
  21. package/src/handlers/profile.css.ts +9332 -0
  22. package/src/handlers/profile.ts +12640 -0
  23. package/src/handlers/register2.ts +2811 -0
  24. package/src/handlers/ski-sign.ts +1901 -0
  25. package/src/handlers/ski.ts +314 -0
  26. package/src/handlers/thunder.ts +940 -0
  27. package/src/handlers/vault.ts +284 -0
  28. package/src/handlers/wallet-api.ts +169 -0
  29. package/src/handlers/x402-register.ts +601 -0
  30. package/src/index.test.ts +55 -0
  31. package/src/index.ts +512 -0
  32. package/src/resolvers/content.ts +231 -0
  33. package/src/resolvers/rpc.ts +222 -0
  34. package/src/resolvers/suins.ts +266 -0
  35. package/src/sdk/messaging.ts +279 -0
  36. package/src/types.ts +230 -0
  37. package/src/utils/agent-keypair.ts +40 -0
  38. package/src/utils/authenticated-events.ts +280 -0
  39. package/src/utils/cache.ts +82 -0
  40. package/src/utils/media-pack.ts +27 -0
  41. package/src/utils/mmr.ts +181 -0
  42. package/src/utils/ns-price.ts +529 -0
  43. package/src/utils/og-image.ts +141 -0
  44. package/src/utils/onchain-activity.ts +211 -0
  45. package/src/utils/onchain-listing.ts +39 -0
  46. package/src/utils/premium.ts +29 -0
  47. package/src/utils/pricing.ts +291 -0
  48. package/src/utils/pyth-price-info.ts +63 -0
  49. package/src/utils/response.ts +204 -0
  50. package/src/utils/rpc.ts +25 -0
  51. package/src/utils/shared-wallet-js.ts +166 -0
  52. package/src/utils/social.ts +152 -0
  53. package/src/utils/status.ts +39 -0
  54. package/src/utils/subdomain.ts +116 -0
  55. package/src/utils/surflux-grpc.ts +241 -0
  56. package/src/utils/swap-transactions.ts +1222 -0
  57. package/src/utils/thunder-css.ts +1341 -0
  58. package/src/utils/thunder-js.ts +5046 -0
  59. package/src/utils/transactions.ts +65 -0
  60. package/src/utils/vault.ts +18 -0
  61. package/src/utils/wallet-kit-js.ts +2312 -0
  62. package/src/utils/wallet-session-js.ts +192 -0
  63. package/src/utils/wallet-tx-js.ts +2287 -0
  64. package/src/utils/wallet-ui-js.ts +3057 -0
  65. package/src/utils/x402-middleware.ts +428 -0
  66. package/src/utils/x402-sui.ts +171 -0
  67. package/src/utils/zksend-js.ts +166 -0
  68. package/tsconfig.json +22 -0
  69. package/workers/x402-multichain/src/index.ts +237 -0
  70. package/workers/x402-multichain/src/types.ts +80 -0
  71. package/workers/x402-multichain/tsconfig.json +20 -0
  72. package/workers/x402-multichain/wrangler.toml +11 -0
  73. package/wrangler.toml +84 -0
@@ -0,0 +1,601 @@
1
+ import { SuiJsonRpcClient as SuiClient } from '@mysten/sui/jsonRpc'
2
+ import { Transaction } from '@mysten/sui/transactions'
3
+ import { SuinsClient, SuinsTransaction } from '@mysten/suins'
4
+ import { Hono } from 'hono'
5
+ import type { Env, X402VerifiedPayment } from '../types'
6
+ import { getAgentAddress, getAgentKeypair } from '../utils/agent-keypair'
7
+ import {
8
+ calculateSuiNeededForNs,
9
+ DEEP_TYPE,
10
+ DEEPBOOK_NS_SUI_POOL,
11
+ DEEPBOOK_PACKAGE,
12
+ DEFAULT_SLIPPAGE_BPS,
13
+ getNSSuiPrice,
14
+ NS_SCALE,
15
+ NS_TYPE_MAINNET,
16
+ SUI_TYPE,
17
+ simulateBuyNsWithSui,
18
+ } from '../utils/ns-price'
19
+ import { calculateRegistrationPrice, formatPricingResponse } from '../utils/pricing'
20
+ import { jsonResponse } from '../utils/response'
21
+ import { getDefaultRpcUrl } from '../utils/rpc'
22
+ import { fetchMultichainPaymentRequirements, resolveX402Providers, x402PaymentMiddleware } from '../utils/x402-middleware'
23
+ import { resolveX402Recipient } from '../utils/x402-sui'
24
+
25
+ type X402RegisterEnv = {
26
+ Bindings: Env
27
+ Variables: {
28
+ env: Env
29
+ x402Payment?: X402VerifiedPayment
30
+ }
31
+ }
32
+
33
+ const CLOCK_OBJECT = '0x6'
34
+ const X402_FEE_PERCENT = 20
35
+ const ESTIMATED_GAS_MIST = 150_000_000
36
+ const DEFAULT_AGENT_FEE_MIST = '1000000000'
37
+ const ADDRESS_PATTERN = /^0x[a-fA-F0-9]{64}$/
38
+
39
+ function generateRequestId(): string {
40
+ return `x402reg_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
41
+ }
42
+
43
+ function getAgentFeeMist(env: Env): string {
44
+ const configured = env.X402_AGENT_FEE_MIST?.trim()
45
+ if (!configured || !/^\d+$/.test(configured)) return DEFAULT_AGENT_FEE_MIST
46
+ return configured
47
+ }
48
+
49
+ async function resolveFeeRecipient(
50
+ client: SuiClient,
51
+ suinsClient: SuinsClient,
52
+ feeName: string | undefined,
53
+ fallback: string,
54
+ ): Promise<string> {
55
+ if (!feeName) return fallback
56
+ const normalizedName = feeName.replace(/\.sui$/i, '') + '.sui'
57
+ try {
58
+ const record = await suinsClient.getNameRecord(normalizedName)
59
+ if (record?.targetAddress && ADDRESS_PATTERN.test(record.targetAddress)) {
60
+ return record.targetAddress
61
+ }
62
+ } catch {
63
+ // fall through
64
+ }
65
+ try {
66
+ const resolved = await client.resolveNameServiceAddress({ name: normalizedName })
67
+ if (resolved && ADDRESS_PATTERN.test(resolved)) {
68
+ return resolved
69
+ }
70
+ } catch {
71
+ // fall through
72
+ }
73
+ return fallback
74
+ }
75
+
76
+ async function storeRequest(
77
+ env: Env,
78
+ requestId: string,
79
+ payload: Record<string, unknown>,
80
+ ): Promise<void> {
81
+ await env.CACHE.put(`x402-register:${requestId}`, JSON.stringify(payload), {
82
+ expirationTtl: 86400,
83
+ })
84
+ }
85
+
86
+ export const x402RegisterRoutes = new Hono<X402RegisterEnv>()
87
+
88
+ x402RegisterRoutes.use('*', async (c, next) => {
89
+ if (!c.get('env')) c.set('env', c.env)
90
+ await next()
91
+ })
92
+
93
+ x402RegisterRoutes.get('/info', async (c) => {
94
+ const env = c.get('env')
95
+ const providers = resolveX402Providers(env)
96
+ const feeMist = getAgentFeeMist(env)
97
+
98
+ const [payTo, multichainAccepts, agentAddress] = await Promise.all([
99
+ resolveX402Recipient(env),
100
+ fetchMultichainPaymentRequirements(env, feeMist),
101
+ Promise.resolve<string | null>((() => {
102
+ try { return getAgentAddress(env) } catch { return null }
103
+ })()),
104
+ ])
105
+
106
+ const suiAccept = {
107
+ scheme: 'exact-sui' as const,
108
+ network: `sui:${env.SUI_NETWORK}`,
109
+ asset: '0x2::sui::SUI',
110
+ amount: feeMist,
111
+ payTo: payTo || '',
112
+ maxTimeoutSeconds: 120,
113
+ extra: { verificationMethod: 'pre-executed' },
114
+ }
115
+
116
+ const accepts = [suiAccept, ...multichainAccepts]
117
+
118
+ return jsonResponse({
119
+ service: 'x402 Auto-Registration Agent',
120
+ description:
121
+ 'Pay with x402, agent registers .sui names with 25% NS discount and transfers NFT to you',
122
+ agentAddress,
123
+ pricing: {
124
+ amountMist: feeMist,
125
+ asset: '0x2::sui::SUI',
126
+ network: `sui:${env.SUI_NETWORK}`,
127
+ },
128
+ features: {
129
+ nsDiscount: '25%',
130
+ x402FeePercent: X402_FEE_PERCENT,
131
+ autoSwap: 'SUI → NS via DeepBook',
132
+ nftTransfer: 'To payer address',
133
+ },
134
+ paymentMethods: ['x402'],
135
+ x402: {
136
+ version: 2,
137
+ accepts,
138
+ verificationProviders: providers,
139
+ },
140
+ endpoints: {
141
+ info: 'GET /api/agents/x402-register/info',
142
+ quote: 'GET /api/agents/x402-register/quote?domain=example&years=1',
143
+ register: 'POST /api/agents/x402-register/register',
144
+ status: 'GET /api/agents/x402-register/status/:id',
145
+ },
146
+ })
147
+ })
148
+
149
+ x402RegisterRoutes.get('/quote', async (c) => {
150
+ const env = c.get('env')
151
+ const domain = c.req.query('domain')?.trim()
152
+ const yearsParam = c.req.query('years')
153
+
154
+ if (!domain) {
155
+ return jsonResponse({ error: 'domain query parameter is required' }, 400)
156
+ }
157
+
158
+ const years = yearsParam ? Number.parseInt(yearsParam, 10) : 1
159
+ if (!Number.isInteger(years) || years < 1 || years > 5) {
160
+ return jsonResponse({ error: 'years must be an integer between 1 and 5' }, 400)
161
+ }
162
+
163
+ const cleanDomain = `${domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
164
+
165
+ try {
166
+ const pricing = await calculateRegistrationPrice({ domain: cleanDomain, years, env })
167
+ const feeMist = getAgentFeeMist(env)
168
+ const x402FeeMist = (pricing.discountedSuiMist * BigInt(X402_FEE_PERCENT)) / 100n
169
+ const totalMist = pricing.discountedSuiMist + x402FeeMist + BigInt(ESTIMATED_GAS_MIST)
170
+
171
+ return jsonResponse({
172
+ domain: cleanDomain,
173
+ years,
174
+ pricing: formatPricingResponse(pricing),
175
+ agentFees: {
176
+ x402PaymentMist: feeMist,
177
+ x402FeeMist: String(x402FeeMist),
178
+ x402FeePercent: X402_FEE_PERCENT,
179
+ estimatedGasMist: String(ESTIMATED_GAS_MIST),
180
+ },
181
+ totalEstimatedMist: String(totalMist),
182
+ note: 'Agent wallet must hold sufficient SUI for gas + registration. x402 payment covers service fee.',
183
+ })
184
+ } catch (error) {
185
+ const message = error instanceof Error ? error.message : 'Failed to calculate pricing'
186
+ return jsonResponse({ error: message }, 400)
187
+ }
188
+ })
189
+
190
+ x402RegisterRoutes.post(
191
+ '/register',
192
+ x402PaymentMiddleware({
193
+ description: 'Auto-register .sui name with 25% NS discount',
194
+ allowBypassWhenUnconfigured: false,
195
+ }),
196
+ async (c) => {
197
+ const env = c.get('env')
198
+ const x402Payment = c.get('x402Payment')
199
+
200
+ if (!x402Payment) {
201
+ return jsonResponse({ error: 'Payment verification missing' }, 402)
202
+ }
203
+
204
+ let payload: {
205
+ domain?: string
206
+ years?: number
207
+ targetAddress?: string
208
+ }
209
+ try {
210
+ payload = await c.req.json()
211
+ } catch {
212
+ return jsonResponse({ error: 'Invalid JSON body' }, 400)
213
+ }
214
+
215
+ if (!payload.domain?.trim()) {
216
+ return jsonResponse({ error: 'domain is required' }, 400)
217
+ }
218
+
219
+ const years = payload.years ?? 1
220
+ if (!Number.isInteger(years) || years < 1 || years > 5) {
221
+ return jsonResponse({ error: 'years must be an integer between 1 and 5' }, 400)
222
+ }
223
+
224
+ const payerAddress = payload.targetAddress || x402Payment.payer
225
+ if (!ADDRESS_PATTERN.test(payerAddress)) {
226
+ return jsonResponse({ error: 'Could not determine valid payer address' }, 400)
227
+ }
228
+
229
+ const cleanDomain = `${payload.domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
230
+ const requestId = generateRequestId()
231
+ const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
232
+
233
+ try {
234
+ const keypair = getAgentKeypair(env)
235
+ const agentAddress = keypair.toSuiAddress()
236
+
237
+ const client = new SuiClient({
238
+ url: getDefaultRpcUrl(env.SUI_NETWORK),
239
+ network: env.SUI_NETWORK,
240
+ })
241
+ const suinsClient = new SuinsClient({ client: client as never, network })
242
+
243
+ const [pricing, nsPrice, x402Recipient, feeRecipient] = await Promise.all([
244
+ calculateRegistrationPrice({ domain: cleanDomain, years, env }),
245
+ getNSSuiPrice(env, true),
246
+ resolveX402Recipient(env),
247
+ resolveFeeRecipient(client, suinsClient, env.SERVICE_FEE_NAME, agentAddress),
248
+ ])
249
+
250
+ const registrationCostNsMist = pricing.nsNeededMist
251
+ const nsPoolAddress = DEEPBOOK_NS_SUI_POOL[network]
252
+ const deepbookPackage = DEEPBOOK_PACKAGE[network]
253
+
254
+ if (!nsPoolAddress || !deepbookPackage) {
255
+ throw new Error(`DeepBook pools not available on ${network}`)
256
+ }
257
+
258
+ const slippageBps = DEFAULT_SLIPPAGE_BPS
259
+ let suiForNsSwap: bigint
260
+ let minNsOutput: bigint
261
+ let priceImpactBps = 0
262
+
263
+ if (nsPrice.asks?.length) {
264
+ const wideSlippage = Math.max(slippageBps, 1500)
265
+ const quote = calculateSuiNeededForNs(registrationCostNsMist, nsPrice.asks, wideSlippage)
266
+ suiForNsSwap = quote.suiNeeded
267
+
268
+ minNsOutput = registrationCostNsMist
269
+
270
+ const simResult = simulateBuyNsWithSui(suiForNsSwap, nsPrice.asks)
271
+ priceImpactBps = simResult.priceImpactBps
272
+
273
+ if (simResult.outputNs < minNsOutput) {
274
+ suiForNsSwap = suiForNsSwap + (suiForNsSwap * 30n) / 100n
275
+ }
276
+ } else {
277
+ const bufferBps = Math.max(slippageBps * 3, 3000)
278
+ const nsWithBuffer =
279
+ registrationCostNsMist + (registrationCostNsMist * BigInt(bufferBps)) / 10000n
280
+ const nsTokens = Number(nsWithBuffer) / NS_SCALE
281
+ suiForNsSwap = BigInt(Math.ceil(nsTokens * nsPrice.suiPerNs * 1e9))
282
+
283
+ minNsOutput = registrationCostNsMist
284
+ }
285
+
286
+ const x402FeeMist = (pricing.discountedSuiMist * BigInt(X402_FEE_PERCENT)) / 100n
287
+
288
+ const tx = new Transaction()
289
+ tx.setSender(agentAddress)
290
+
291
+ const [suiCoinForNs] = tx.splitCoins(tx.gas, [tx.pure.u64(suiForNsSwap)])
292
+
293
+ const [zeroDeepCoin] = tx.moveCall({
294
+ target: '0x2::coin::zero',
295
+ typeArguments: [DEEP_TYPE],
296
+ })
297
+
298
+ const [nsCoin, nsLeftoverSui, nsLeftoverDeep] = tx.moveCall({
299
+ target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
300
+ typeArguments: [NS_TYPE_MAINNET, SUI_TYPE],
301
+ arguments: [
302
+ tx.object(nsPoolAddress),
303
+ suiCoinForNs,
304
+ zeroDeepCoin,
305
+ tx.pure.u64(minNsOutput),
306
+ tx.object(CLOCK_OBJECT),
307
+ ],
308
+ })
309
+
310
+ tx.transferObjects([nsLeftoverSui, nsLeftoverDeep], agentAddress)
311
+
312
+ const suinsTx = new SuinsTransaction(suinsClient, tx)
313
+ const coinConfig = suinsClient.config.coins.NS
314
+ if (!coinConfig) {
315
+ throw new Error('SuiNS NS coin configuration not found')
316
+ }
317
+
318
+ const priceInfoObjectId = coinConfig.feed
319
+ ? (await suinsClient.getPriceInfoObject(tx, coinConfig.feed))[0]
320
+ : undefined
321
+
322
+ const nft = suinsTx.register({
323
+ domain: cleanDomain,
324
+ years,
325
+ coinConfig,
326
+ coin: nsCoin,
327
+ priceInfoObjectId,
328
+ })
329
+
330
+ suinsTx.setTargetAddress({
331
+ nft,
332
+ address: payerAddress,
333
+ isSubname: cleanDomain.replace(/\.sui$/i, '').includes('.'),
334
+ })
335
+
336
+ tx.transferObjects([nft], payerAddress)
337
+
338
+ if (x402Recipient && x402FeeMist > 0n) {
339
+ const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(x402FeeMist)])
340
+ tx.transferObjects([feeCoin], x402Recipient)
341
+ }
342
+
343
+ tx.transferObjects([nsCoin], feeRecipient)
344
+ tx.setGasBudget(ESTIMATED_GAS_MIST)
345
+
346
+ const txBytes = await tx.build({ client: client as never })
347
+ const { signature } = await keypair.signTransaction(txBytes)
348
+ const txBytesBase64 = btoa(String.fromCharCode(...txBytes))
349
+
350
+ const rpcResponse = await fetch(env.SUI_RPC_URL, {
351
+ method: 'POST',
352
+ headers: { 'Content-Type': 'application/json' },
353
+ body: JSON.stringify({
354
+ jsonrpc: '2.0',
355
+ id: Date.now(),
356
+ method: 'sui_executeTransactionBlock',
357
+ params: [
358
+ txBytesBase64,
359
+ [signature],
360
+ { showEffects: true, showEvents: true, showObjectChanges: true },
361
+ 'WaitForLocalExecution',
362
+ ],
363
+ }),
364
+ })
365
+
366
+ const rpcJson = (await rpcResponse.json()) as {
367
+ result?: {
368
+ digest?: string
369
+ effects?: { status?: { status: string; error?: string } }
370
+ events?: Array<{ type: string; parsedJson?: Record<string, unknown> }>
371
+ objectChanges?: Array<{
372
+ type: string
373
+ objectType?: string
374
+ objectId?: string
375
+ owner?: unknown
376
+ }>
377
+ }
378
+ error?: { message?: string }
379
+ }
380
+
381
+ if (rpcJson.error) {
382
+ throw new Error(`Transaction failed: ${rpcJson.error.message}`)
383
+ }
384
+
385
+ const txResult = rpcJson.result
386
+ if (!txResult?.effects?.status || txResult.effects.status.status !== 'success') {
387
+ throw new Error(
388
+ `Transaction execution failed: ${txResult?.effects?.status?.error || 'unknown error'}`,
389
+ )
390
+ }
391
+
392
+ const digest = txResult.digest || 'unknown'
393
+
394
+ let nftId: string | null = null
395
+ if (txResult.objectChanges) {
396
+ for (const change of txResult.objectChanges) {
397
+ if (
398
+ change.type === 'created' &&
399
+ change.objectType?.includes('::suins_registration::SuinsRegistration')
400
+ ) {
401
+ nftId = change.objectId || null
402
+ break
403
+ }
404
+ }
405
+ }
406
+
407
+ let registrationEvent: Record<string, unknown> | null = null
408
+ if (txResult.events) {
409
+ for (const event of txResult.events) {
410
+ if (
411
+ event.type.includes('::register::NameRegistered') ||
412
+ event.type.includes('Register')
413
+ ) {
414
+ registrationEvent = event.parsedJson || null
415
+ break
416
+ }
417
+ }
418
+ }
419
+
420
+ const receipt = {
421
+ success: true,
422
+ requestId,
423
+ domain: cleanDomain,
424
+ years,
425
+ nftId,
426
+ digest,
427
+ payerAddress,
428
+ agentAddress,
429
+ pricing: {
430
+ registrationCostNsMist: String(registrationCostNsMist),
431
+ suiInputMist: String(suiForNsSwap),
432
+ x402FeeMist: String(x402FeeMist),
433
+ nsPerSui: nsPrice.nsPerSui,
434
+ priceImpactBps,
435
+ },
436
+ registrationEvent,
437
+ x402Payment,
438
+ timestamp: Date.now(),
439
+ }
440
+
441
+ await storeRequest(env, requestId, receipt)
442
+
443
+ return jsonResponse(receipt)
444
+ } catch (error) {
445
+ const message = error instanceof Error ? error.message : 'Registration failed'
446
+ const failureRecord = {
447
+ success: false,
448
+ requestId,
449
+ domain: cleanDomain,
450
+ error: message,
451
+ payerAddress,
452
+ x402Payment,
453
+ timestamp: Date.now(),
454
+ }
455
+ await storeRequest(env, requestId, failureRecord).catch(() => {})
456
+ return jsonResponse({ error: message, requestId }, 500)
457
+ }
458
+ },
459
+ )
460
+
461
+ x402RegisterRoutes.post('/sweep', async (c) => {
462
+ const env = c.get('env')
463
+
464
+ try {
465
+ const keypair = getAgentKeypair(env)
466
+ const agentAddress = keypair.toSuiAddress()
467
+ const client = new SuiClient({
468
+ url: getDefaultRpcUrl(env.SUI_NETWORK),
469
+ network: env.SUI_NETWORK,
470
+ })
471
+ const x402Recipient = await resolveX402Recipient(env)
472
+ if (!x402Recipient) {
473
+ return jsonResponse({ error: 'Could not resolve x402.sui recipient address' }, 500)
474
+ }
475
+
476
+ const GAS_RESERVE_MIST = 200_000_000n
477
+ const SWEEP_GAS_BUDGET = 50_000_000
478
+
479
+ const STABLECOIN_TYPES = [
480
+ '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC',
481
+ '0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN',
482
+ ]
483
+
484
+ const allCoins = await client.getAllCoins({ owner: agentAddress })
485
+ const coinsByType: Map<string, Array<{ objectId: string; balance: bigint }>> = new Map()
486
+
487
+ for (const coin of allCoins.data) {
488
+ const existing = coinsByType.get(coin.coinType) || []
489
+ existing.push({ objectId: coin.coinObjectId, balance: BigInt(coin.balance) })
490
+ coinsByType.set(coin.coinType, existing)
491
+ }
492
+
493
+ const suiCoins = coinsByType.get('0x2::sui::SUI') || []
494
+ let totalSuiBalance = 0n
495
+ for (const coin of suiCoins) totalSuiBalance += coin.balance
496
+
497
+ const sweepableSui = totalSuiBalance - GAS_RESERVE_MIST - BigInt(SWEEP_GAS_BUDGET)
498
+ const transfers: Array<{ type: string; amount: string }> = []
499
+
500
+ if (sweepableSui <= 0n && STABLECOIN_TYPES.every((t) => !coinsByType.has(t))) {
501
+ return jsonResponse({
502
+ message: 'Nothing to sweep',
503
+ agentAddress,
504
+ recipient: x402Recipient,
505
+ suiBalance: String(totalSuiBalance),
506
+ gasReserve: String(GAS_RESERVE_MIST),
507
+ })
508
+ }
509
+
510
+ const tx = new Transaction()
511
+ tx.setSender(agentAddress)
512
+
513
+ if (sweepableSui > 0n) {
514
+ const [sweepCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(sweepableSui)])
515
+ tx.transferObjects([sweepCoin], x402Recipient)
516
+ transfers.push({ type: 'SUI', amount: String(sweepableSui) })
517
+ }
518
+
519
+ for (const coinType of STABLECOIN_TYPES) {
520
+ const coins = coinsByType.get(coinType)
521
+ if (!coins?.length) continue
522
+
523
+ let totalBalance = 0n
524
+ for (const c of coins) totalBalance += c.balance
525
+ if (totalBalance === 0n) continue
526
+
527
+ const coinRefs = coins.map((c) => tx.object(c.objectId))
528
+ if (coinRefs.length > 1) {
529
+ tx.mergeCoins(coinRefs[0], coinRefs.slice(1))
530
+ }
531
+ tx.transferObjects([coinRefs[0]], x402Recipient)
532
+
533
+ const label = coinType.includes('usdc') ? 'USDC' : coinType.split('::').pop() || coinType
534
+ transfers.push({ type: label, amount: String(totalBalance) })
535
+ }
536
+
537
+ tx.setGasBudget(SWEEP_GAS_BUDGET)
538
+
539
+ const txBytes = await tx.build({ client: client as never })
540
+ const { signature } = await keypair.signTransaction(txBytes)
541
+ const txBytesBase64 = btoa(String.fromCharCode(...txBytes))
542
+
543
+ const rpcResponse = await fetch(env.SUI_RPC_URL, {
544
+ method: 'POST',
545
+ headers: { 'Content-Type': 'application/json' },
546
+ body: JSON.stringify({
547
+ jsonrpc: '2.0',
548
+ id: Date.now(),
549
+ method: 'sui_executeTransactionBlock',
550
+ params: [
551
+ txBytesBase64,
552
+ [signature],
553
+ { showEffects: true, showBalanceChanges: true },
554
+ 'WaitForLocalExecution',
555
+ ],
556
+ }),
557
+ })
558
+
559
+ const rpcJson = (await rpcResponse.json()) as {
560
+ result?: {
561
+ digest?: string
562
+ effects?: { status?: { status: string; error?: string } }
563
+ }
564
+ error?: { message?: string }
565
+ }
566
+
567
+ if (rpcJson.error) {
568
+ throw new Error(`Sweep transaction failed: ${rpcJson.error.message}`)
569
+ }
570
+
571
+ if (rpcJson.result?.effects?.status?.status !== 'success') {
572
+ throw new Error(
573
+ `Sweep execution failed: ${rpcJson.result?.effects?.status?.error || 'unknown'}`,
574
+ )
575
+ }
576
+
577
+ return jsonResponse({
578
+ success: true,
579
+ digest: rpcJson.result.digest,
580
+ agentAddress,
581
+ recipient: x402Recipient,
582
+ transfers,
583
+ timestamp: Date.now(),
584
+ })
585
+ } catch (error) {
586
+ const message = error instanceof Error ? error.message : 'Sweep failed'
587
+ return jsonResponse({ error: message }, 500)
588
+ }
589
+ })
590
+
591
+ x402RegisterRoutes.get('/status/:id', async (c) => {
592
+ const env = c.get('env')
593
+ const requestId = c.req.param('id')
594
+ try {
595
+ const data = await env.CACHE.get(`x402-register:${requestId}`)
596
+ if (!data) return jsonResponse({ error: 'Request not found' }, 404)
597
+ return jsonResponse(JSON.parse(data))
598
+ } catch {
599
+ return jsonResponse({ error: 'Failed to fetch request status' }, 500)
600
+ }
601
+ })
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { parseSubdomain, toSuiNSName } from './utils/subdomain'
3
+
4
+ describe('parseSubdomain', () => {
5
+ it('parses root domain', () => {
6
+ const result = parseSubdomain('sui.ski')
7
+ expect(result.type).toBe('root')
8
+ expect(result.subdomain).toBe('')
9
+ })
10
+
11
+ it('parses www as root', () => {
12
+ const result = parseSubdomain('www.sui.ski')
13
+ expect(result.type).toBe('root')
14
+ })
15
+
16
+ it('parses SuiNS name subdomain', () => {
17
+ const result = parseSubdomain('myname.sui.ski')
18
+ expect(result.type).toBe('suins')
19
+ expect(result.subdomain).toBe('myname')
20
+ })
21
+
22
+ it('parses RPC subdomain', () => {
23
+ const result = parseSubdomain('rpc.sui.ski')
24
+ expect(result.type).toBe('rpc')
25
+ })
26
+
27
+ it('parses IPFS content subdomain', () => {
28
+ const result = parseSubdomain('ipfs-QmTest123.sui.ski')
29
+ expect(result.type).toBe('content')
30
+ // Hostname is normalized to lowercase (DNS is case-insensitive)
31
+ expect(result.subdomain).toBe('ipfs-qmtest123')
32
+ })
33
+
34
+ it('parses Walrus content subdomain', () => {
35
+ const result = parseSubdomain('walrus-abc123.sui.ski')
36
+ expect(result.type).toBe('content')
37
+ expect(result.subdomain).toBe('walrus-abc123')
38
+ })
39
+
40
+ it('parses staging environment', () => {
41
+ const result = parseSubdomain('myname.staging.sui.ski')
42
+ expect(result.type).toBe('suins')
43
+ expect(result.subdomain).toBe('myname')
44
+ })
45
+ })
46
+
47
+ describe('toSuiNSName', () => {
48
+ it('adds .sui suffix', () => {
49
+ expect(toSuiNSName('myname')).toBe('myname.sui')
50
+ })
51
+
52
+ it('preserves existing .sui suffix', () => {
53
+ expect(toSuiNSName('myname.sui')).toBe('myname.sui')
54
+ })
55
+ })