Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions examples/tempo-payment-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Tempo Python Payment Demo

A minimal, practical Python example demonstrating end-to-end payment flow on Tempo testnet using **web3.py**.

## 🎯 What This Demonstrates

- βœ… Request test funds from Tempo faucet (`tempo_fundAddress`)
- βœ… Build, sign, and broadcast TIP-20 (PATHUSD) transfers
- βœ… Verify transactions on Tempo explorer
- βœ… Secure key management with environment variables

## πŸ”— Live Example

A real test transaction was successfully broadcast during development:

- **Transaction Hash**: `b98e091a23a761e25d8ff4e421ab4f201f2019ecd6c9ee549168ee4bdd73347d`
- **Explorer Link**: https://explore.tempo.xyz/tx/b98e091a23a761e25d8ff4e421ab4f201f2019ecd6c9ee549168ee4bdd73347d

## πŸ“‹ Prerequisites

- Python 3.8 or higher
- pip (Python package manager)
- Access to Tempo testnet

## πŸš€ Quick Start

### 1. Setup Environment
```bash
# Navigate to example directory
cd examples/tempo-payment-demo

# Create virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate

# Install dependencies
pip install -r requirements.txt
```

### 2. Configure
```bash
# Copy environment template
cp .env.example .env

# Edit .env with your test credentials
nano .env # or use your favorite editor
```

Your `.env` should look like:
```env
RPC_URL=https://rpc.testnet.tempo.xyz
PRIVATE_KEY=your_test_private_key_here_without_0x
```

⚠️ **Security Warning**: Use **test-only** private keys. Never commit real keys!

### 3. Run the Demo
```bash
# Request funds and check balance
python -m src.main

# Send a test transaction
python -m src.send_tx
```

## πŸ“ Project Structure
```
tempo-payment-demo/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ __init__.py
β”‚ β”œβ”€β”€ client.py # Web3 client wrapper
β”‚ β”œβ”€β”€ config.py # Environment configuration loader
β”‚ β”œβ”€β”€ faucet.py # Faucet interaction helper
β”‚ β”œβ”€β”€ payment.py # Payment utilities (TIP-20 transfers)
β”‚ β”œβ”€β”€ send_tx.py # Transaction sender script
β”‚ └── main.py # Main demo entrypoint
β”œβ”€β”€ requirements.txt # Python dependencies (web3, python-dotenv)
β”œβ”€β”€ .env.example # Environment template
β”œβ”€β”€ .gitignore # Git ignore rules (venv, .env)
└── README.md # This file
```

## πŸ’» Usage Examples

### Check Balance
```python
from src.client import get_web3_client
from src.config import Config

config = Config()
w3, account = get_web3_client(config)

balance = w3.eth.get_balance(account.address)
print(f"Balance: {w3.from_wei(balance, 'ether')} PATHUSD")
```

### Send TIP-20 Transfer
```python
from src.payment import send_tip20_transfer

tx_hash = send_tip20_transfer(
w3=w3,
account=account,
to_address="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
amount_in_ether=10.0
)

print(f"Transaction sent: {tx_hash.hex()}")
print(f"Explorer: https://explore.tempo.xyz/tx/{tx_hash.hex()}")
```

### Request Faucet Funds
```python
from src.faucet import request_faucet_funds

success = request_faucet_funds(w3, account.address)
if success:
print("βœ… Funds received!")
```

## 🌐 Network Details

| Parameter | Value |
|-----------|-------|
| **Network** | Tempo Testnet (Andantino) |
| **Chain ID** | 42429 |
| **RPC URL** | https://rpc.testnet.tempo.xyz |
| **WebSocket** | wss://rpc.testnet.tempo.xyz |
| **Explorer** | https://explore.tempo.xyz |
| **Currency** | PATHUSD (TIP-20) |

## πŸ”§ Troubleshooting

### Insufficient Funds Error
```bash
# Request funds from faucet
python -m src.faucet
```

### Connection Issues

