User Guide
Initialization
To start using Safe Kit, you need to initialize a Safe instance. This requires a Web3Adapter which connects to an Ethereum node and handles signing.
from web3 import Web3
from eth_account import Account
from safe_kit.safe import Safe
from safe_kit.adapter import Web3Adapter
# Setup Web3 and Signer
w3 = Web3(Web3.HTTPProvider("RPC_URL"))
owner = Account.from_key("PRIVATE_KEY")
adapter = Web3Adapter(web3=w3, signer=owner)
# Initialize Safe
safe = Safe(eth_adapter=adapter, safe_address="0xSafeAddress")
Creating and Signing Transactions
You can create transactions to send assets or interact with other contracts.
from safe_kit.types import SafeTransactionData
# Create a transaction (e.g., send ETH)
tx_data = SafeTransactionData(
to="0xRecipient",
value=1000000000000000000, # 1 ETH
data="0x"
)
safe_tx = safe.create_transaction(tx_data)
# Sign the transaction
# Supports "eth_sign_typed_data" (default, EIP-712) or "eth_sign" (legacy)
signed_tx = safe.sign_transaction(safe_tx)
Executing Transactions
Once a transaction has enough signatures (based on the Safe's threshold), it can be executed.
# Execute the transaction on-chain
tx_hash = safe.execute_transaction(signed_tx)
print(f"Transaction executed: {tx_hash}")
Batching Transactions (MultiSend)
Simple Batch Helper
You can easily batch multiple transactions using the convenience method:
from safe_kit.types import SafeTransactionData
# Define multiple transactions
tx1 = SafeTransactionData(to="0xRecipient1", value=100, data="0x")
tx2 = SafeTransactionData(to="0xRecipient2", value=200, data="0x")
# Create a batched transaction (automatically uses MultiSend)
batch_tx = safe.create_batch_transaction([tx1, tx2])
# Sign and execute as usual
signed_tx = safe.sign_transaction(batch_tx)
tx_hash = safe.execute_transaction(signed_tx)
Transaction Builder Pattern (New in v0.0.14)
For more complex scenarios, use the fluent TransactionBuilder API:
# Build a complex batch transaction with a fluent API
tx = safe.tx() \
.send_eth("0xRecipient1", 1000000000000000000) \
.send_erc20("0xTokenAddress", "0xRecipient2", 100) \
.send_erc721("0xNFTAddress", "0xRecipient3", token_id=42) \
.call("0xContractAddress", "0x12345678", value=0) \
.build()
# Sign and execute
signed_tx = safe.sign_transaction(tx)
tx_hash = safe.execute_transaction(signed_tx)
Manual MultiSend (Advanced)
If you need more control, you can still use MultiSend directly:
from safe_kit.types import SafeTransactionData
# Define multiple transactions
tx1 = SafeTransactionData(to="0xRecipient1", value=100, data="0x")
tx2 = SafeTransactionData(to="0xRecipient2", value=200, data="0x")
# Create a MultiSend transaction
# You need the address of the MultiSend contract on your chain
multi_send_address = "0x..."
multi_send_tx = safe.create_multi_send_transaction([tx1, tx2], multi_send_address)
# Sign and execute as usual
signed_tx = safe.sign_transaction(multi_send_tx)
tx_hash = safe.execute_transaction(signed_tx)
Transaction Status Checking (New in v0.0.14)
Check the status of transactions before execution:
# Check if a transaction has enough signatures
if safe.has_enough_signatures(safe_tx):
print("Ready to execute!")
else:
missing = safe.get_missing_signatures(safe_tx)
print(f"Need {missing} more signatures")
# Get signature count and signers
sig_count = safe.get_signature_count(safe_tx)
signers = safe.get_signers(safe_tx)
print(f"Transaction has {sig_count} signatures from: {signers}")
Performance Optimization with Caching (New in v0.0.14)
Enable caching to reduce RPC calls for frequently accessed Safe properties:
# Initialize Safe with caching enabled
safe = Safe(
eth_adapter=adapter,
safe_address="0xSafeAddress",
enable_cache=True,
cache_ttl=60 # Cache for 60 seconds
)
# These calls will use cached values within the TTL period
owners = safe.get_owners() # Makes RPC call
threshold = safe.get_threshold() # Makes RPC call
nonce = safe.get_nonce() # Makes RPC call
# Subsequent calls within 60 seconds use cache
owners2 = safe.get_owners() # Uses cache, no RPC call
# Manually clear cache when needed
safe.clear_cache()
# Or invalidate specific entries
safe.invalidate_cache("owners")
Using Safe Transaction Service
You can interact with the Safe Transaction Service to propose transactions for other owners to sign.
from safe_kit.service import SafeServiceClient
client = SafeServiceClient("https://safe-transaction-mainnet.safe.global")
# 1. Propose a transaction
safe_tx_hash = safe.get_transaction_hash(signed_tx)
client.propose_transaction(
safe_address=safe.safe_address,
safe_tx_data=signed_tx.data,
safe_tx_hash=safe_tx_hash,
sender_address=owner.address,
signature=signed_tx.signatures[owner.address].data.hex()
)
# 2. Get pending transactions
pending_txs = client.get_pending_transactions(safe.safe_address)
# 3. Confirm (sign) a pending transaction
# Assume you are a different owner now
client.confirm_transaction(safe_tx_hash, new_signature)
Deploying a Safe with Custom Modules
You can deploy a new Safe and enable modules during deployment.
from safe_kit.factory import SafeFactory
from safe_kit.types import SafeAccountConfig
factory = SafeFactory(
eth_adapter=adapter,
safe_singleton_address="0x...",
safe_proxy_factory_address="0x..."
)
# To enable a module during deployment, you would typically use a "setup" call
# encoded in the `data` field of SafeAccountConfig.
# However, a simpler way is to deploy first, then enable the module.
# 1. Deploy Safe
config = SafeAccountConfig(
owners=[owner.address],
threshold=1
)
safe = factory.deploy_safe(config)
print(f"Deployed Safe at: {safe.safe_address}")
# 2. Enable Module
module_address = "0xModuleAddress"
enable_tx = safe.create_enable_module_transaction(module_address)
signed_tx = safe.sign_transaction(enable_tx)
tx_hash = safe.execute_transaction(signed_tx)
print(f"Module enabled: {tx_hash}")