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,1222 @@
|
|
|
1
|
+
import { SuiJsonRpcClient as SuiClient } from '@mysten/sui/jsonRpc'
|
|
2
|
+
import { Transaction, TransactionDataBuilder } from '@mysten/sui/transactions'
|
|
3
|
+
import { SuinsClient, SuinsTransaction } from '@mysten/suins'
|
|
4
|
+
import type { Env } from '../types'
|
|
5
|
+
import {
|
|
6
|
+
calculateSuiNeededForNs,
|
|
7
|
+
DEEP_TYPE,
|
|
8
|
+
DEEPBOOK_NS_SUI_POOL,
|
|
9
|
+
DEEPBOOK_PACKAGE,
|
|
10
|
+
DEEPBOOK_SUI_USDC_POOL,
|
|
11
|
+
DEFAULT_SLIPPAGE_BPS,
|
|
12
|
+
getDeepBookSuiPools,
|
|
13
|
+
getNSSuiPrice,
|
|
14
|
+
NS_SCALE,
|
|
15
|
+
NS_TYPE_MAINNET,
|
|
16
|
+
SUI_TYPE,
|
|
17
|
+
simulateBuyNsWithSui,
|
|
18
|
+
USDC_TYPE,
|
|
19
|
+
} from './ns-price'
|
|
20
|
+
import { calculateRegistrationPrice, calculateRenewalPrice } from './pricing'
|
|
21
|
+
import { getDefaultRpcUrl } from './rpc'
|
|
22
|
+
|
|
23
|
+
const CLOCK_OBJECT = '0x6'
|
|
24
|
+
const DUST_SINK_ADDRESS = '0x0000000000000000000000000000000000000000000000000000000000000000'
|
|
25
|
+
|
|
26
|
+
async function resolveFeeRecipient(
|
|
27
|
+
client: SuiClient,
|
|
28
|
+
suinsClient: SuinsClient,
|
|
29
|
+
feeName: string | undefined,
|
|
30
|
+
fallback: string,
|
|
31
|
+
): Promise<string> {
|
|
32
|
+
if (!feeName) return fallback
|
|
33
|
+
const normalizedName = feeName.replace(/\.sui$/i, '') + '.sui'
|
|
34
|
+
try {
|
|
35
|
+
const record = await suinsClient.getNameRecord(normalizedName)
|
|
36
|
+
if (record?.targetAddress && /^0x[a-fA-F0-9]{64}$/.test(record.targetAddress)) {
|
|
37
|
+
return record.targetAddress
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// try fallback resolver below
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const resolved = await client.resolveNameServiceAddress({ name: normalizedName })
|
|
44
|
+
if (resolved && /^0x[a-fA-F0-9]{64}$/.test(resolved)) {
|
|
45
|
+
return resolved
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// use fallback
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
if (/^0x[a-fA-F0-9]{64}$/.test(fallback)) {
|
|
52
|
+
return fallback
|
|
53
|
+
}
|
|
54
|
+
const resolvedFallback = await client.resolveNameServiceAddress({
|
|
55
|
+
name: fallback.replace(/\.sui$/i, '') + '.sui',
|
|
56
|
+
})
|
|
57
|
+
if (resolvedFallback && /^0x[a-fA-F0-9]{64}$/.test(resolvedFallback)) {
|
|
58
|
+
return resolvedFallback
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// use raw fallback
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const recordFallback = await suinsClient.getNameRecord(fallback.replace(/\.sui$/i, '') + '.sui')
|
|
65
|
+
if (recordFallback?.targetAddress && /^0x[a-fA-F0-9]{64}$/.test(recordFallback.targetAddress)) {
|
|
66
|
+
return recordFallback.targetAddress
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// use raw fallback
|
|
70
|
+
}
|
|
71
|
+
if (/^0x[a-fA-F0-9]{64}$/.test(fallback)) {
|
|
72
|
+
return fallback
|
|
73
|
+
}
|
|
74
|
+
return fallback
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface SwapRegisterParams {
|
|
78
|
+
domain: string
|
|
79
|
+
years: number
|
|
80
|
+
senderAddress: string
|
|
81
|
+
slippageBps?: number
|
|
82
|
+
expirationMs?: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface SwapBreakdown {
|
|
86
|
+
suiInputMist: bigint
|
|
87
|
+
nsOutputEstimate: bigint
|
|
88
|
+
registrationCostNsMist: bigint
|
|
89
|
+
slippageBps: number
|
|
90
|
+
nsPerSui: number
|
|
91
|
+
source: 'deepbook' | 'fallback'
|
|
92
|
+
priceImpactBps: number
|
|
93
|
+
minNsOutput: bigint
|
|
94
|
+
feeRecipient?: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface SwapRegisterResult {
|
|
98
|
+
tx: Transaction
|
|
99
|
+
breakdown: SwapBreakdown
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface MultiCoinRegisterParams {
|
|
103
|
+
domain: string
|
|
104
|
+
years: number
|
|
105
|
+
senderAddress: string
|
|
106
|
+
sourceCoinType: string
|
|
107
|
+
coinObjectIds: string[]
|
|
108
|
+
slippageBps?: number
|
|
109
|
+
expirationMs?: number
|
|
110
|
+
extraSuiForFeesMist?: bigint
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface MultiCoinRegisterResult {
|
|
114
|
+
tx: Transaction
|
|
115
|
+
breakdown: SwapBreakdown & {
|
|
116
|
+
sourceCoinType: string
|
|
117
|
+
sourceTokensNeeded: string
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function buildSwapAndRegisterTx(
|
|
122
|
+
params: SwapRegisterParams,
|
|
123
|
+
env: Env,
|
|
124
|
+
): Promise<SwapRegisterResult> {
|
|
125
|
+
const { domain, years, senderAddress, expirationMs } = params
|
|
126
|
+
const slippageBps = params.slippageBps ?? DEFAULT_SLIPPAGE_BPS
|
|
127
|
+
|
|
128
|
+
const cleanDomain = `${domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
|
|
129
|
+
const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
|
|
130
|
+
|
|
131
|
+
const [pricing, nsPrice] = await Promise.all([
|
|
132
|
+
calculateRegistrationPrice({ domain: cleanDomain, years, expirationMs, env }),
|
|
133
|
+
getNSSuiPrice(env, true),
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
const registrationCostNsMist = pricing.nsNeededMist
|
|
137
|
+
|
|
138
|
+
const nsPoolAddress = DEEPBOOK_NS_SUI_POOL[network]
|
|
139
|
+
const deepbookPackage = DEEPBOOK_PACKAGE[network]
|
|
140
|
+
|
|
141
|
+
if (!nsPoolAddress || !deepbookPackage) {
|
|
142
|
+
throw new Error(`DeepBook pools not available on ${network}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let suiForNsSwap: bigint
|
|
146
|
+
let minNsOutput: bigint
|
|
147
|
+
let expectedNsOutput: bigint
|
|
148
|
+
let priceImpactBps = 0
|
|
149
|
+
|
|
150
|
+
if (nsPrice.asks?.length) {
|
|
151
|
+
const wideSlippage = Math.max(slippageBps, 1500)
|
|
152
|
+
const quote = calculateSuiNeededForNs(registrationCostNsMist, nsPrice.asks, wideSlippage)
|
|
153
|
+
suiForNsSwap = quote.suiNeeded
|
|
154
|
+
expectedNsOutput = quote.expectedNs
|
|
155
|
+
minNsOutput = registrationCostNsMist
|
|
156
|
+
|
|
157
|
+
const simResult = simulateBuyNsWithSui(suiForNsSwap, nsPrice.asks)
|
|
158
|
+
priceImpactBps = simResult.priceImpactBps
|
|
159
|
+
|
|
160
|
+
if (simResult.outputNs < minNsOutput) {
|
|
161
|
+
const extraBuffer = (suiForNsSwap * 30n) / 100n
|
|
162
|
+
suiForNsSwap = suiForNsSwap + extraBuffer
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const bufferBps = Math.max(slippageBps * 3, 3000)
|
|
166
|
+
const nsWithBuffer =
|
|
167
|
+
registrationCostNsMist + (registrationCostNsMist * BigInt(bufferBps)) / 10000n
|
|
168
|
+
const nsTokens = Number(nsWithBuffer) / NS_SCALE
|
|
169
|
+
suiForNsSwap = BigInt(Math.ceil(nsTokens * nsPrice.suiPerNs * 1e9))
|
|
170
|
+
expectedNsOutput = nsWithBuffer
|
|
171
|
+
minNsOutput = registrationCostNsMist
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const suiInputMist = suiForNsSwap
|
|
175
|
+
|
|
176
|
+
const client = new SuiClient({ url: getDefaultRpcUrl(env.SUI_NETWORK), network: env.SUI_NETWORK })
|
|
177
|
+
const suinsClient = new SuinsClient({ client: client as never, network })
|
|
178
|
+
|
|
179
|
+
const tx = new Transaction()
|
|
180
|
+
tx.setSender(senderAddress)
|
|
181
|
+
|
|
182
|
+
const [suiCoinForNs] = tx.splitCoins(tx.gas, [tx.pure.u64(suiForNsSwap)])
|
|
183
|
+
|
|
184
|
+
const [zeroDeepCoin] = tx.moveCall({
|
|
185
|
+
target: '0x2::coin::zero',
|
|
186
|
+
typeArguments: [DEEP_TYPE],
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const [nsCoin, nsLeftoverSui, nsLeftoverDeep] = tx.moveCall({
|
|
190
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
191
|
+
typeArguments: [NS_TYPE_MAINNET, SUI_TYPE],
|
|
192
|
+
arguments: [
|
|
193
|
+
tx.object(nsPoolAddress),
|
|
194
|
+
suiCoinForNs,
|
|
195
|
+
zeroDeepCoin,
|
|
196
|
+
tx.pure.u64(minNsOutput),
|
|
197
|
+
tx.object(CLOCK_OBJECT),
|
|
198
|
+
],
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
tx.transferObjects([nsLeftoverSui, nsLeftoverDeep], senderAddress)
|
|
202
|
+
|
|
203
|
+
const suinsTx = new SuinsTransaction(suinsClient, tx)
|
|
204
|
+
const coinConfig = suinsClient.config.coins.NS
|
|
205
|
+
if (!coinConfig) {
|
|
206
|
+
throw new Error('SuiNS NS coin configuration not found')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const priceInfoObjectId = coinConfig.feed
|
|
210
|
+
? (await suinsClient.getPriceInfoObject(tx, coinConfig.feed))[0]
|
|
211
|
+
: undefined
|
|
212
|
+
|
|
213
|
+
const nft = suinsTx.register({
|
|
214
|
+
domain: cleanDomain,
|
|
215
|
+
years,
|
|
216
|
+
coinConfig,
|
|
217
|
+
coin: nsCoin,
|
|
218
|
+
priceInfoObjectId,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
suinsTx.setTargetAddress({
|
|
222
|
+
nft,
|
|
223
|
+
address: senderAddress,
|
|
224
|
+
isSubname: cleanDomain.replace(/\.sui$/i, '').includes('.'),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
tx.transferObjects([nft], senderAddress)
|
|
228
|
+
|
|
229
|
+
const feeRecipient = await resolveFeeRecipient(
|
|
230
|
+
client,
|
|
231
|
+
suinsClient,
|
|
232
|
+
env.SERVICE_FEE_NAME,
|
|
233
|
+
senderAddress,
|
|
234
|
+
)
|
|
235
|
+
tx.transferObjects([nsCoin], feeRecipient)
|
|
236
|
+
|
|
237
|
+
tx.setGasBudget(100_000_000)
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
tx,
|
|
241
|
+
breakdown: {
|
|
242
|
+
suiInputMist,
|
|
243
|
+
nsOutputEstimate: expectedNsOutput,
|
|
244
|
+
registrationCostNsMist,
|
|
245
|
+
slippageBps,
|
|
246
|
+
nsPerSui: nsPrice.nsPerSui,
|
|
247
|
+
source: nsPrice.source,
|
|
248
|
+
priceImpactBps,
|
|
249
|
+
minNsOutput,
|
|
250
|
+
feeRecipient,
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function buildMultiCoinRegisterTx(
|
|
256
|
+
params: MultiCoinRegisterParams,
|
|
257
|
+
env: Env,
|
|
258
|
+
): Promise<MultiCoinRegisterResult> {
|
|
259
|
+
const { domain, years, senderAddress, sourceCoinType, coinObjectIds, expirationMs } = params
|
|
260
|
+
const slippageBps =
|
|
261
|
+
typeof params.slippageBps === 'number' && Number.isFinite(params.slippageBps)
|
|
262
|
+
? Math.max(0, Math.floor(params.slippageBps))
|
|
263
|
+
: DEFAULT_SLIPPAGE_BPS
|
|
264
|
+
|
|
265
|
+
if (!coinObjectIds.length) {
|
|
266
|
+
throw new Error('At least one coin object ID is required')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const cleanDomain = `${domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
|
|
270
|
+
const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
|
|
271
|
+
|
|
272
|
+
const [pricing, nsPrice, pools] = await Promise.all([
|
|
273
|
+
calculateRegistrationPrice({ domain: cleanDomain, years, expirationMs, env }),
|
|
274
|
+
getNSSuiPrice(env, true),
|
|
275
|
+
getDeepBookSuiPools(env),
|
|
276
|
+
])
|
|
277
|
+
|
|
278
|
+
const pool = pools.find((p) => p.coinType === sourceCoinType)
|
|
279
|
+
if (!pool) {
|
|
280
|
+
throw new Error(`No DeepBook pool found for coin type: ${sourceCoinType}`)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const registrationCostNsMist = pricing.nsNeededMist
|
|
284
|
+
|
|
285
|
+
const nsPoolAddress = DEEPBOOK_NS_SUI_POOL[network]
|
|
286
|
+
const deepbookPackage = DEEPBOOK_PACKAGE[network]
|
|
287
|
+
|
|
288
|
+
if (!nsPoolAddress || !deepbookPackage) {
|
|
289
|
+
throw new Error(`DeepBook pools not available on ${network}`)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let suiForNsSwap: bigint
|
|
293
|
+
let minNsOutput: bigint
|
|
294
|
+
let expectedNsOutput: bigint
|
|
295
|
+
let priceImpactBps = 0
|
|
296
|
+
|
|
297
|
+
if (nsPrice.asks?.length) {
|
|
298
|
+
const wideSlippage = slippageBps
|
|
299
|
+
const quote = calculateSuiNeededForNs(registrationCostNsMist, nsPrice.asks, wideSlippage)
|
|
300
|
+
suiForNsSwap = quote.suiNeeded
|
|
301
|
+
expectedNsOutput = quote.expectedNs
|
|
302
|
+
minNsOutput = registrationCostNsMist
|
|
303
|
+
|
|
304
|
+
const simResult = simulateBuyNsWithSui(suiForNsSwap, nsPrice.asks)
|
|
305
|
+
priceImpactBps = simResult.priceImpactBps
|
|
306
|
+
|
|
307
|
+
if (simResult.outputNs < minNsOutput) {
|
|
308
|
+
suiForNsSwap = suiForNsSwap + (suiForNsSwap * 30n) / 100n
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
const bufferBps = Math.max(slippageBps * 3, 3000)
|
|
312
|
+
const nsWithBuffer =
|
|
313
|
+
registrationCostNsMist + (registrationCostNsMist * BigInt(bufferBps)) / 10000n
|
|
314
|
+
const nsTokens = Number(nsWithBuffer) / NS_SCALE
|
|
315
|
+
suiForNsSwap = BigInt(Math.ceil(nsTokens * nsPrice.suiPerNs * 1e9))
|
|
316
|
+
expectedNsOutput = nsWithBuffer
|
|
317
|
+
minNsOutput = registrationCostNsMist
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const extraSui = params.extraSuiForFeesMist ?? 0n
|
|
321
|
+
const totalSuiNeeded = suiForNsSwap + extraSui
|
|
322
|
+
|
|
323
|
+
const tokensNeededFloat = Number(totalSuiNeeded) / 1e9 / pool.suiPerToken
|
|
324
|
+
const tokenMistNeeded = BigInt(Math.ceil(tokensNeededFloat * 10 ** pool.decimals))
|
|
325
|
+
const tokenMistWithSlippage =
|
|
326
|
+
tokenMistNeeded + (tokenMistNeeded * BigInt(slippageBps)) / 10000n
|
|
327
|
+
|
|
328
|
+
const client = new SuiClient({ url: getDefaultRpcUrl(env.SUI_NETWORK), network: env.SUI_NETWORK })
|
|
329
|
+
const suinsClient = new SuinsClient({ client: client as never, network })
|
|
330
|
+
|
|
331
|
+
const tx = new Transaction()
|
|
332
|
+
tx.setSender(senderAddress)
|
|
333
|
+
|
|
334
|
+
const sourceCoin = tx.object(coinObjectIds[0])
|
|
335
|
+
if (coinObjectIds.length > 1) {
|
|
336
|
+
tx.mergeCoins(
|
|
337
|
+
sourceCoin,
|
|
338
|
+
coinObjectIds.slice(1).map((id) => tx.object(id)),
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const [tokenToSell] = tx.splitCoins(sourceCoin, [tx.pure.u64(tokenMistWithSlippage)])
|
|
343
|
+
|
|
344
|
+
const [zeroDeepCoin] = tx.moveCall({
|
|
345
|
+
target: '0x2::coin::zero',
|
|
346
|
+
typeArguments: [DEEP_TYPE],
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
const minSuiFromSwap = suiForNsSwap - (suiForNsSwap * BigInt(slippageBps)) / 10000n
|
|
350
|
+
|
|
351
|
+
let swappedSuiCoin: ReturnType<Transaction['moveCall']>[0]
|
|
352
|
+
if (!pool.isDirect) {
|
|
353
|
+
const suiUsdcPoolAddress = DEEPBOOK_SUI_USDC_POOL[network]
|
|
354
|
+
if (!suiUsdcPoolAddress) {
|
|
355
|
+
throw new Error('SUI/USDC pool not available for indirect swap')
|
|
356
|
+
}
|
|
357
|
+
const [zeroDeep1] = tx.moveCall({
|
|
358
|
+
target: '0x2::coin::zero',
|
|
359
|
+
typeArguments: [DEEP_TYPE],
|
|
360
|
+
})
|
|
361
|
+
const [tokenLeft1, usdcOut, deepLeft1] = tx.moveCall({
|
|
362
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
363
|
+
typeArguments: [sourceCoinType, USDC_TYPE],
|
|
364
|
+
arguments: [
|
|
365
|
+
tx.object(pool.poolAddress),
|
|
366
|
+
tokenToSell,
|
|
367
|
+
zeroDeepCoin,
|
|
368
|
+
tx.pure.u64(0n),
|
|
369
|
+
tx.object(CLOCK_OBJECT),
|
|
370
|
+
],
|
|
371
|
+
})
|
|
372
|
+
const [suiOut, usdcLeft, deepLeft2] = tx.moveCall({
|
|
373
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
374
|
+
typeArguments: [SUI_TYPE, USDC_TYPE],
|
|
375
|
+
arguments: [
|
|
376
|
+
tx.object(suiUsdcPoolAddress),
|
|
377
|
+
usdcOut,
|
|
378
|
+
zeroDeep1,
|
|
379
|
+
tx.pure.u64(minSuiFromSwap),
|
|
380
|
+
tx.object(CLOCK_OBJECT),
|
|
381
|
+
],
|
|
382
|
+
})
|
|
383
|
+
swappedSuiCoin = suiOut
|
|
384
|
+
tx.transferObjects([tokenLeft1, usdcLeft, deepLeft1, deepLeft2, sourceCoin], senderAddress)
|
|
385
|
+
} else if (pool.suiIsBase) {
|
|
386
|
+
const [suiOut, tokenLeft, deepLeft2] = tx.moveCall({
|
|
387
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
388
|
+
typeArguments: [SUI_TYPE, sourceCoinType],
|
|
389
|
+
arguments: [
|
|
390
|
+
tx.object(pool.poolAddress),
|
|
391
|
+
tokenToSell,
|
|
392
|
+
zeroDeepCoin,
|
|
393
|
+
tx.pure.u64(minSuiFromSwap),
|
|
394
|
+
tx.object(CLOCK_OBJECT),
|
|
395
|
+
],
|
|
396
|
+
})
|
|
397
|
+
swappedSuiCoin = suiOut
|
|
398
|
+
tx.transferObjects([tokenLeft, deepLeft2, sourceCoin], senderAddress)
|
|
399
|
+
} else {
|
|
400
|
+
const [tokenLeft, suiOut, deepLeft2] = tx.moveCall({
|
|
401
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
402
|
+
typeArguments: [sourceCoinType, SUI_TYPE],
|
|
403
|
+
arguments: [
|
|
404
|
+
tx.object(pool.poolAddress),
|
|
405
|
+
tokenToSell,
|
|
406
|
+
zeroDeepCoin,
|
|
407
|
+
tx.pure.u64(minSuiFromSwap),
|
|
408
|
+
tx.object(CLOCK_OBJECT),
|
|
409
|
+
],
|
|
410
|
+
})
|
|
411
|
+
swappedSuiCoin = suiOut
|
|
412
|
+
tx.transferObjects([tokenLeft, deepLeft2, sourceCoin], senderAddress)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const [suiCoinForNs] = tx.splitCoins(swappedSuiCoin, [tx.pure.u64(suiForNsSwap)])
|
|
416
|
+
tx.mergeCoins(tx.gas, [swappedSuiCoin])
|
|
417
|
+
|
|
418
|
+
const [zeroDeepForNs] = tx.moveCall({
|
|
419
|
+
target: '0x2::coin::zero',
|
|
420
|
+
typeArguments: [DEEP_TYPE],
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
const [nsCoin, nsLeftoverSui, nsLeftoverDeep] = tx.moveCall({
|
|
424
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
425
|
+
typeArguments: [NS_TYPE_MAINNET, SUI_TYPE],
|
|
426
|
+
arguments: [
|
|
427
|
+
tx.object(nsPoolAddress),
|
|
428
|
+
suiCoinForNs,
|
|
429
|
+
zeroDeepForNs,
|
|
430
|
+
tx.pure.u64(minNsOutput),
|
|
431
|
+
tx.object(CLOCK_OBJECT),
|
|
432
|
+
],
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
tx.transferObjects([nsLeftoverSui, nsLeftoverDeep], senderAddress)
|
|
436
|
+
|
|
437
|
+
const suinsTx = new SuinsTransaction(suinsClient, tx)
|
|
438
|
+
const coinConfig = suinsClient.config.coins.NS
|
|
439
|
+
if (!coinConfig) {
|
|
440
|
+
throw new Error('SuiNS NS coin configuration not found')
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const priceInfoObjectId = coinConfig.feed
|
|
444
|
+
? (await suinsClient.getPriceInfoObject(tx, coinConfig.feed))[0]
|
|
445
|
+
: undefined
|
|
446
|
+
|
|
447
|
+
const nft = suinsTx.register({
|
|
448
|
+
domain: cleanDomain,
|
|
449
|
+
years,
|
|
450
|
+
coinConfig,
|
|
451
|
+
coin: nsCoin,
|
|
452
|
+
priceInfoObjectId,
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
suinsTx.setTargetAddress({
|
|
456
|
+
nft,
|
|
457
|
+
address: senderAddress,
|
|
458
|
+
isSubname: cleanDomain.replace(/\.sui$/i, '').includes('.'),
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
tx.transferObjects([nft], senderAddress)
|
|
462
|
+
|
|
463
|
+
const feeRecipient = await resolveFeeRecipient(
|
|
464
|
+
client,
|
|
465
|
+
suinsClient,
|
|
466
|
+
env.SERVICE_FEE_NAME,
|
|
467
|
+
senderAddress,
|
|
468
|
+
)
|
|
469
|
+
tx.transferObjects([nsCoin], feeRecipient)
|
|
470
|
+
tx.setGasBudget(100_000_000)
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
tx,
|
|
474
|
+
breakdown: {
|
|
475
|
+
suiInputMist: totalSuiNeeded,
|
|
476
|
+
nsOutputEstimate: expectedNsOutput,
|
|
477
|
+
registrationCostNsMist,
|
|
478
|
+
slippageBps,
|
|
479
|
+
nsPerSui: nsPrice.nsPerSui,
|
|
480
|
+
source: nsPrice.source,
|
|
481
|
+
priceImpactBps,
|
|
482
|
+
minNsOutput,
|
|
483
|
+
feeRecipient,
|
|
484
|
+
sourceCoinType,
|
|
485
|
+
sourceTokensNeeded: String(tokenMistWithSlippage),
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export async function buildSuiRegisterTx(
|
|
491
|
+
params: Omit<SwapRegisterParams, 'slippageBps'>,
|
|
492
|
+
env: Env,
|
|
493
|
+
): Promise<Transaction> {
|
|
494
|
+
const { domain, years, senderAddress, expirationMs } = params
|
|
495
|
+
const cleanDomain = `${domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
|
|
496
|
+
const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
|
|
497
|
+
|
|
498
|
+
const client = new SuiClient({ url: getDefaultRpcUrl(env.SUI_NETWORK), network: env.SUI_NETWORK })
|
|
499
|
+
const suinsClient = new SuinsClient({ client: client as never, network })
|
|
500
|
+
|
|
501
|
+
const pricing = await calculateRegistrationPrice({
|
|
502
|
+
domain: cleanDomain,
|
|
503
|
+
years,
|
|
504
|
+
expirationMs,
|
|
505
|
+
env,
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const tx = new Transaction()
|
|
509
|
+
tx.setSender(senderAddress)
|
|
510
|
+
|
|
511
|
+
const suinsTx = new SuinsTransaction(suinsClient, tx)
|
|
512
|
+
const coinConfig = suinsClient.config.coins.SUI
|
|
513
|
+
if (!coinConfig) {
|
|
514
|
+
throw new Error('SuiNS SUI coin configuration not found')
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const priceInfoObjectId = coinConfig.feed
|
|
518
|
+
? (await suinsClient.getPriceInfoObject(tx, coinConfig.feed))[0]
|
|
519
|
+
: undefined
|
|
520
|
+
|
|
521
|
+
const priceWithBuffer = pricing.directSuiMist + (pricing.directSuiMist * 1n) / 100n
|
|
522
|
+
const [paymentCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(priceWithBuffer)])
|
|
523
|
+
|
|
524
|
+
const nft = suinsTx.register({
|
|
525
|
+
domain: cleanDomain,
|
|
526
|
+
years,
|
|
527
|
+
coinConfig,
|
|
528
|
+
coin: paymentCoin,
|
|
529
|
+
priceInfoObjectId,
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
suinsTx.setTargetAddress({
|
|
533
|
+
nft,
|
|
534
|
+
address: senderAddress,
|
|
535
|
+
isSubname: cleanDomain.replace(/\.sui$/i, '').includes('.'),
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
tx.transferObjects([nft], senderAddress)
|
|
539
|
+
tx.mergeCoins(tx.gas, [paymentCoin])
|
|
540
|
+
tx.setGasBudget(100_000_000)
|
|
541
|
+
|
|
542
|
+
return tx
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
interface SwapRenewParams {
|
|
546
|
+
domain: string
|
|
547
|
+
nftId: string
|
|
548
|
+
years: number
|
|
549
|
+
senderAddress: string
|
|
550
|
+
slippageBps?: number
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
interface SwapRenewResult {
|
|
554
|
+
tx: Transaction
|
|
555
|
+
breakdown: SwapBreakdown
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function buildSwapAndRenewTx(
|
|
559
|
+
params: SwapRenewParams,
|
|
560
|
+
env: Env,
|
|
561
|
+
): Promise<SwapRenewResult> {
|
|
562
|
+
const { domain, years, senderAddress, nftId } = params
|
|
563
|
+
const slippageBps = params.slippageBps ?? DEFAULT_SLIPPAGE_BPS
|
|
564
|
+
|
|
565
|
+
const cleanDomain = `${domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
|
|
566
|
+
const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
|
|
567
|
+
|
|
568
|
+
const [pricing, nsPrice] = await Promise.all([
|
|
569
|
+
calculateRenewalPrice({ domain: cleanDomain, years, env }),
|
|
570
|
+
getNSSuiPrice(env, true),
|
|
571
|
+
])
|
|
572
|
+
|
|
573
|
+
const renewalCostNsMist = pricing.nsNeededMist
|
|
574
|
+
|
|
575
|
+
const nsPoolAddress = DEEPBOOK_NS_SUI_POOL[network]
|
|
576
|
+
const deepbookPackage = DEEPBOOK_PACKAGE[network]
|
|
577
|
+
|
|
578
|
+
if (!nsPoolAddress || !deepbookPackage) {
|
|
579
|
+
throw new Error(`DeepBook pools not available on ${network}`)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let suiForNsSwap: bigint
|
|
583
|
+
let minNsOutput: bigint
|
|
584
|
+
let expectedNsOutput: bigint
|
|
585
|
+
let priceImpactBps = 0
|
|
586
|
+
|
|
587
|
+
if (nsPrice.asks?.length) {
|
|
588
|
+
const wideSlippage = Math.max(slippageBps, 1500)
|
|
589
|
+
const quote = calculateSuiNeededForNs(renewalCostNsMist, nsPrice.asks, wideSlippage)
|
|
590
|
+
suiForNsSwap = quote.suiNeeded
|
|
591
|
+
expectedNsOutput = quote.expectedNs
|
|
592
|
+
minNsOutput = renewalCostNsMist
|
|
593
|
+
|
|
594
|
+
const simResult = simulateBuyNsWithSui(suiForNsSwap, nsPrice.asks)
|
|
595
|
+
priceImpactBps = simResult.priceImpactBps
|
|
596
|
+
|
|
597
|
+
if (simResult.outputNs < minNsOutput) {
|
|
598
|
+
const extraBuffer = (suiForNsSwap * 30n) / 100n
|
|
599
|
+
suiForNsSwap = suiForNsSwap + extraBuffer
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
const bufferBps = Math.max(slippageBps * 3, 3000)
|
|
603
|
+
const nsWithBuffer = renewalCostNsMist + (renewalCostNsMist * BigInt(bufferBps)) / 10000n
|
|
604
|
+
const nsTokens = Number(nsWithBuffer) / NS_SCALE
|
|
605
|
+
suiForNsSwap = BigInt(Math.ceil(nsTokens * nsPrice.suiPerNs * 1e9))
|
|
606
|
+
expectedNsOutput = nsWithBuffer
|
|
607
|
+
minNsOutput = renewalCostNsMist
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const suiInputMist = suiForNsSwap
|
|
611
|
+
|
|
612
|
+
const client = new SuiClient({ url: getDefaultRpcUrl(env.SUI_NETWORK), network: env.SUI_NETWORK })
|
|
613
|
+
const suinsClient = new SuinsClient({ client: client as never, network })
|
|
614
|
+
|
|
615
|
+
const tx = new Transaction()
|
|
616
|
+
tx.setSender(senderAddress)
|
|
617
|
+
|
|
618
|
+
const [suiCoinForNs] = tx.splitCoins(tx.gas, [tx.pure.u64(suiForNsSwap)])
|
|
619
|
+
|
|
620
|
+
const [zeroDeepCoin] = tx.moveCall({
|
|
621
|
+
target: '0x2::coin::zero',
|
|
622
|
+
typeArguments: [DEEP_TYPE],
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
const [nsCoin, nsLeftoverSui, nsLeftoverDeep] = tx.moveCall({
|
|
626
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
627
|
+
typeArguments: [NS_TYPE_MAINNET, SUI_TYPE],
|
|
628
|
+
arguments: [
|
|
629
|
+
tx.object(nsPoolAddress),
|
|
630
|
+
suiCoinForNs,
|
|
631
|
+
zeroDeepCoin,
|
|
632
|
+
tx.pure.u64(minNsOutput),
|
|
633
|
+
tx.object(CLOCK_OBJECT),
|
|
634
|
+
],
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
const suinsTx = new SuinsTransaction(suinsClient, tx)
|
|
638
|
+
const coinConfig = suinsClient.config.coins.NS
|
|
639
|
+
if (!coinConfig) {
|
|
640
|
+
throw new Error('SuiNS NS coin configuration not found')
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const priceInfoObjectId = coinConfig.feed
|
|
644
|
+
? (await suinsClient.getPriceInfoObject(tx, coinConfig.feed))[0]
|
|
645
|
+
: undefined
|
|
646
|
+
|
|
647
|
+
suinsTx.renew({
|
|
648
|
+
nft: tx.object(nftId),
|
|
649
|
+
years,
|
|
650
|
+
coinConfig,
|
|
651
|
+
coin: nsCoin,
|
|
652
|
+
priceInfoObjectId,
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
const feeRecipient = await resolveFeeRecipient(
|
|
656
|
+
client,
|
|
657
|
+
suinsClient,
|
|
658
|
+
env.DISCOUNT_RECIPIENT_NAME || 'extra.sui',
|
|
659
|
+
senderAddress,
|
|
660
|
+
)
|
|
661
|
+
const senderLower = senderAddress.toLowerCase()
|
|
662
|
+
const feeRecipientLower = feeRecipient.toLowerCase()
|
|
663
|
+
const residualRecipient = feeRecipientLower === senderLower ? DUST_SINK_ADDRESS : feeRecipient
|
|
664
|
+
|
|
665
|
+
const [postRenewNsLeftover, nsSweepSui, nsSweepDeep] = tx.moveCall({
|
|
666
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
667
|
+
typeArguments: [NS_TYPE_MAINNET, SUI_TYPE],
|
|
668
|
+
arguments: [
|
|
669
|
+
tx.object(nsPoolAddress),
|
|
670
|
+
nsCoin,
|
|
671
|
+
nsLeftoverDeep,
|
|
672
|
+
tx.pure.u64(0),
|
|
673
|
+
tx.object(CLOCK_OBJECT),
|
|
674
|
+
],
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
const [postRenewNsLeftoverDust, nsSweepSuiDust, nsSweepDeepDust] = tx.moveCall({
|
|
678
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
679
|
+
typeArguments: [NS_TYPE_MAINNET, SUI_TYPE],
|
|
680
|
+
arguments: [
|
|
681
|
+
tx.object(nsPoolAddress),
|
|
682
|
+
postRenewNsLeftover,
|
|
683
|
+
nsSweepDeep,
|
|
684
|
+
tx.pure.u64(0),
|
|
685
|
+
tx.object(CLOCK_OBJECT),
|
|
686
|
+
],
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
tx.mergeCoins(nsLeftoverSui, [nsSweepSui, nsSweepSuiDust])
|
|
690
|
+
tx.transferObjects([nsLeftoverSui], feeRecipient)
|
|
691
|
+
tx.transferObjects([postRenewNsLeftoverDust, nsSweepDeepDust], residualRecipient)
|
|
692
|
+
tx.setGasBudget(100_000_000)
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
tx,
|
|
696
|
+
breakdown: {
|
|
697
|
+
suiInputMist,
|
|
698
|
+
nsOutputEstimate: expectedNsOutput,
|
|
699
|
+
registrationCostNsMist: renewalCostNsMist,
|
|
700
|
+
slippageBps,
|
|
701
|
+
nsPerSui: nsPrice.nsPerSui,
|
|
702
|
+
source: nsPrice.source,
|
|
703
|
+
priceImpactBps,
|
|
704
|
+
minNsOutput,
|
|
705
|
+
feeRecipient,
|
|
706
|
+
},
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export async function buildSuiRenewTx(
|
|
711
|
+
params: Omit<SwapRenewParams, 'slippageBps'>,
|
|
712
|
+
env: Env,
|
|
713
|
+
): Promise<Transaction> {
|
|
714
|
+
const { domain, years, senderAddress, nftId } = params
|
|
715
|
+
const cleanDomain = `${domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
|
|
716
|
+
const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
|
|
717
|
+
|
|
718
|
+
const client = new SuiClient({ url: getDefaultRpcUrl(env.SUI_NETWORK), network: env.SUI_NETWORK })
|
|
719
|
+
const suinsClient = new SuinsClient({ client: client as never, network })
|
|
720
|
+
|
|
721
|
+
const pricing = await calculateRenewalPrice({
|
|
722
|
+
domain: cleanDomain,
|
|
723
|
+
years,
|
|
724
|
+
env,
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
const tx = new Transaction()
|
|
728
|
+
tx.setSender(senderAddress)
|
|
729
|
+
|
|
730
|
+
const suinsTx = new SuinsTransaction(suinsClient, tx)
|
|
731
|
+
const coinConfig = suinsClient.config.coins.SUI
|
|
732
|
+
if (!coinConfig) {
|
|
733
|
+
throw new Error('SuiNS SUI coin configuration not found')
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const priceInfoObjectId = coinConfig.feed
|
|
737
|
+
? (await suinsClient.getPriceInfoObject(tx, coinConfig.feed))[0]
|
|
738
|
+
: undefined
|
|
739
|
+
|
|
740
|
+
const priceWithBuffer = pricing.directSuiMist + (pricing.directSuiMist * 1n) / 100n
|
|
741
|
+
const [paymentCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(priceWithBuffer)])
|
|
742
|
+
|
|
743
|
+
suinsTx.renew({
|
|
744
|
+
nft: tx.object(nftId),
|
|
745
|
+
years,
|
|
746
|
+
coinConfig,
|
|
747
|
+
coin: paymentCoin,
|
|
748
|
+
priceInfoObjectId,
|
|
749
|
+
})
|
|
750
|
+
tx.mergeCoins(tx.gas, [paymentCoin])
|
|
751
|
+
|
|
752
|
+
tx.setGasBudget(100_000_000)
|
|
753
|
+
|
|
754
|
+
return tx
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
interface MultiCoinRenewParams {
|
|
758
|
+
domain: string
|
|
759
|
+
nftId: string
|
|
760
|
+
years: number
|
|
761
|
+
senderAddress: string
|
|
762
|
+
sourceCoinType: string
|
|
763
|
+
coinObjectIds: string[]
|
|
764
|
+
slippageBps?: number
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
interface MultiCoinRenewResult {
|
|
768
|
+
tx: Transaction
|
|
769
|
+
breakdown: SwapBreakdown & {
|
|
770
|
+
sourceCoinType: string
|
|
771
|
+
sourceTokensNeeded: string
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export async function buildMultiCoinRenewTx(
|
|
776
|
+
params: MultiCoinRenewParams,
|
|
777
|
+
env: Env,
|
|
778
|
+
): Promise<MultiCoinRenewResult> {
|
|
779
|
+
const { domain, years, senderAddress, nftId, sourceCoinType, coinObjectIds } = params
|
|
780
|
+
const slippageBps =
|
|
781
|
+
typeof params.slippageBps === 'number' && Number.isFinite(params.slippageBps)
|
|
782
|
+
? Math.max(0, Math.floor(params.slippageBps))
|
|
783
|
+
: DEFAULT_SLIPPAGE_BPS
|
|
784
|
+
|
|
785
|
+
if (!coinObjectIds.length) {
|
|
786
|
+
throw new Error('At least one coin object ID is required')
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const cleanDomain = `${domain.toLowerCase().replace(/\.sui$/i, '')}.sui`
|
|
790
|
+
const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
|
|
791
|
+
|
|
792
|
+
const [pricing, nsPrice, pools] = await Promise.all([
|
|
793
|
+
calculateRenewalPrice({ domain: cleanDomain, years, env }),
|
|
794
|
+
getNSSuiPrice(env, true),
|
|
795
|
+
getDeepBookSuiPools(env),
|
|
796
|
+
])
|
|
797
|
+
|
|
798
|
+
const pool = pools.find((p) => p.coinType === sourceCoinType)
|
|
799
|
+
if (!pool) {
|
|
800
|
+
throw new Error(`No DeepBook pool found for coin type: ${sourceCoinType}`)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const renewalCostNsMist = pricing.nsNeededMist
|
|
804
|
+
|
|
805
|
+
const nsPoolAddress = DEEPBOOK_NS_SUI_POOL[network]
|
|
806
|
+
const deepbookPackage = DEEPBOOK_PACKAGE[network]
|
|
807
|
+
|
|
808
|
+
if (!nsPoolAddress || !deepbookPackage) {
|
|
809
|
+
throw new Error(`DeepBook pools not available on ${network}`)
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
let suiForNsSwap: bigint
|
|
813
|
+
let minNsOutput: bigint
|
|
814
|
+
let expectedNsOutput: bigint
|
|
815
|
+
let priceImpactBps = 0
|
|
816
|
+
|
|
817
|
+
if (nsPrice.asks?.length) {
|
|
818
|
+
const wideSlippage = slippageBps
|
|
819
|
+
const quote = calculateSuiNeededForNs(renewalCostNsMist, nsPrice.asks, wideSlippage)
|
|
820
|
+
suiForNsSwap = quote.suiNeeded
|
|
821
|
+
expectedNsOutput = quote.expectedNs
|
|
822
|
+
minNsOutput = renewalCostNsMist
|
|
823
|
+
|
|
824
|
+
const simResult = simulateBuyNsWithSui(suiForNsSwap, nsPrice.asks)
|
|
825
|
+
priceImpactBps = simResult.priceImpactBps
|
|
826
|
+
|
|
827
|
+
if (simResult.outputNs < minNsOutput) {
|
|
828
|
+
suiForNsSwap = suiForNsSwap + (suiForNsSwap * 30n) / 100n
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
const bufferBps = Math.max(slippageBps * 3, 3000)
|
|
832
|
+
const nsWithBuffer = renewalCostNsMist + (renewalCostNsMist * BigInt(bufferBps)) / 10000n
|
|
833
|
+
const nsTokens = Number(nsWithBuffer) / NS_SCALE
|
|
834
|
+
suiForNsSwap = BigInt(Math.ceil(nsTokens * nsPrice.suiPerNs * 1e9))
|
|
835
|
+
expectedNsOutput = nsWithBuffer
|
|
836
|
+
minNsOutput = renewalCostNsMist
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const totalSuiNeeded = suiForNsSwap
|
|
840
|
+
|
|
841
|
+
const tokensNeededFloat = Number(totalSuiNeeded) / 1e9 / pool.suiPerToken
|
|
842
|
+
const tokenMistNeeded = BigInt(Math.ceil(tokensNeededFloat * 10 ** pool.decimals))
|
|
843
|
+
const tokenMistWithSlippage =
|
|
844
|
+
tokenMistNeeded + (tokenMistNeeded * BigInt(slippageBps)) / 10000n
|
|
845
|
+
|
|
846
|
+
const client = new SuiClient({ url: getDefaultRpcUrl(env.SUI_NETWORK), network: env.SUI_NETWORK })
|
|
847
|
+
const suinsClient = new SuinsClient({ client: client as never, network })
|
|
848
|
+
|
|
849
|
+
const tx = new Transaction()
|
|
850
|
+
tx.setSender(senderAddress)
|
|
851
|
+
|
|
852
|
+
const sourceCoin = tx.object(coinObjectIds[0])
|
|
853
|
+
if (coinObjectIds.length > 1) {
|
|
854
|
+
tx.mergeCoins(
|
|
855
|
+
sourceCoin,
|
|
856
|
+
coinObjectIds.slice(1).map((id) => tx.object(id)),
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const [tokenToSell] = tx.splitCoins(sourceCoin, [tx.pure.u64(tokenMistWithSlippage)])
|
|
861
|
+
|
|
862
|
+
const [zeroDeepCoin] = tx.moveCall({
|
|
863
|
+
target: '0x2::coin::zero',
|
|
864
|
+
typeArguments: [DEEP_TYPE],
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
const minSuiFromSwap = suiForNsSwap - (suiForNsSwap * BigInt(slippageBps)) / 10000n
|
|
868
|
+
|
|
869
|
+
let swappedSuiCoin: ReturnType<Transaction['moveCall']>[0]
|
|
870
|
+
if (!pool.isDirect) {
|
|
871
|
+
const suiUsdcPoolAddress = DEEPBOOK_SUI_USDC_POOL[network]
|
|
872
|
+
if (!suiUsdcPoolAddress) {
|
|
873
|
+
throw new Error('SUI/USDC pool not available for indirect swap')
|
|
874
|
+
}
|
|
875
|
+
const [zeroDeep1] = tx.moveCall({
|
|
876
|
+
target: '0x2::coin::zero',
|
|
877
|
+
typeArguments: [DEEP_TYPE],
|
|
878
|
+
})
|
|
879
|
+
const [tokenLeft1, usdcOut, deepLeft1] = tx.moveCall({
|
|
880
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
881
|
+
typeArguments: [sourceCoinType, USDC_TYPE],
|
|
882
|
+
arguments: [
|
|
883
|
+
tx.object(pool.poolAddress),
|
|
884
|
+
tokenToSell,
|
|
885
|
+
zeroDeepCoin,
|
|
886
|
+
tx.pure.u64(0n),
|
|
887
|
+
tx.object(CLOCK_OBJECT),
|
|
888
|
+
],
|
|
889
|
+
})
|
|
890
|
+
const [suiOut, usdcLeft, deepLeft2] = tx.moveCall({
|
|
891
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
892
|
+
typeArguments: [SUI_TYPE, USDC_TYPE],
|
|
893
|
+
arguments: [
|
|
894
|
+
tx.object(suiUsdcPoolAddress),
|
|
895
|
+
usdcOut,
|
|
896
|
+
zeroDeep1,
|
|
897
|
+
tx.pure.u64(minSuiFromSwap),
|
|
898
|
+
tx.object(CLOCK_OBJECT),
|
|
899
|
+
],
|
|
900
|
+
})
|
|
901
|
+
swappedSuiCoin = suiOut
|
|
902
|
+
tx.transferObjects([tokenLeft1, usdcLeft, deepLeft1, deepLeft2, sourceCoin], senderAddress)
|
|
903
|
+
} else if (pool.suiIsBase) {
|
|
904
|
+
const [suiOut, tokenLeft, deepLeft2] = tx.moveCall({
|
|
905
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
906
|
+
typeArguments: [SUI_TYPE, sourceCoinType],
|
|
907
|
+
arguments: [
|
|
908
|
+
tx.object(pool.poolAddress),
|
|
909
|
+
tokenToSell,
|
|
910
|
+
zeroDeepCoin,
|
|
911
|
+
tx.pure.u64(minSuiFromSwap),
|
|
912
|
+
tx.object(CLOCK_OBJECT),
|
|
913
|
+
],
|
|
914
|
+
})
|
|
915
|
+
swappedSuiCoin = suiOut
|
|
916
|
+
tx.transferObjects([tokenLeft, deepLeft2, sourceCoin], senderAddress)
|
|
917
|
+
} else {
|
|
918
|
+
const [tokenLeft, suiOut, deepLeft2] = tx.moveCall({
|
|
919
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
920
|
+
typeArguments: [sourceCoinType, SUI_TYPE],
|
|
921
|
+
arguments: [
|
|
922
|
+
tx.object(pool.poolAddress),
|
|
923
|
+
tokenToSell,
|
|
924
|
+
zeroDeepCoin,
|
|
925
|
+
tx.pure.u64(minSuiFromSwap),
|
|
926
|
+
tx.object(CLOCK_OBJECT),
|
|
927
|
+
],
|
|
928
|
+
})
|
|
929
|
+
swappedSuiCoin = suiOut
|
|
930
|
+
tx.transferObjects([tokenLeft, deepLeft2, sourceCoin], senderAddress)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const [suiCoinForNs] = tx.splitCoins(swappedSuiCoin, [tx.pure.u64(suiForNsSwap)])
|
|
934
|
+
tx.mergeCoins(tx.gas, [swappedSuiCoin])
|
|
935
|
+
|
|
936
|
+
const [zeroDeepForNs] = tx.moveCall({
|
|
937
|
+
target: '0x2::coin::zero',
|
|
938
|
+
typeArguments: [DEEP_TYPE],
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
const [nsCoin, nsLeftoverSui, nsLeftoverDeep] = tx.moveCall({
|
|
942
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
943
|
+
typeArguments: [NS_TYPE_MAINNET, SUI_TYPE],
|
|
944
|
+
arguments: [
|
|
945
|
+
tx.object(nsPoolAddress),
|
|
946
|
+
suiCoinForNs,
|
|
947
|
+
zeroDeepForNs,
|
|
948
|
+
tx.pure.u64(minNsOutput),
|
|
949
|
+
tx.object(CLOCK_OBJECT),
|
|
950
|
+
],
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
const suinsTx = new SuinsTransaction(suinsClient, tx)
|
|
954
|
+
const coinConfig = suinsClient.config.coins.NS
|
|
955
|
+
if (!coinConfig) {
|
|
956
|
+
throw new Error('SuiNS NS coin configuration not found')
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const priceInfoObjectId = coinConfig.feed
|
|
960
|
+
? (await suinsClient.getPriceInfoObject(tx, coinConfig.feed))[0]
|
|
961
|
+
: undefined
|
|
962
|
+
|
|
963
|
+
suinsTx.renew({
|
|
964
|
+
nft: tx.object(nftId),
|
|
965
|
+
years,
|
|
966
|
+
coinConfig,
|
|
967
|
+
coin: nsCoin,
|
|
968
|
+
priceInfoObjectId,
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
const feeRecipient = await resolveFeeRecipient(
|
|
972
|
+
client,
|
|
973
|
+
suinsClient,
|
|
974
|
+
env.DISCOUNT_RECIPIENT_NAME || 'extra.sui',
|
|
975
|
+
senderAddress,
|
|
976
|
+
)
|
|
977
|
+
const senderLower = senderAddress.toLowerCase()
|
|
978
|
+
const feeRecipientLower = feeRecipient.toLowerCase()
|
|
979
|
+
const residualRecipient = feeRecipientLower === senderLower ? DUST_SINK_ADDRESS : feeRecipient
|
|
980
|
+
|
|
981
|
+
const [postRenewNsLeftover, nsSweepSui, nsSweepDeep] = tx.moveCall({
|
|
982
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
983
|
+
typeArguments: [NS_TYPE_MAINNET, SUI_TYPE],
|
|
984
|
+
arguments: [
|
|
985
|
+
tx.object(nsPoolAddress),
|
|
986
|
+
nsCoin,
|
|
987
|
+
nsLeftoverDeep,
|
|
988
|
+
tx.pure.u64(0),
|
|
989
|
+
tx.object(CLOCK_OBJECT),
|
|
990
|
+
],
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
const [postRenewNsLeftoverDust, nsSweepSuiDust, nsSweepDeepDust] = tx.moveCall({
|
|
994
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
995
|
+
typeArguments: [NS_TYPE_MAINNET, SUI_TYPE],
|
|
996
|
+
arguments: [
|
|
997
|
+
tx.object(nsPoolAddress),
|
|
998
|
+
postRenewNsLeftover,
|
|
999
|
+
nsSweepDeep,
|
|
1000
|
+
tx.pure.u64(0),
|
|
1001
|
+
tx.object(CLOCK_OBJECT),
|
|
1002
|
+
],
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
tx.mergeCoins(nsLeftoverSui, [nsSweepSui, nsSweepSuiDust])
|
|
1006
|
+
tx.transferObjects([nsLeftoverSui], feeRecipient)
|
|
1007
|
+
tx.transferObjects([postRenewNsLeftoverDust, nsSweepDeepDust], residualRecipient)
|
|
1008
|
+
tx.setGasBudget(100_000_000)
|
|
1009
|
+
|
|
1010
|
+
return {
|
|
1011
|
+
tx,
|
|
1012
|
+
breakdown: {
|
|
1013
|
+
suiInputMist: totalSuiNeeded,
|
|
1014
|
+
nsOutputEstimate: expectedNsOutput,
|
|
1015
|
+
registrationCostNsMist: renewalCostNsMist,
|
|
1016
|
+
slippageBps,
|
|
1017
|
+
nsPerSui: nsPrice.nsPerSui,
|
|
1018
|
+
source: nsPrice.source,
|
|
1019
|
+
priceImpactBps,
|
|
1020
|
+
minNsOutput,
|
|
1021
|
+
feeRecipient,
|
|
1022
|
+
sourceCoinType,
|
|
1023
|
+
sourceTokensNeeded: String(tokenMistWithSlippage),
|
|
1024
|
+
},
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
interface GasSwapInfo {
|
|
1029
|
+
pool: import('./ns-price').SwappablePool
|
|
1030
|
+
coinObjectIds: string[]
|
|
1031
|
+
amountToSell: bigint
|
|
1032
|
+
minSuiOut: bigint
|
|
1033
|
+
tokenCoinType: string
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async function findBestGasSwap(
|
|
1037
|
+
client: SuiClient,
|
|
1038
|
+
sender: string,
|
|
1039
|
+
shortfallMist: bigint,
|
|
1040
|
+
env: Env,
|
|
1041
|
+
): Promise<GasSwapInfo | null> {
|
|
1042
|
+
const pools = await getDeepBookSuiPools(env)
|
|
1043
|
+
if (pools.length === 0) return null
|
|
1044
|
+
|
|
1045
|
+
const poolBalances = await Promise.all(
|
|
1046
|
+
pools.map((p) => client.getBalance({ owner: sender, coinType: p.coinType })),
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
const candidates: Array<{ pool: (typeof pools)[0]; balance: bigint }> = []
|
|
1050
|
+
for (let i = 0; i < pools.length; i++) {
|
|
1051
|
+
const bal = BigInt(poolBalances[i]?.totalBalance ?? '0')
|
|
1052
|
+
if (bal > 0n) candidates.push({ pool: pools[i], balance: bal })
|
|
1053
|
+
}
|
|
1054
|
+
if (candidates.length === 0) return null
|
|
1055
|
+
|
|
1056
|
+
const slippageBps = Math.max(DEFAULT_SLIPPAGE_BPS, 800)
|
|
1057
|
+
let best: GasSwapInfo | null = null
|
|
1058
|
+
let bestSuiValue = 0n
|
|
1059
|
+
|
|
1060
|
+
for (const { pool, balance } of candidates) {
|
|
1061
|
+
const shortfallSui = Number(shortfallMist) / 1e9
|
|
1062
|
+
const tokensNeeded = shortfallSui / pool.suiPerToken
|
|
1063
|
+
const tokenMistNeeded = BigInt(Math.ceil(tokensNeeded * 10 ** pool.decimals))
|
|
1064
|
+
const tokenMistWithBuffer = (tokenMistNeeded * BigInt(10000 + slippageBps)) / 10000n
|
|
1065
|
+
const maxSellable = (balance * 95n) / 100n
|
|
1066
|
+
const amountToSell =
|
|
1067
|
+
tokenMistWithBuffer > maxSellable ? maxSellable : tokenMistWithBuffer
|
|
1068
|
+
|
|
1069
|
+
const expectedSui =
|
|
1070
|
+
(Number(amountToSell) / 10 ** pool.decimals) * pool.suiPerToken
|
|
1071
|
+
const minSuiOut = BigInt(Math.floor(expectedSui * 0.8 * 1e9))
|
|
1072
|
+
|
|
1073
|
+
if (minSuiOut <= 0n) continue
|
|
1074
|
+
|
|
1075
|
+
const coinsRes = await client.getCoins({
|
|
1076
|
+
owner: sender,
|
|
1077
|
+
coinType: pool.coinType,
|
|
1078
|
+
limit: 50,
|
|
1079
|
+
})
|
|
1080
|
+
const coinIds = coinsRes.data
|
|
1081
|
+
.filter((c) => typeof c.coinObjectId === 'string')
|
|
1082
|
+
.map((c) => c.coinObjectId as string)
|
|
1083
|
+
if (coinIds.length === 0) continue
|
|
1084
|
+
|
|
1085
|
+
const suiValue = minSuiOut
|
|
1086
|
+
if (suiValue > bestSuiValue) {
|
|
1087
|
+
bestSuiValue = suiValue
|
|
1088
|
+
best = {
|
|
1089
|
+
pool,
|
|
1090
|
+
coinObjectIds: coinIds,
|
|
1091
|
+
amountToSell,
|
|
1092
|
+
minSuiOut,
|
|
1093
|
+
tokenCoinType: pool.coinType,
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
return best
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function addTokenToSuiGasSwap(
|
|
1102
|
+
tx: Transaction,
|
|
1103
|
+
swapInfo: GasSwapInfo,
|
|
1104
|
+
sender: string,
|
|
1105
|
+
network: 'mainnet' | 'testnet',
|
|
1106
|
+
): void {
|
|
1107
|
+
const deepbookPackage = DEEPBOOK_PACKAGE[network]
|
|
1108
|
+
const suiUsdcPoolAddress = DEEPBOOK_SUI_USDC_POOL[network]
|
|
1109
|
+
if (!deepbookPackage) return
|
|
1110
|
+
|
|
1111
|
+
const sourceCoin = tx.object(swapInfo.coinObjectIds[0])
|
|
1112
|
+
if (swapInfo.coinObjectIds.length > 1) {
|
|
1113
|
+
tx.mergeCoins(
|
|
1114
|
+
sourceCoin,
|
|
1115
|
+
swapInfo.coinObjectIds.slice(1).map((id) => tx.object(id)),
|
|
1116
|
+
)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const [tokenToSell] = tx.splitCoins(sourceCoin, [tx.pure.u64(swapInfo.amountToSell)])
|
|
1120
|
+
|
|
1121
|
+
const [zeroDeepCoin] = tx.moveCall({
|
|
1122
|
+
target: '0x2::coin::zero',
|
|
1123
|
+
typeArguments: [DEEP_TYPE],
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
const minSuiFromSwap =
|
|
1127
|
+
swapInfo.minSuiOut -
|
|
1128
|
+
(swapInfo.minSuiOut * BigInt(DEFAULT_SLIPPAGE_BPS)) / 10000n
|
|
1129
|
+
|
|
1130
|
+
let swappedSui: ReturnType<Transaction['moveCall']>[0]
|
|
1131
|
+
if (!swapInfo.pool.isDirect && swapInfo.pool.usdcPoolAddress && suiUsdcPoolAddress) {
|
|
1132
|
+
const [zeroDeep1] = tx.moveCall({
|
|
1133
|
+
target: '0x2::coin::zero',
|
|
1134
|
+
typeArguments: [DEEP_TYPE],
|
|
1135
|
+
})
|
|
1136
|
+
const [tokenLeft1, usdcOut, deepLeft1] = tx.moveCall({
|
|
1137
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
1138
|
+
typeArguments: [swapInfo.tokenCoinType, USDC_TYPE],
|
|
1139
|
+
arguments: [
|
|
1140
|
+
tx.object(swapInfo.pool.poolAddress),
|
|
1141
|
+
tokenToSell,
|
|
1142
|
+
zeroDeepCoin,
|
|
1143
|
+
tx.pure.u64(0n),
|
|
1144
|
+
tx.object(CLOCK_OBJECT),
|
|
1145
|
+
],
|
|
1146
|
+
})
|
|
1147
|
+
const [suiOut, usdcLeft, deepLeft2] = tx.moveCall({
|
|
1148
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
1149
|
+
typeArguments: [SUI_TYPE, USDC_TYPE],
|
|
1150
|
+
arguments: [
|
|
1151
|
+
tx.object(suiUsdcPoolAddress),
|
|
1152
|
+
usdcOut,
|
|
1153
|
+
zeroDeep1,
|
|
1154
|
+
tx.pure.u64(minSuiFromSwap),
|
|
1155
|
+
tx.object(CLOCK_OBJECT),
|
|
1156
|
+
],
|
|
1157
|
+
})
|
|
1158
|
+
swappedSui = suiOut
|
|
1159
|
+
tx.transferObjects([tokenLeft1, usdcLeft, deepLeft1, deepLeft2, sourceCoin], sender)
|
|
1160
|
+
} else if (swapInfo.pool.suiIsBase) {
|
|
1161
|
+
const [suiOut, tokenLeft, deepLeft2] = tx.moveCall({
|
|
1162
|
+
target: `${deepbookPackage}::pool::swap_exact_quote_for_base`,
|
|
1163
|
+
typeArguments: [SUI_TYPE, swapInfo.tokenCoinType],
|
|
1164
|
+
arguments: [
|
|
1165
|
+
tx.object(swapInfo.pool.poolAddress),
|
|
1166
|
+
tokenToSell,
|
|
1167
|
+
zeroDeepCoin,
|
|
1168
|
+
tx.pure.u64(minSuiFromSwap),
|
|
1169
|
+
tx.object(CLOCK_OBJECT),
|
|
1170
|
+
],
|
|
1171
|
+
})
|
|
1172
|
+
swappedSui = suiOut
|
|
1173
|
+
tx.transferObjects([tokenLeft, deepLeft2, sourceCoin], sender)
|
|
1174
|
+
} else {
|
|
1175
|
+
const [tokenLeft, suiOut, deepLeft2] = tx.moveCall({
|
|
1176
|
+
target: `${deepbookPackage}::pool::swap_exact_base_for_quote`,
|
|
1177
|
+
typeArguments: [swapInfo.tokenCoinType, SUI_TYPE],
|
|
1178
|
+
arguments: [
|
|
1179
|
+
tx.object(swapInfo.pool.poolAddress),
|
|
1180
|
+
tokenToSell,
|
|
1181
|
+
zeroDeepCoin,
|
|
1182
|
+
tx.pure.u64(minSuiFromSwap),
|
|
1183
|
+
tx.object(CLOCK_OBJECT),
|
|
1184
|
+
],
|
|
1185
|
+
})
|
|
1186
|
+
swappedSui = suiOut
|
|
1187
|
+
tx.transferObjects([tokenLeft, deepLeft2, sourceCoin], sender)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
tx.mergeCoins(tx.gas, [swappedSui])
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
export async function prependGasSwapIfNeeded(
|
|
1194
|
+
tx: Transaction,
|
|
1195
|
+
client: SuiClient,
|
|
1196
|
+
sender: string,
|
|
1197
|
+
suiNeededMist: bigint,
|
|
1198
|
+
env: Env,
|
|
1199
|
+
): Promise<Transaction> {
|
|
1200
|
+
const balanceRes = await client.getBalance({ owner: sender, coinType: SUI_TYPE })
|
|
1201
|
+
const availableMist = BigInt(balanceRes?.totalBalance ?? '0')
|
|
1202
|
+
if (availableMist >= suiNeededMist) return tx
|
|
1203
|
+
|
|
1204
|
+
const shortfall = suiNeededMist - availableMist
|
|
1205
|
+
const swapInfo = await findBestGasSwap(client, sender, shortfall, env)
|
|
1206
|
+
if (!swapInfo) return tx
|
|
1207
|
+
|
|
1208
|
+
const network = env.SUI_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
|
|
1209
|
+
const swapTx = new Transaction()
|
|
1210
|
+
swapTx.setSender(sender)
|
|
1211
|
+
addTokenToSuiGasSwap(swapTx, swapInfo, sender, network)
|
|
1212
|
+
|
|
1213
|
+
const mainData = tx.getData()
|
|
1214
|
+
const swapData = swapTx.getData()
|
|
1215
|
+
const combined = TransactionDataBuilder.restore(
|
|
1216
|
+
mainData as Parameters<typeof TransactionDataBuilder.restore>[0],
|
|
1217
|
+
)
|
|
1218
|
+
combined.insertTransaction(0, swapData as Parameters<typeof combined.insertTransaction>[1])
|
|
1219
|
+
|
|
1220
|
+
const restored = Transaction.from(JSON.stringify(combined.snapshot()))
|
|
1221
|
+
return restored
|
|
1222
|
+
}
|