@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 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,9 @@
1
+ [package]
2
+ name = "walrus_sui_pay"
3
+ edition = "2024.beta"
4
+
5
+ [dependencies]
6
+ Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
7
+
8
+ [addresses]
9
+ walrus_sui_pay = "0x0"
@@ -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
+ }