@lamdanghoang/sui-pay-wal 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/README.md +263 -0
- package/contracts/Move.toml +9 -0
- package/contracts/sources/wal_exchange_pyth.move +315 -0
- package/dist/index.d.mts +350 -0
- package/dist/index.d.ts +350 -0
- package/dist/index.js +679 -0
- package/dist/index.mjs +619 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# @lamdanghoang/sui-pay
|
|
2
|
+
|
|
3
|
+
Pay Walrus storage costs with SUI directly — no need to acquire WAL tokens separately.
|
|
4
|
+
|
|
5
|
+
This SDK provides automatic SUI → WAL swapping using Pyth Oracle for real-time pricing, making it seamless for users to pay for Walrus decentralized storage.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔄 **Automatic SUI → WAL swap** via Pyth Oracle pricing
|
|
10
|
+
- 💰 **Accurate cost estimation** using Walrus pricing formula
|
|
11
|
+
- 🔌 **Wallet agnostic** — works with any Sui wallet
|
|
12
|
+
- 📦 **Zero WAL required** — users only need SUI
|
|
13
|
+
- ⚡ **Single transaction** swap execution
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @lamdanghoang/sui-pay @mysten/sui
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { WalrusSuiPay } from '@lamdanghoang/sui-pay'
|
|
25
|
+
|
|
26
|
+
// Initialize the client
|
|
27
|
+
const client = new WalrusSuiPay({
|
|
28
|
+
network: 'testnet',
|
|
29
|
+
exchangePackageId: '0x...', // Your deployed contract
|
|
30
|
+
exchangeObjectId: '0x...', // Your exchange object
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Check if user has enough WAL, get SUI needed if not
|
|
34
|
+
const check = await client.checkBalance(
|
|
35
|
+
userAddress,
|
|
36
|
+
fileSize, // bytes
|
|
37
|
+
3 // epochs
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if (!check.sufficient) {
|
|
41
|
+
console.log(`Need to swap ${check.suiNeeded} SUI for WAL`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Ensure WAL balance before upload (auto-swaps if needed)
|
|
45
|
+
const result = await client.ensureWalBalance(
|
|
46
|
+
fileSize,
|
|
47
|
+
epochs,
|
|
48
|
+
userAddress,
|
|
49
|
+
signAndExecute // Your wallet's signing function
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if (result.success) {
|
|
53
|
+
// Now upload to Walrus...
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### `WalrusSuiPay`
|
|
60
|
+
|
|
61
|
+
Main client class for swap operations.
|
|
62
|
+
|
|
63
|
+
#### Constructor
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
new WalrusSuiPay({
|
|
67
|
+
network?: 'testnet' | 'mainnet', // Default: 'testnet'
|
|
68
|
+
rpcUrl?: string, // Custom RPC URL
|
|
69
|
+
exchangePackageId: string, // WAL Exchange contract package ID
|
|
70
|
+
exchangeObjectId: string, // WAL Exchange object ID
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Methods
|
|
75
|
+
|
|
76
|
+
##### `getWalBalance(address: string): Promise<bigint>`
|
|
77
|
+
Get WAL balance for an address (in FROST, 1 WAL = 1e9 FROST).
|
|
78
|
+
|
|
79
|
+
##### `getSuiBalance(address: string): Promise<bigint>`
|
|
80
|
+
Get SUI balance for an address.
|
|
81
|
+
|
|
82
|
+
##### `quoteSuiToWal(suiAmount: bigint): Promise<SwapQuote | null>`
|
|
83
|
+
Get a quote for swapping SUI to WAL.
|
|
84
|
+
|
|
85
|
+
##### `quoteWalToSui(walAmount: bigint): Promise<SwapQuote | null>`
|
|
86
|
+
Get a quote for swapping WAL to SUI.
|
|
87
|
+
|
|
88
|
+
##### `checkBalance(address, fileSizeBytes, epochs, bufferPercent?): Promise<BalanceCheckResult>`
|
|
89
|
+
Check if user has enough WAL for storage, calculate SUI needed if not.
|
|
90
|
+
|
|
91
|
+
##### `ensureWalBalance(fileSizeBytes, epochs, address, signAndExecute, bufferPercent?): Promise<Result>`
|
|
92
|
+
Ensure user has enough WAL, automatically swapping SUI if needed.
|
|
93
|
+
|
|
94
|
+
##### `createSwapSuiToWalTransaction(suiAmount, slippageBps?): Promise<SwapTransactionResult | null>`
|
|
95
|
+
Create a swap transaction for wallet signing.
|
|
96
|
+
|
|
97
|
+
##### `swapSuiToWal(suiAmount, signAndExecute, slippageBps?): Promise<SwapResult>`
|
|
98
|
+
Execute a SUI → WAL swap.
|
|
99
|
+
|
|
100
|
+
### Storage Cost Utilities
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import {
|
|
104
|
+
calculateStorageCost,
|
|
105
|
+
calculateStorageCostLive,
|
|
106
|
+
estimateWalNeeded,
|
|
107
|
+
estimateWalNeededLive,
|
|
108
|
+
getWalrusSystemInfo,
|
|
109
|
+
formatWal,
|
|
110
|
+
formatSui
|
|
111
|
+
} from '@lamdanghoang/sui-pay'
|
|
112
|
+
|
|
113
|
+
// Calculate with default pricing (fast, no RPC)
|
|
114
|
+
const cost = calculateStorageCost(fileSize, epochs)
|
|
115
|
+
console.log(`Storage: ${cost.totalWal} WAL`)
|
|
116
|
+
|
|
117
|
+
// Calculate with live on-chain pricing (accurate)
|
|
118
|
+
const costLive = await calculateStorageCostLive(fileSize, epochs, 'testnet')
|
|
119
|
+
console.log(`Storage: ${costLive.totalWal} WAL (live pricing)`)
|
|
120
|
+
console.log(`System info:`, costLive.systemInfo)
|
|
121
|
+
|
|
122
|
+
// Estimate with buffer (default pricing)
|
|
123
|
+
const walNeeded = estimateWalNeeded(fileSize, epochs, 20) // 20% buffer
|
|
124
|
+
|
|
125
|
+
// Estimate with buffer (live pricing)
|
|
126
|
+
const walNeededLive = await estimateWalNeededLive(fileSize, epochs, 'testnet', 20)
|
|
127
|
+
|
|
128
|
+
// Get system info directly
|
|
129
|
+
const systemInfo = await getWalrusSystemInfo('testnet')
|
|
130
|
+
console.log(`nShards: ${systemInfo.nShards}`)
|
|
131
|
+
console.log(`Storage price: ${systemInfo.storagePricePerUnit} FROST/unit/epoch`)
|
|
132
|
+
console.log(`Write price: ${systemInfo.writePricePerUnit} FROST/unit`)
|
|
133
|
+
|
|
134
|
+
// Format amounts
|
|
135
|
+
console.log(formatWal(1000000000n)) // "1.0000"
|
|
136
|
+
console.log(formatSui(1000000000n)) // "1.0000"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Pyth Oracle Functions
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { getSwapPrices, getSuiUsdPrice, getWalUsdPrice } from '@lamdanghoang/sui-pay'
|
|
143
|
+
|
|
144
|
+
// Get both prices
|
|
145
|
+
const prices = await getSwapPrices()
|
|
146
|
+
console.log(`SUI: $${prices.suiUsd}, WAL: $${prices.walUsd}`)
|
|
147
|
+
console.log(`Rate: 1 SUI = ${prices.suiWalRate} WAL`)
|
|
148
|
+
|
|
149
|
+
// Get individual prices
|
|
150
|
+
const sui = await getSuiUsdPrice()
|
|
151
|
+
const wal = await getWalUsdPrice()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Usage with Different Wallets
|
|
155
|
+
|
|
156
|
+
### With @mysten/dapp-kit (React)
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { useSignAndExecuteTransaction } from '@mysten/dapp-kit'
|
|
160
|
+
|
|
161
|
+
function UploadComponent() {
|
|
162
|
+
const { mutateAsync: signAndExecute } = useSignAndExecuteTransaction()
|
|
163
|
+
|
|
164
|
+
const handleUpload = async () => {
|
|
165
|
+
await client.ensureWalBalance(
|
|
166
|
+
fileSize,
|
|
167
|
+
epochs,
|
|
168
|
+
address,
|
|
169
|
+
signAndExecute
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### With Sui Wallet Standard
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const signAndExecute = async ({ transaction }) => {
|
|
179
|
+
const result = await wallet.signAndExecuteTransactionBlock({
|
|
180
|
+
transactionBlock: transaction,
|
|
181
|
+
})
|
|
182
|
+
return { digest: result.digest }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await client.ensureWalBalance(fileSize, epochs, address, signAndExecute)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### With zkLogin
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { signWithZkLogin } from './your-zklogin-utils'
|
|
192
|
+
|
|
193
|
+
const signAndExecute = async ({ transaction }) => {
|
|
194
|
+
const txBytes = await transaction.build({ client: suiClient })
|
|
195
|
+
const signature = await signWithZkLogin(txBytes, credentials)
|
|
196
|
+
|
|
197
|
+
const result = await suiClient.executeTransactionBlock({
|
|
198
|
+
transactionBlock: txBytes,
|
|
199
|
+
signature,
|
|
200
|
+
})
|
|
201
|
+
return { digest: result.digest }
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Contract Deployment
|
|
206
|
+
|
|
207
|
+
The SDK requires a WAL Exchange contract to be deployed. See the `contracts/` directory for the Move source code.
|
|
208
|
+
|
|
209
|
+
### Environment Variables
|
|
210
|
+
|
|
211
|
+
```env
|
|
212
|
+
NEXT_PUBLIC_WAL_EXCHANGE_PACKAGE_ID=0x...
|
|
213
|
+
NEXT_PUBLIC_WAL_EXCHANGE_OBJECT_ID=0x...
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Storage Cost Formula
|
|
217
|
+
|
|
218
|
+
Walrus uses the following pricing formula:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
Total = encodedUnits × (writePricePerUnit + storagePricePerUnit × epochs)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Where:
|
|
225
|
+
- `encodedUnits = ceil(encodedSize / 1 MiB)`
|
|
226
|
+
- `encodedSize` is calculated using RedStuff encoding (varies by file size and nShards)
|
|
227
|
+
- Prices are fetched from on-chain Walrus system object
|
|
228
|
+
|
|
229
|
+
### On-Chain System Info
|
|
230
|
+
|
|
231
|
+
The SDK fetches live pricing from the Walrus system object:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { getWalrusSystemInfo } from '@lamdanghoang/sui-pay'
|
|
235
|
+
|
|
236
|
+
const systemInfo = await getWalrusSystemInfo('testnet')
|
|
237
|
+
console.log({
|
|
238
|
+
nShards: systemInfo.nShards, // e.g., 1000
|
|
239
|
+
storagePricePerUnit: systemInfo.storagePricePerUnit, // FROST per MiB per epoch
|
|
240
|
+
writePricePerUnit: systemInfo.writePricePerUnit, // FROST per MiB
|
|
241
|
+
currentEpoch: systemInfo.currentEpoch,
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Live vs Default Pricing
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// Use live on-chain pricing (recommended)
|
|
249
|
+
const costLive = await calculateStorageCostLive(fileSize, epochs, 'testnet')
|
|
250
|
+
|
|
251
|
+
// Use default pricing (faster, no RPC call)
|
|
252
|
+
const costDefault = calculateStorageCost(fileSize, epochs)
|
|
253
|
+
|
|
254
|
+
// Check balance with live pricing
|
|
255
|
+
const check = await client.checkBalance(address, fileSize, epochs, 20, true)
|
|
256
|
+
|
|
257
|
+
// Check balance with default pricing
|
|
258
|
+
const checkFast = await client.checkBalance(address, fileSize, epochs, 20, false)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
MIT
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/// Generic SUI <-> Token Exchange with Pyth Oracle pricing
|
|
2
|
+
/// T = the token type to exchange with SUI (e.g., WAL)
|
|
3
|
+
///
|
|
4
|
+
/// Pyth Price Feed IDs:
|
|
5
|
+
/// - SUI/USD: 0x23d7315113f5b1d3ba7a83604c44b94d79f4fd69af77f804fc7f920a6dc65744
|
|
6
|
+
/// - WAL/USD: 0xeba0732395fae9dec4bae12e52760b35fc1c5671e2da8b449c9af4efe5d54341
|
|
7
|
+
module walrus_sui_pay::wal_exchange_pyth {
|
|
8
|
+
use sui::coin::{Self, Coin};
|
|
9
|
+
use sui::balance::{Self, Balance};
|
|
10
|
+
use sui::sui::SUI;
|
|
11
|
+
use sui::event;
|
|
12
|
+
use sui::clock::Clock;
|
|
13
|
+
|
|
14
|
+
// === Errors ===
|
|
15
|
+
const EInsufficientLiquidity: u64 = 0;
|
|
16
|
+
const EZeroAmount: u64 = 1;
|
|
17
|
+
const EInvalidPrice: u64 = 3;
|
|
18
|
+
const ESlippageExceeded: u64 = 4;
|
|
19
|
+
const EPaused: u64 = 6;
|
|
20
|
+
|
|
21
|
+
// === Constants ===
|
|
22
|
+
const FEE_BPS: u64 = 30; // 0.3% fee
|
|
23
|
+
const BPS_DENOMINATOR: u64 = 10_000;
|
|
24
|
+
const MAX_PRICE_AGE_SECONDS: u64 = 60;
|
|
25
|
+
|
|
26
|
+
// === Structs ===
|
|
27
|
+
|
|
28
|
+
/// Generic exchange pool for SUI <-> T swaps
|
|
29
|
+
public struct Exchange<phantom T> has key {
|
|
30
|
+
id: UID,
|
|
31
|
+
sui_balance: Balance<SUI>,
|
|
32
|
+
token_balance: Balance<T>,
|
|
33
|
+
admin: address,
|
|
34
|
+
paused: bool,
|
|
35
|
+
/// Token/USD price scaled by 1e8
|
|
36
|
+
token_usd_price: u64,
|
|
37
|
+
max_price_age: u64,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Admin capability
|
|
41
|
+
public struct AdminCap has key, store {
|
|
42
|
+
id: UID,
|
|
43
|
+
exchange_id: ID,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// === Events ===
|
|
47
|
+
|
|
48
|
+
public struct ExchangeCreated has copy, drop {
|
|
49
|
+
exchange_id: ID,
|
|
50
|
+
admin: address,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public struct SwapExecuted has copy, drop {
|
|
54
|
+
user: address,
|
|
55
|
+
sui_amount: u64,
|
|
56
|
+
token_amount: u64,
|
|
57
|
+
is_sui_to_token: bool,
|
|
58
|
+
sui_price: u64,
|
|
59
|
+
token_price: u64,
|
|
60
|
+
fee_amount: u64,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public struct LiquidityAdded has copy, drop {
|
|
64
|
+
provider: address,
|
|
65
|
+
sui_amount: u64,
|
|
66
|
+
token_amount: u64,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// === Initialization ===
|
|
70
|
+
|
|
71
|
+
/// Create a new exchange for SUI <-> T
|
|
72
|
+
public fun create_exchange<T>(
|
|
73
|
+
initial_token_price: u64,
|
|
74
|
+
ctx: &mut TxContext
|
|
75
|
+
): (Exchange<T>, AdminCap) {
|
|
76
|
+
let exchange = Exchange<T> {
|
|
77
|
+
id: object::new(ctx),
|
|
78
|
+
sui_balance: balance::zero(),
|
|
79
|
+
token_balance: balance::zero(),
|
|
80
|
+
admin: ctx.sender(),
|
|
81
|
+
paused: false,
|
|
82
|
+
token_usd_price: initial_token_price,
|
|
83
|
+
max_price_age: MAX_PRICE_AGE_SECONDS,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let exchange_id = object::id(&exchange);
|
|
87
|
+
let admin_cap = AdminCap {
|
|
88
|
+
id: object::new(ctx),
|
|
89
|
+
exchange_id,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
event::emit(ExchangeCreated {
|
|
93
|
+
exchange_id,
|
|
94
|
+
admin: ctx.sender(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
(exchange, admin_cap)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Create and share exchange (entry function for deployment)
|
|
101
|
+
public entry fun create_and_share_exchange<T>(
|
|
102
|
+
initial_token_price: u64,
|
|
103
|
+
ctx: &mut TxContext
|
|
104
|
+
) {
|
|
105
|
+
let (exchange, admin_cap) = create_exchange<T>(initial_token_price, ctx);
|
|
106
|
+
transfer::share_object(exchange);
|
|
107
|
+
transfer::transfer(admin_cap, ctx.sender());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// === Swap Functions ===
|
|
111
|
+
|
|
112
|
+
/// Swap SUI for Token using real-time Pyth prices
|
|
113
|
+
///
|
|
114
|
+
/// @param exchange - The exchange pool
|
|
115
|
+
/// @param sui_coin - SUI to swap
|
|
116
|
+
/// @param sui_price - SUI/USD price from Pyth (scaled by 1e8)
|
|
117
|
+
/// @param token_price - Token/USD price from Pyth (scaled by 1e8)
|
|
118
|
+
/// @param min_token_out - Minimum tokens to receive (slippage protection)
|
|
119
|
+
/// @param clock - Sui clock object
|
|
120
|
+
public fun swap_sui_for_token<T>(
|
|
121
|
+
exchange: &mut Exchange<T>,
|
|
122
|
+
sui_coin: Coin<SUI>,
|
|
123
|
+
sui_price: u64,
|
|
124
|
+
token_price: u64,
|
|
125
|
+
min_token_out: u64,
|
|
126
|
+
_clock: &Clock,
|
|
127
|
+
ctx: &mut TxContext
|
|
128
|
+
) {
|
|
129
|
+
assert!(!exchange.paused, EPaused);
|
|
130
|
+
assert!(sui_price > 0, EInvalidPrice);
|
|
131
|
+
assert!(token_price > 0, EInvalidPrice);
|
|
132
|
+
|
|
133
|
+
let sui_amount = coin::value(&sui_coin);
|
|
134
|
+
assert!(sui_amount > 0, EZeroAmount);
|
|
135
|
+
|
|
136
|
+
// Calculate token output: (sui_amount * sui_price) / token_price
|
|
137
|
+
let sui_value_usd = (sui_amount as u128) * (sui_price as u128);
|
|
138
|
+
let token_amount_raw = sui_value_usd / (token_price as u128);
|
|
139
|
+
|
|
140
|
+
// Deduct fee (0.3%)
|
|
141
|
+
let fee = (token_amount_raw * (FEE_BPS as u128)) / (BPS_DENOMINATOR as u128);
|
|
142
|
+
let token_amount = ((token_amount_raw - fee) as u64);
|
|
143
|
+
|
|
144
|
+
assert!(token_amount >= min_token_out, ESlippageExceeded);
|
|
145
|
+
assert!(balance::value(&exchange.token_balance) >= token_amount, EInsufficientLiquidity);
|
|
146
|
+
|
|
147
|
+
// Execute swap
|
|
148
|
+
balance::join(&mut exchange.sui_balance, coin::into_balance(sui_coin));
|
|
149
|
+
let token_out = coin::from_balance(
|
|
150
|
+
balance::split(&mut exchange.token_balance, token_amount),
|
|
151
|
+
ctx
|
|
152
|
+
);
|
|
153
|
+
transfer::public_transfer(token_out, ctx.sender());
|
|
154
|
+
|
|
155
|
+
event::emit(SwapExecuted {
|
|
156
|
+
user: ctx.sender(),
|
|
157
|
+
sui_amount,
|
|
158
|
+
token_amount,
|
|
159
|
+
is_sui_to_token: true,
|
|
160
|
+
sui_price,
|
|
161
|
+
token_price,
|
|
162
|
+
fee_amount: (fee as u64),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Swap Token for SUI using real-time Pyth prices
|
|
167
|
+
public fun swap_token_for_sui<T>(
|
|
168
|
+
exchange: &mut Exchange<T>,
|
|
169
|
+
token_coin: Coin<T>,
|
|
170
|
+
sui_price: u64,
|
|
171
|
+
token_price: u64,
|
|
172
|
+
min_sui_out: u64,
|
|
173
|
+
_clock: &Clock,
|
|
174
|
+
ctx: &mut TxContext
|
|
175
|
+
) {
|
|
176
|
+
assert!(!exchange.paused, EPaused);
|
|
177
|
+
assert!(sui_price > 0, EInvalidPrice);
|
|
178
|
+
assert!(token_price > 0, EInvalidPrice);
|
|
179
|
+
|
|
180
|
+
let token_amount = coin::value(&token_coin);
|
|
181
|
+
assert!(token_amount > 0, EZeroAmount);
|
|
182
|
+
|
|
183
|
+
// Calculate SUI output
|
|
184
|
+
let token_value_usd = (token_amount as u128) * (token_price as u128);
|
|
185
|
+
let sui_amount_raw = token_value_usd / (sui_price as u128);
|
|
186
|
+
|
|
187
|
+
let fee = (sui_amount_raw * (FEE_BPS as u128)) / (BPS_DENOMINATOR as u128);
|
|
188
|
+
let sui_amount = ((sui_amount_raw - fee) as u64);
|
|
189
|
+
|
|
190
|
+
assert!(sui_amount >= min_sui_out, ESlippageExceeded);
|
|
191
|
+
assert!(balance::value(&exchange.sui_balance) >= sui_amount, EInsufficientLiquidity);
|
|
192
|
+
|
|
193
|
+
// Execute swap
|
|
194
|
+
balance::join(&mut exchange.token_balance, coin::into_balance(token_coin));
|
|
195
|
+
let sui_out = coin::from_balance(
|
|
196
|
+
balance::split(&mut exchange.sui_balance, sui_amount),
|
|
197
|
+
ctx
|
|
198
|
+
);
|
|
199
|
+
transfer::public_transfer(sui_out, ctx.sender());
|
|
200
|
+
|
|
201
|
+
event::emit(SwapExecuted {
|
|
202
|
+
user: ctx.sender(),
|
|
203
|
+
sui_amount,
|
|
204
|
+
token_amount,
|
|
205
|
+
is_sui_to_token: false,
|
|
206
|
+
sui_price,
|
|
207
|
+
token_price,
|
|
208
|
+
fee_amount: (fee as u64),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// === Liquidity Functions ===
|
|
213
|
+
|
|
214
|
+
/// Add both SUI and Token liquidity
|
|
215
|
+
public fun add_liquidity<T>(
|
|
216
|
+
exchange: &mut Exchange<T>,
|
|
217
|
+
sui_coin: Coin<SUI>,
|
|
218
|
+
token_coin: Coin<T>,
|
|
219
|
+
ctx: &TxContext
|
|
220
|
+
) {
|
|
221
|
+
let sui_amount = coin::value(&sui_coin);
|
|
222
|
+
let token_amount = coin::value(&token_coin);
|
|
223
|
+
|
|
224
|
+
balance::join(&mut exchange.sui_balance, coin::into_balance(sui_coin));
|
|
225
|
+
balance::join(&mut exchange.token_balance, coin::into_balance(token_coin));
|
|
226
|
+
|
|
227
|
+
event::emit(LiquidityAdded {
|
|
228
|
+
provider: ctx.sender(),
|
|
229
|
+
sui_amount,
|
|
230
|
+
token_amount,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// Add SUI liquidity only
|
|
235
|
+
public fun add_sui_liquidity<T>(
|
|
236
|
+
exchange: &mut Exchange<T>,
|
|
237
|
+
sui_coin: Coin<SUI>,
|
|
238
|
+
) {
|
|
239
|
+
balance::join(&mut exchange.sui_balance, coin::into_balance(sui_coin));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Add Token liquidity only
|
|
243
|
+
public fun add_token_liquidity<T>(
|
|
244
|
+
exchange: &mut Exchange<T>,
|
|
245
|
+
token_coin: Coin<T>,
|
|
246
|
+
) {
|
|
247
|
+
balance::join(&mut exchange.token_balance, coin::into_balance(token_coin));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// === Admin Functions ===
|
|
251
|
+
|
|
252
|
+
/// Update the stored token price (for reference only, swaps use Pyth prices)
|
|
253
|
+
public fun update_token_price<T>(
|
|
254
|
+
exchange: &mut Exchange<T>,
|
|
255
|
+
_admin_cap: &AdminCap,
|
|
256
|
+
new_price: u64,
|
|
257
|
+
) {
|
|
258
|
+
assert!(new_price > 0, EInvalidPrice);
|
|
259
|
+
exchange.token_usd_price = new_price;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Pause/unpause the exchange
|
|
263
|
+
public fun set_paused<T>(
|
|
264
|
+
exchange: &mut Exchange<T>,
|
|
265
|
+
_admin_cap: &AdminCap,
|
|
266
|
+
paused: bool,
|
|
267
|
+
) {
|
|
268
|
+
exchange.paused = paused;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Remove liquidity (admin only)
|
|
272
|
+
public fun remove_liquidity<T>(
|
|
273
|
+
exchange: &mut Exchange<T>,
|
|
274
|
+
_admin_cap: &AdminCap,
|
|
275
|
+
sui_amount: u64,
|
|
276
|
+
token_amount: u64,
|
|
277
|
+
ctx: &mut TxContext
|
|
278
|
+
) {
|
|
279
|
+
if (sui_amount > 0) {
|
|
280
|
+
let sui_coin = coin::from_balance(
|
|
281
|
+
balance::split(&mut exchange.sui_balance, sui_amount),
|
|
282
|
+
ctx
|
|
283
|
+
);
|
|
284
|
+
transfer::public_transfer(sui_coin, ctx.sender());
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
if (token_amount > 0) {
|
|
288
|
+
let token_coin = coin::from_balance(
|
|
289
|
+
balance::split(&mut exchange.token_balance, token_amount),
|
|
290
|
+
ctx
|
|
291
|
+
);
|
|
292
|
+
transfer::public_transfer(token_coin, ctx.sender());
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// === View Functions ===
|
|
297
|
+
|
|
298
|
+
/// Get current balances in the pool
|
|
299
|
+
public fun get_balances<T>(exchange: &Exchange<T>): (u64, u64) {
|
|
300
|
+
(
|
|
301
|
+
balance::value(&exchange.sui_balance),
|
|
302
|
+
balance::value(&exchange.token_balance)
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// Get stored token price
|
|
307
|
+
public fun get_token_price<T>(exchange: &Exchange<T>): u64 {
|
|
308
|
+
exchange.token_usd_price
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// Check if exchange is paused
|
|
312
|
+
public fun is_paused<T>(exchange: &Exchange<T>): bool {
|
|
313
|
+
exchange.paused
|
|
314
|
+
}
|
|
315
|
+
}
|