Skip to content

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}")