- Verify `RPC_URL` in `.env` is correct
- Check network connectivity
- Ensure you're using testnet, not mainnet

### Transaction Failed

- Check you have sufficient balance for gas + transfer amount
- Verify recipient address format (must start with 0x)
- Ensure private key is valid (64 hex characters, no 0x prefix)

### Import Errors
```bash
# Make sure you're in the venv and dependencies are installed
source venv/bin/activate
pip install -r requirements.txt
```

## πŸ” Security Best Practices

- βœ… Use `.env` for sensitive data (git-ignored by default)
- βœ… **Never** commit private keys to version control
- βœ… Use **test-only keys** for testnet (generate at https://vanity-eth.tk/)
- βœ… For production, use secure vaults (AWS KMS, HashiCorp Vault, GitHub Secrets)
- βœ… Rotate keys regularly
- βœ… Never share private keys via chat, email, or screenshots

## πŸ“š Resources

- [Tempo Documentation](https://docs.tempo.xyz)
- [Tempo Testnet Faucet](https://docs.tempo.xyz/quickstart/faucet)
- [TIP-20 Token Standard](https://docs.tempo.xyz/protocol/tip20/overview)
- [web3.py Documentation](https://web3py.readthedocs.io/)
- [Tempo Python SDK](https://github.com/tempoxyz/pytempo)

## 🀝 Contributing

Found a bug or want to improve this example? Contributions are welcome!

1. Fork the [pytempo repository](https://github.com/tempoxyz/pytempo)
2. Create a feature branch (`git checkout -b feature/my-improvement`)
3. Commit your changes (`git commit -m 'feat: add some feature'`)
4. Push to the branch (`git push origin feature/my-improvement`)
5. Open a Pull Request

## ⚠️ Important Notes

- This is a **community example** for educational purposes
- Use **test-only** private keys and testnet funds
- Not audited for production use
- Tempo is still under development - API may change

## πŸ“„ License

This example follows the pytempo repository license.

See [LICENSE](../../LICENSE) for details.

---

**Need help?** Check the [Tempo Discord](https://discord.gg/tempo) or [GitHub Issues](https://github.com/tempoxyz/pytempo/issues).
3 changes: 3 additions & 0 deletions examples/tempo-payment-demo/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
web3==6.0.0
python-dotenv==1.0.0
eth-account==0.8.1
1 change: 1 addition & 0 deletions examples/tempo-payment-demo/src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# tempo python payment demo package
5 changes: 5 additions & 0 deletions examples/tempo-payment-demo/src/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from web3 import Web3
from .config import RPC_URL

def get_web3():
return Web3(Web3.HTTPProvider(RPC_URL))
11 changes: 11 additions & 0 deletions examples/tempo-payment-demo/src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from dotenv import load_dotenv
import os

load_dotenv()

RPC_URL = os.getenv("RPC_URL", "https://rpc.testnet.tempo.xyz")
PRIVATE_KEY = os.getenv("PRIVATE_KEY", "")
FACTORY_ADDRESS = os.getenv("FACTORY_ADDRESS", "0x20fc000000000000000000000000000000000000")
PATHUSD_ADDRESS = os.getenv("PATHUSD_ADDRESS", "0x20c0000000000000000000000000000000000000")
FEE_MANAGER_ADDRESS = os.getenv("FEE_MANAGER_ADDRESS", "0xfeec000000000000000000000000000000000000")
EXPLORER = os.getenv("EXPLORER", "https://explore.tempo.xyz")
4 changes: 4 additions & 0 deletions examples/tempo-payment-demo/src/faucet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# simple wrapper for tempo_fundAddress via JSON-RPC
def fund_address(w3, address):
# web3.py low-level JSON RPC call
return w3.provider.make_request('tempo_fundAddress', [address])
31 changes: 31 additions & 0 deletions examples/tempo-payment-demo/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from .client import get_web3
from .config import PRIVATE_KEY, EXPLORER
from .faucet import fund_address
from .payment import build_transfer_tx
from web3 import Web3

def demo():
w3 = get_web3()
acct = w3.eth.account.from_key(PRIVATE_KEY)
print("Address:", acct.address)
# faucet example (may fail if RPC doesn't expose)
try:
res = fund_address(w3, acct.address)
print("Faucet result:", res)
except Exception as e:
print("Faucet call error (non-fatal):", e)
# build tx (not broadcasted here)
dummy_token = "0x0000000000000000000000000000000000000000" # placeholder

tx = build_transfer_tx(
w3,
dummy_token,
acct.key.hex(),
acct.address,
1
)

print("Built tx (preview):", tx)

if __name__ == '__main__':
demo()
51 changes: 51 additions & 0 deletions examples/tempo-payment-demo/src/payment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from web3 import Web3

# ABI ERC20 sederhana
ERC20_ABI = [
{
"constant": True,
"inputs": [
{"name": "_owner", "type": "address"}
],
"name": "balanceOf",
"outputs": [
{"name": "balance", "type": "uint256"}
],
"type": "function"
},
{
"constant": False,
"inputs": [
{"name": "_to", "type": "address"},
{"name": "_value", "type": "uint256"}
],
"name": "transfer",
"outputs": [
{"name": "success", "type": "bool"}
],
"type": "function"
}
]

def get_token_contract(web3: Web3, token_address: str):
"""Return ERC20 contract instance"""
return web3.eth.contract(address=Web3.to_checksum_address(token_address), abi=ERC20_ABI)


def build_transfer_tx(web3: Web3, token_address: str, private_key: str, to: str, amount: int):
"""Build unsigned ERC20 transfer transaction"""
account = web3.eth.account.from_key(private_key)
contract = get_token_contract(web3, token_address)

tx = contract.functions.transfer(
Web3.to_checksum_address(to),
amount
).build_transaction({
"from": account.address,
"nonce": web3.eth.get_transaction_count(account.address),
"gas": 120000,
"gasPrice": web3.to_wei("0.1", "gwei"),
})

return tx

46 changes: 46 additions & 0 deletions examples/tempo-payment-demo/src/send_tx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from .client import get_web3
from .config import PRIVATE_KEY, PATHUSD_ADDRESS, EXPLORER
from .payment import build_transfer_tx
from web3 import Web3

def main():
w3 = get_web3()
# target (default merchant)
to_addr = "0x000000000000000000000000000000000000dEaD"
# amount: 0.001 PATHUSD (assume 18 decimals)
amount = Web3.to_wei(0.001, "ether")

# build unsigned tx
unsigned = build_transfer_tx(w3, PATHUSD_ADDRESS, PRIVATE_KEY, to_addr, amount)

# ensure chainId and correct nonce
unsigned.setdefault("chainId", w3.eth.chain_id)
unsigned["nonce"] = w3.eth.get_transaction_count(w3.eth.account.from_key(PRIVATE_KEY).address)

# Dynamically choose gasPrice from node, add small buffer to avoid 'underpriced'
try:
node_gas_price = w3.eth.gas_price # recommended gas price from node (int, wei)
# add 20% buffer
buffered = node_gas_price * 12 // 10
unsigned["gasPrice"] = buffered
print(f"Using gasPrice from node: {node_gas_price} wei, buffered: {buffered} wei")
except Exception as e:
# fallback to existing or conservative default (1 gwei)
fallback = Web3.to_wei(1, "gwei")
unsigned.setdefault("gasPrice", fallback)
print("Could not fetch node gas price, using fallback:", fallback)

print("Unsigned tx preview:")
print(unsigned)

# sign
signed = w3.eth.account.sign_transaction(unsigned, PRIVATE_KEY)
raw = signed.raw_transaction

# send
tx_hash = w3.eth.send_raw_transaction(raw)
print("Sent tx:", tx_hash.hex())
print("Explorer:", f"{EXPLORER.rstrip('/')}/tx/{tx_hash.hex()}")

if __name__ == '__main__':
main()