safteawallet-py
SafteaWallet-Py is a Python SDK for interacting with SafeTea Multi-Signature Wallets on Ethereum-compatible networks.
It provides a simple and modular interface to create new multi-signature wallets, manage transaction proposals, and execute approved transactions. Both synchronous and asynchronous interfaces are available.
Installation
Install the package via pip:
pip install safteawallet-py
Usage
Sync vs Async
| Sync | Async | |
|---|---|---|
| Import | safteawallet_py |
safteawallet_py.aio |
| Client | SafeTeaClient |
AsyncSafeTeaClient |
| Usage | Regular calls | await all methods |
Synchronous API
Connecting and Initializing the Client
from safteawallet_py.client import SafeTeaClient
client = SafeTeaClient(
rpc_url="https://your.rpc.endpoint",
factory_address="0xYourFactoryAddress",
private_key="0x...",
)
Creating a New Multi-Signature Wallet
A minimum of two owners is required.
owners = [
"0xOwnerAddress1",
"0xOwnerAddress2",
"0xOwnerAddress3",
]
result = client.factory.create_wallet(owners)
print(f"Wallet: {result.wallet_address}")
print(f"Tx hash: {result.transaction_hash}")
Proposing a Transaction
wallet = client.wallet("0xYourWalletAddress")
expiry = w3.eth.get_block("latest")["timestamp"] + 86400 # 24 hours
tx_index = wallet.submit_transaction(
to="0xRecipientAddress",
value=w3.to_wei(1, "ether"),
data=b"",
expiry=expiry,
)
print(f"Submitted at index: {tx_index}")
Confirming / Rejecting a Transaction
wallet.confirm_transaction(tx_index)
wallet.reject_transaction(tx_index)
Managing Owners
expiry = w3.eth.get_block("latest")["timestamp"] + 86400
wallet.propose_add_owner("0xNewOwnerAddress", expiry)
wallet.propose_remove_owner("0xOwnerToRemove", expiry)
wallet.confirm_owner_proposal(proposal_index)
wallet.reject_owner_proposal(proposal_index)
ERC20 Token Operations
Get an ERC20Manager via client.erc20(token_address, wallet_address).
token = client.erc20("0xTokenAddress", "0xYourWalletAddress")
Read operations query the token contract directly:
name = token.name()
symbol = token.symbol()
decimals = token.decimals()
supply = token.total_supply()
# Balance of any address
balance = token.balance_of("0xSomeAddress")
# Shortcut — balance held by the wallet itself
wallet_balance = token.wallet_balance()
# Allowance
allowance = token.allowance("0xOwner", "0xSpender")
Write operations encode the ERC20 calldata and submit it through the wallet's multi-sig queue. They return the proposal tx_index, which owners then confirm or reject like any other transaction.
import time
expiry = int(time.time()) + 3600
# Propose a token transfer from the wallet
tx_index = token.transfer("0xRecipient", 100 * 10**18, expiry=expiry)
# Propose an approval
tx_index = token.approve("0xSpender", 500 * 10**18, expiry=expiry)
# Propose a transferFrom
tx_index = token.transfer_from("0xFrom", "0xTo", 50 * 10**18, expiry=expiry)
# Owners vote on the proposal as usual
wallet = client.wallet("0xYourWalletAddress")
wallet.confirm_transaction(tx_index)
Async API (safteawallet_py.aio)
The async module mirrors the sync API but every method is a coroutine. Use it with asyncio or any async framework (FastAPI, aiohttp, etc.).
Client
from safteawallet_py.aio import AsyncSafeTeaClient
client = AsyncSafeTeaClient(
rpc_url="https://your.rpc.endpoint",
factory_address="0xYourFactoryAddress",
private_key="0x...", # optional — omit for read-only use
)
AsyncSafeTeaClient exposes two attributes after construction:
client.factory— anAsyncSafeTeaFactoryinstanceclient.account— theLocalAccountderived fromprivate_key, orNone
Check connection
await client.check_connection() # raises ConnectionError if unreachable
Get a wallet instance
wallet = client.wallet("0xYourWalletAddress") # returns AsyncSafeTeaWallet
Factory — AsyncSafeTeaFactory
Create a wallet
from safteawallet_py.aio import AsyncSafeTeaClient
client = AsyncSafeTeaClient(rpc_url=..., factory_address=..., private_key=...)
owners = ["0xOwner1", "0xOwner2", "0xOwner3"] # minimum 2
result = await client.factory.create_wallet(owners)
print(result.wallet_address) # checksummed address of the new wallet
print(result.transaction_hash) # hex tx hash
Raises ValueError if fewer than 2 owners are provided.
Get wallets for a user
wallets = await client.factory.get_user_wallets("0xUserAddress")
# returns List[ChecksumAddress]
Wallet — AsyncSafeTeaWallet
Obtain a wallet instance via client.wallet(address) or construct directly:
from safteawallet_py.aio.wallet import AsyncSafeTeaWallet
wallet = AsyncSafeTeaWallet(web3=client.web3, wallet_address="0x...", account=client.account)
All write methods require the account to be one of the wallet's owners, otherwise NotOwnerError is raised.
Info
# Full wallet snapshot
info = await wallet.get_info()
# info.address — wallet address
# info.owners — list of owner addresses
# info.threshold — majority threshold (votes needed to execute)
# Individual getters
owners = await wallet.get_owners() # List[ChecksumAddress]
threshold = await wallet.get_threshold() # int
Transactions
# Submit a new proposal
expiry = int(time.time()) + 3600 # unix timestamp, must be within 30 days
tx_index = await wallet.submit_transaction(
to="0xRecipientAddress",
value=1_000_000_000_000_000_000, # wei
data=b"", # optional calldata
expiry=expiry, # optional, default 3600s from now
)
# Confirm or reject
await wallet.confirm_transaction(tx_index)
await wallet.reject_transaction(tx_index)
# Read
count = await wallet.get_transaction_count() # int
tx = await wallet.get_transaction(tx_index) # Transaction dataclass
Transaction fields:
| Field | Type | Description |
|---|---|---|
to |
str |
Recipient address |
value |
int |
Value in wei |
data |
bytes |
Calldata |
status |
TxStatus |
PENDING, EXECUTED, or CANCELED |
confirmations |
int |
Confirmation vote count |
rejections |
int |
Rejection vote count |
expiry |
int |
Unix timestamp |
created_at |
int |
Unix timestamp |
confirm_transaction and reject_transaction validate state before sending and raise:
SafeTeaError— if already executed or canceledAlreadyVotedError— if the caller already votedTransactionExpiredError— if the proposal has expired
Owner proposals
expiry = int(time.time()) + 3600
# Propose changes
await wallet.propose_add_owner("0xNewOwner", expiry)
await wallet.propose_remove_owner("0xOwnerToRemove", expiry)
# Vote on a proposal
await wallet.confirm_owner_proposal(proposal_index)
await wallet.reject_owner_proposal(proposal_index)
# Read
count = await wallet.get_owner_proposal_count() # int
proposal = await wallet.get_owner_proposal(index) # OwnerProposal dataclass
OwnerProposal fields:
| Field | Type | Description |
|---|---|---|
proposed_owner |
str |
Address being added or removed |
proposal_type |
ProposalType |
ADD or REMOVE |
status |
TxStatus |
PENDING, EXECUTED, or CANCELED |
confirmations |
int |
Confirmation vote count |
rejections |
int |
Rejection vote count |
expiry |
int |
Unix timestamp |
created_at |
int |
Unix timestamp |
confirm_owner_proposal and reject_owner_proposal raise ProposalNotFoundError if the index is out of range.
ERC20 token operations
Get an AsyncERC20Manager via client.erc20(token_address, wallet_address).
token = client.erc20("0xTokenAddress", "0xYourWalletAddress")
Read operations query the token contract directly:
name = await token.name()
symbol = await token.symbol()
decimals = await token.decimals()
supply = await token.total_supply()
# Balance of any address
balance = await token.balance_of("0xSomeAddress")
# Shortcut — balance held by the wallet itself
wallet_balance = await token.wallet_balance()
# Allowance
allowance = await token.allowance("0xOwner", "0xSpender")
Write operations encode the ERC20 calldata and submit it through the wallet's multi-sig queue. They return the proposal tx_index, which owners then confirm or reject like any other transaction.
import time
expiry = int(time.time()) + 3600
# Propose a token transfer from the wallet
tx_index = await token.transfer("0xRecipient", 100 * 10**18, expiry=expiry)
# Propose an approval
tx_index = await token.approve("0xSpender", 500 * 10**18, expiry=expiry)
# Propose a transferFrom
tx_index = await token.transfer_from("0xFrom", "0xTo", 50 * 10**18, expiry=expiry)
# Owners vote on the proposal as usual
wallet = client.wallet("0xYourWalletAddress")
await wallet.confirm_transaction(tx_index)
Full async example
import asyncio
from safteawallet_py.aio import AsyncSafeTeaClient
async def main():
client = AsyncSafeTeaClient(
rpc_url="https://your.rpc.endpoint",
factory_address="0xFactoryAddress",
private_key="0xYourPrivateKey",
)
await client.check_connection()
# Deploy a new wallet
result = await client.factory.create_wallet([
"0xOwner1",
"0xOwner2",
])
print("Wallet deployed:", result.wallet_address)
wallet = client.wallet(result.wallet_address)
# Submit a transaction
import time
tx_index = await wallet.submit_transaction(
to="0xRecipient",
value=10 ** 18, # 1 ETH in wei
expiry=int(time.time()) + 3600,
)
print("Submitted tx index:", tx_index)
# Second owner confirms
client2 = AsyncSafeTeaClient(
rpc_url="https://your.rpc.endpoint",
factory_address="0xFactoryAddress",
private_key="0xOwner2PrivateKey",
)
wallet2 = client2.wallet(result.wallet_address)
await wallet2.confirm_transaction(tx_index)
print("Transaction confirmed and executed.")
asyncio.run(main())
Full async ERC20 example
import asyncio
import time
from safteawallet_py.aio import AsyncSafeTeaClient
async def main():
client = AsyncSafeTeaClient(
rpc_url="https://your.rpc.endpoint",
factory_address="0xFactoryAddress",
private_key="0xOwner1PrivateKey",
)
await client.check_connection()
token = client.erc20("0xTokenAddress", "0xYourWalletAddress")
# Read token info
print(await token.name())
print(await token.symbol())
print(await token.decimals())
print("Wallet balance:", await token.wallet_balance())
# Propose a transfer — goes into the multi-sig queue
expiry = int(time.time()) + 3600
tx_index = await token.transfer("0xRecipient", 100 * 10**18, expiry=expiry)
print("Transfer proposal submitted at index:", tx_index)
# Second owner confirms
client2 = AsyncSafeTeaClient(
rpc_url="https://your.rpc.endpoint",
factory_address="0xFactoryAddress",
private_key="0xOwner2PrivateKey",
)
wallet2 = client2.wallet("0xYourWalletAddress")
await wallet2.confirm_transaction(tx_index)
print("Transfer confirmed and executed.")
asyncio.run(main())
Exceptions
| Exception | Description |
|---|---|
SafeTeaError |
Base exception for all SDK errors (also wraps ERC20 operation errors) |
NotOwnerError |
Caller is not a wallet owner |
AlreadyVotedError |
Caller already voted on this proposal |
TransactionExpiredError |
Proposal has passed its expiry timestamp |
ProposalNotFoundError |
Owner proposal index does not exist |
ConfigurationError |
Client misconfigured (e.g. write op without a signer) |
Development
To run the test suite:
pytest tests/
License
This project is open-source and available under the MIT License.
Metadata
Release files for safteawallet-py 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| safteawallet_py-0.3.1.tar.gz | 26.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| safteawallet_py-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 51.2 kB
Release files / safteawallet_py-0.3.1.tar.gz
| Download URL | safteawallet_py-0.3.1.tar.gz |
|---|---|
| Size | 26.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8732e1adfd33fb0d9f29a4a7f1c16ee4c45ab529db084c0c60477d351be3d08b
|
|
BLAKE2b-256 checksum How to use checksums |
e50fb0fea0bdb725ae4dc938a646e9f6b15d2ae675806c151fc2c6af116db48b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.12
|
Release files / safteawallet_py-0.3.1-py3-none-any.whl
| Download URL | safteawallet_py-0.3.1-py3-none-any.whl |
|---|---|
| Size | 24.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
78f7ae272b05e183ff28fb3dc219788d5ea58c1b15bb543171e15cbbc6b87ec3
|
|
BLAKE2b-256 checksum How to use checksums |
82d5f3dabf694aa542bf2de5f956db37f7977089042992d55c42fe8f7e04dd58
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.12
|