7508654d00
- Introduced `safeAbi.py` containing the ABI for the Safe contract, defining events and functions for managing ownership and transactions. - Created `merger.py` to handle merging conditional tokens back to USDC, including logic for determining merge amounts, signing transactions, and executing them through the Safe contract. - Integrated environment variable loading for sensitive data and RPC connection setup. - Implemented asynchronous polling for mergeable positions from the Polymarket API, with error handling and transaction receipt verification.
216 lines
7.2 KiB
Python
216 lines
7.2 KiB
Python
import os
|
|
import time
|
|
import requests
|
|
import asyncio
|
|
from web3 import Web3
|
|
from eth_account import Account
|
|
from dotenv import load_dotenv
|
|
from web3.middleware import ExtraDataToPOAMiddleware
|
|
from abi.ctfAbi import ctf_abi
|
|
from abi.safeAbi import safe_abi
|
|
|
|
# Constants
|
|
CONDITIONAL_TOKENS_FRAMEWORK_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
|
|
NEG_RISK_ADAPTER_ADDRESS = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
|
|
USDC_ADDRESS = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174"
|
|
USDCE_DIGITS = 6
|
|
load_dotenv()
|
|
|
|
|
|
def merge_tokens(condition_id, amount=None, neg_risk=False):
|
|
"""
|
|
Merge conditional tokens back to USDC.
|
|
|
|
Args:
|
|
condition_id: The condition ID (bytes32 hex string)
|
|
amount: Amount to merge in USDC (e.g., "1.5"). If None, merges all available tokens
|
|
neg_risk: Whether to use NEG_RISK_ADAPTER (default: False)
|
|
|
|
Returns:
|
|
bool: True if merge successful, False otherwise
|
|
"""
|
|
try:
|
|
# Connect to provider
|
|
rpc_url = os.getenv("RPC_URL")
|
|
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
|
|
# Inject POA middleware for Polygon
|
|
from web3.middleware import ExtraDataToPOAMiddleware
|
|
|
|
w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
|
|
# Load wallet and safe
|
|
private_key = os.getenv("PRIVATE_KEY")
|
|
account = Account.from_key(private_key)
|
|
safe_address = Web3.to_checksum_address(os.getenv("POLYMARKET_PROXY_ADDRESS"))
|
|
|
|
safe = w3.eth.contract(address=safe_address, abi=safe_abi)
|
|
|
|
# Determine merge amount
|
|
if amount is None:
|
|
# Get minimum balance of both positions
|
|
ctf_contract = w3.eth.contract(
|
|
address=Web3.to_checksum_address(CONDITIONAL_TOKENS_FRAMEWORK_ADDRESS),
|
|
abi=ctf_abi,
|
|
)
|
|
|
|
parent_collection_id = bytes(32)
|
|
collection_id_0 = ctf_contract.functions.getCollectionId(
|
|
parent_collection_id, bytes.fromhex(condition_id[2:]), 1
|
|
).call()
|
|
collection_id_1 = ctf_contract.functions.getCollectionId(
|
|
parent_collection_id, bytes.fromhex(condition_id[2:]), 2
|
|
).call()
|
|
|
|
position_id_0 = ctf_contract.functions.getPositionId(
|
|
Web3.to_checksum_address(USDC_ADDRESS), collection_id_0
|
|
).call()
|
|
position_id_1 = ctf_contract.functions.getPositionId(
|
|
Web3.to_checksum_address(USDC_ADDRESS), collection_id_1
|
|
).call()
|
|
|
|
balance_0 = ctf_contract.functions.balanceOf(
|
|
safe_address, position_id_0
|
|
).call()
|
|
balance_1 = ctf_contract.functions.balanceOf(
|
|
safe_address, position_id_1
|
|
).call()
|
|
|
|
amount_wei = min(balance_0, balance_1)
|
|
|
|
if amount_wei == 0:
|
|
print("Merge failed: No tokens to merge")
|
|
return False
|
|
else:
|
|
amount_wei = int(float(amount) * (10**USDCE_DIGITS))
|
|
|
|
# Encode merge transaction
|
|
ctf_contract = w3.eth.contract(abi=ctf_abi)
|
|
parent_collection_id = (
|
|
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
|
)
|
|
partition = [1, 2]
|
|
|
|
data = ctf_contract.functions.mergePositions(
|
|
Web3.to_checksum_address(USDC_ADDRESS),
|
|
bytes.fromhex(parent_collection_id[2:]),
|
|
bytes.fromhex(condition_id[2:]),
|
|
partition,
|
|
amount_wei,
|
|
)._encode_transaction_data()
|
|
|
|
# Sign and execute Safe transaction
|
|
nonce = safe.functions.nonce().call()
|
|
to = (
|
|
NEG_RISK_ADAPTER_ADDRESS
|
|
if neg_risk
|
|
else CONDITIONAL_TOKENS_FRAMEWORK_ADDRESS
|
|
)
|
|
|
|
tx_hash = safe.functions.getTransactionHash(
|
|
Web3.to_checksum_address(to),
|
|
0,
|
|
bytes.fromhex(data[2:]),
|
|
0, # operation: Call
|
|
0,
|
|
0,
|
|
0, # safeTxGas, baseGas, gasPrice
|
|
"0x0000000000000000000000000000000000000000", # gasToken
|
|
"0x0000000000000000000000000000000000000000", # refundReceiver
|
|
nonce,
|
|
).call()
|
|
|
|
# Sign the hash
|
|
hash_bytes = Web3.to_bytes(
|
|
hexstr=tx_hash.hex() if hasattr(tx_hash, "hex") else tx_hash
|
|
)
|
|
signature_obj = account.unsafe_sign_hash(hash_bytes)
|
|
|
|
r = signature_obj.r.to_bytes(32, byteorder="big")
|
|
s = signature_obj.s.to_bytes(32, byteorder="big")
|
|
v = signature_obj.v.to_bytes(1, byteorder="big")
|
|
signature = r + s + v
|
|
|
|
# Build and send transaction
|
|
tx = safe.functions.execTransaction(
|
|
Web3.to_checksum_address(to),
|
|
0,
|
|
bytes.fromhex(data[2:]),
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
"0x0000000000000000000000000000000000000000",
|
|
"0x0000000000000000000000000000000000000000",
|
|
signature,
|
|
).build_transaction(
|
|
{
|
|
"from": account.address,
|
|
"nonce": w3.eth.get_transaction_count(account.address),
|
|
"gas": 500000,
|
|
"gasPrice": w3.eth.gas_price,
|
|
}
|
|
)
|
|
|
|
signed_tx = account.sign_transaction(tx)
|
|
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
|
|
# Wait for receipt
|
|
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
|
|
|
|
# Check if transaction succeeded
|
|
if receipt["status"] == 1:
|
|
print(f"Merge successful! Amount: {amount_wei / 10**USDCE_DIGITS} USDC")
|
|
return True
|
|
else:
|
|
print("Merge failed: Transaction reverted")
|
|
return False
|
|
|
|
except Exception as e:
|
|
print(f"Merge failed: {str(e)}")
|
|
return False
|
|
|
|
|
|
private_key = os.getenv("PRIVATE_KEY")
|
|
account = Account.from_key(private_key)
|
|
safe_address = Web3.to_checksum_address(os.getenv("POLYMARKET_PROXY_ADDRESS"))
|
|
w3 = Web3(Web3.HTTPProvider(os.getenv("RPC_URL")))
|
|
w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
matic_balance = w3.eth.get_balance(account.address)
|
|
proxy_balance = w3.eth.get_balance(safe_address)
|
|
|
|
print(f"--- Wallet Check ---")
|
|
print(f"Signer Address (from Private Key): {account.address}")
|
|
print(f"Signer POL Balance: {w3.from_wei(matic_balance, 'ether')} POL")
|
|
print(f"Proxy Address (Safe): {safe_address}")
|
|
print(f"Proxy POL Balance: {w3.from_wei(proxy_balance, 'ether')} POL")
|
|
print(f"--------------------")
|
|
|
|
if matic_balance == 0:
|
|
raise Exception(f"STOP: Your Signer address {account.address} has NO gas!")
|
|
|
|
|
|
async def do_it():
|
|
|
|
url = f"https://data-api.polymarket.com/positions?sizeThreshold=1&limit=100&sortBy=TOKENS&sortDirection=DESC&user={os.getenv('POLYMARKET_PROXY_ADDRESS')}&mergeable=true"
|
|
|
|
response = requests.get(url).json()
|
|
if not response:
|
|
print("No mergeable positions found.")
|
|
return
|
|
condition_ids = set([token["conditionId"] for token in response])
|
|
condition_ids
|
|
for condition_id in condition_ids:
|
|
print("Merging tokens for condition ID:", condition_id)
|
|
merge_tokens(condition_id)
|
|
|
|
|
|
async def main():
|
|
while True:
|
|
asyncio.create_task(do_it())
|
|
await asyncio.sleep(10)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|