Advanced Transactions
Build complex multi-step transactions with dry runs, gas control, coin management, and BCS serialization.
Overview
Beyond the basic *_quick methods, the SDK provides fine-grained control over transaction construction:
- Dry Runs: Simulate transactions before executing
- Gas Budget: Control gas spending
- Coin Management: Split, merge, and select coins manually
- Multi-Step PTBs: Compose multiple operations in a single transaction
- BCS Serialization: Build raw serialized transactions
ScallopBuilder & ScallopTxBlock
from sui_scallop_sdk import ScallopClient
client = ScallopClient(secret_key="...", network="mainnet")
builder = client.create_builder()
tx = builder.create_tx_block()
The ScallopBuilder handles signing and coin selection. The ScallopTxBlock accumulates commands.
Dry Runs (Simulation)
Simulate a transaction without executing it:
from sui_scallop_sdk import DryRunResult
tx = builder.create_tx_block()
market_coin = tx.supply_quick(1_000_000_000, "sui", client.wallet_address)
tx.transfer_objects([market_coin], client.wallet_address)
# Simulate instead of executing
dry_result: DryRunResult = builder.dry_run_tx_block(tx)
if dry_result.success:
print(f"Gas estimate: {dry_result.gas_estimate}")
print(f"Balance changes: {dry_result.balance_changes}")
print(f"Events: {len(dry_result.events)}")
else:
print(f"Would fail: {dry_result.error}")
DryRunResult Fields
# DryRunResult is a Pydantic model:
# success: bool - Whether simulation passed
# gas_estimate: int = 0 - Estimated gas consumption
# effects: dict = {} - Simulated transaction effects
# events: list[dict] = [] - Simulated events
# balance_changes: list[dict] = [] - Token balance changes
# error: str | None = None - Error if simulation failed
Dry runs are useful for:
- Estimating gas costs before committing
- Validating transaction correctness
- Checking balance changes without spending gas
Gas Budget Control
tx = builder.create_tx_block()
# Set explicit gas budget (in MIST, 1 SUI = 1e9 MIST)
tx.set_gas_budget(50_000_000) # 0.05 SUI max gas
# Build and execute
market_coin = tx.supply_quick(1_000_000_000, "sui", client.wallet_address)
tx.transfer_objects([market_coin], client.wallet_address)
result = builder.sign_and_send_tx_block(tx)
If no budget is set, the SDK uses the default from dry-run estimation.
Coin Management
Split Coins
Split a coin into specific amounts:
tx = builder.create_tx_block()
# Split from an existing coin (by input index)
coin_idx = tx.add_object_input("0xCOIN_OBJECT_ID")
smaller_coin = tx.split_coin(coin_idx, 500_000_000, coin_is_input=True)
# Split from gas coin (SUI only)
sui_for_fee = tx.split_from_gas(1_000_000) # 0.001 SUI from gas
Merge Coins
Combine multiple coins into one:
tx = builder.create_tx_block()
# Merge coin objects
destination = tx.add_object_input("0xCOIN_A")
source1 = tx.add_object_input("0xCOIN_B")
source2 = tx.add_object_input("0xCOIN_C")
tx.merge_coins(destination, [source1, source2], is_objects=True)
Coin Selection via Builder
The builder can automatically select coins to cover an amount:
# Get coins covering a specific amount
coins = builder.get_coins_for_amount("sui", 5_000_000_000)
# Or let select_coin handle it within a transaction
coin_ref = builder.select_coin(tx, "sui", 5_000_000_000, sender=client.wallet_address)
# coin_ref["takeCoin"] - the coin input to use
# coin_ref["leftCoin"] - remaining change (or None)
Multi-Step Programmable Transaction Blocks
Compose multiple operations in a single atomic transaction:
tx = builder.create_tx_block()
# Step 1: Supply SUI to get market coins
market_coin = tx.supply_quick(10_000_000_000, "sui", client.wallet_address)
# Step 2: Mint sCoin from market coins (uses result of step 1)
scoin = tx.mint_scoin(market_coin, "sui", coin_is_result=True)
# Step 3: Transfer sCoin
tx.transfer_objects([scoin], client.wallet_address)
# All 3 steps execute atomically
result = builder.sign_and_send_tx_block(tx)
Chaining Results
Many methods return an int index that references a transaction result. Pass these between operations using coin_is_result=True:
tx = builder.create_tx_block()
# Result of supply is index 0
market_coin_idx = tx.supply(coin_input_idx, "sui", coin_is_result=True)
# Feed result into deposit_collateral
tx.deposit_collateral(
obligation_id,
market_coin_idx,
"sui",
coin_is_result=True, # coin_input is a result, not an object input
)
Complex Example: Flash Loan Arbitrage
tx = builder.create_tx_block()
# 1. Borrow flash loan
loan_coin, receipt = tx.borrow_flash_loan(1_000_000_000, "sui")
# 2. Use borrowed funds (e.g., supply to earn market coins)
market_coin = tx.supply(loan_coin, "sui", coin_is_result=True)
# 3. Withdraw to get back underlying
withdrawn = tx.withdraw(market_coin, "sui", coin_is_result=True)
# 4. Repay flash loan (must happen in same transaction)
tx.repay_flash_loan(withdrawn, receipt, "sui")
# 5. Transfer any profit
# (In a real arbitrage, you'd do a swap between steps 2-3)
result = builder.sign_and_send_tx_block(tx)
Multi-Asset Obligation Setup
This example uses an existing obligation (already created in a previous transaction):
# obligation_id and obligation_key must already exist on-chain
tx = builder.create_tx_block()
# Deposit multiple collaterals
tx.deposit_collateral_quick(5_000_000_000, "sui", obligation_id, client.wallet_address)
tx.deposit_collateral_quick(1_000_000_000, "weth", obligation_id, client.wallet_address)
# Update oracle prices
tx.update_asset_prices_quick(["sui", "weth", "usdc"])
# Borrow against combined collateral
borrowed = tx.borrow_quick(500_000_000, "usdc", obligation_id, obligation_key)
tx.transfer_objects([borrowed], client.wallet_address)
result = builder.sign_and_send_tx_block(tx)
Note:
create_obligation()returns a singleint(command index). The obligation ID and key must be parsed from the transaction result'screated_objectsafter executing.
Adding Arbitrary Object Inputs
For custom Move calls or advanced composition:
# Add any on-chain object as a transaction input
input_idx = tx.add_object_input("0xOBJECT_ID")
Transaction Inspection
# Convert transaction to a dictionary for debugging
tx_dict = tx.to_dict()
print(tx_dict)
# Shows all commands, inputs, and their types
BCS Serialization
Build a raw BCS-serialized transaction for manual signing or external submission:
# Get gas coins and price
rpc = client.rpc_client
gas_price = rpc.get_reference_gas_price()
coins_data = rpc.get_coins(client.wallet_address)
gas_coins = [
{
"objectId": c["coinObjectId"],
"version": c["version"],
"digest": c["digest"],
}
for c in coins_data["data"][:1] # Use first coin as gas
]
# Build BCS bytes
tx_bytes = tx.build_bcs_transaction(
sender=client.wallet_address,
gas_coins=gas_coins,
gas_price=gas_price,
)
# Sign manually
from sui_scallop_sdk import SuiKeypair
keypair = SuiKeypair.from_bech32("suiprivkey1...")
signature = keypair.sign_transaction(tx_bytes)
# Submit via RPC
result = rpc.execute_transaction(
tx_bytes=base64.b64encode(tx_bytes).decode(),
signatures=[signature],
)
TransactionResult Fields
from sui_scallop_sdk import TransactionResult
# TransactionResult from sign_and_send_tx_block:
# success: bool - Whether transaction succeeded
# digest: str | None - Transaction hash
# error: str | None - Error message if failed
# gas_used: int | None - Gas consumed
# effects: dict | None - Full transaction effects
# events: list[dict] - Emitted events
# created_objects: list[str] - IDs of new objects
# mutated_objects: list[str] - IDs of changed objects
References
- Transaction Flow - Basic transaction patterns
- Error Handling - Transaction error recovery
- Supported Coins - Coin names and decimals