Add KoreAnchor Ethereum integration - real on-chain anchoring
This commit is contained in:
172
eth_anchor.py
Normal file
172
eth_anchor.py
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Vector Zulu — Ethereum KoreAnchor Caller
|
||||
Calls KoreAnchor.sol at 0x8AaA785c7dC116b834E3A1766866078051fC0313
|
||||
to anchor KoreIDs immutably on Ethereum mainnet.
|
||||
|
||||
Transmitter: 0xA08298ed3Bf2aD64499BEB115441aed85E0985Cc
|
||||
Uses Chainlink oracle wallet keystore for signing.
|
||||
"""
|
||||
|
||||
import os, json, hashlib, logging
|
||||
from web3 import Web3
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
KORE_ANCHOR_ADDRESS = '0x8AaA785c7dC116b834E3A1766866078051fC0313'
|
||||
ETH_RPC = os.environ.get('ETH_RPC', 'https://eth-mainnet.g.alchemy.com/v2/nRVT_IneMUDF3Pokubawx')
|
||||
KEYSTORE_PATH = os.environ.get('ANCHOR_KEYSTORE', '/opt/eth-deploy-v21/oracle_keystore.json')
|
||||
|
||||
KORE_ANCHOR_ABI = [
|
||||
{
|
||||
"inputs": [
|
||||
{"name": "koreId", "type": "string"},
|
||||
{"name": "uetr", "type": "string"},
|
||||
{"name": "eventType", "type": "string"},
|
||||
{"name": "payloadHash", "type": "bytes32"},
|
||||
{"name": "amount", "type": "uint256"},
|
||||
{"name": "currency", "type": "bytes3"},
|
||||
{"name": "linkedTo", "type": "string"}
|
||||
],
|
||||
"name": "anchor",
|
||||
"outputs": [{"name": "anchoredAt", "type": "uint256"}],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [{"name": "koreId", "type": "string"}],
|
||||
"name": "isAnchored",
|
||||
"outputs": [{"name": "", "type": "bool"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "totalAnchors",
|
||||
"outputs": [{"name": "", "type": "uint256"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"anonymous": False,
|
||||
"inputs": [
|
||||
{"indexed": True, "name": "koreId", "type": "string"},
|
||||
{"indexed": True, "name": "uetr", "type": "string"},
|
||||
{"indexed": False, "name": "eventType", "type": "string"},
|
||||
{"indexed": False, "name": "amount", "type": "uint256"},
|
||||
{"indexed": False, "name": "currency", "type": "bytes3"},
|
||||
{"indexed": False, "name": "anchoredAt", "type": "uint256"}
|
||||
],
|
||||
"name": "AnchorCreated",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class EthereumAnchor:
|
||||
|
||||
def __init__(self):
|
||||
self.w3 = Web3(Web3.HTTPProvider(ETH_RPC))
|
||||
self.contract = self.w3.eth.contract(
|
||||
address=Web3.to_checksum_address(KORE_ANCHOR_ADDRESS),
|
||||
abi=KORE_ANCHOR_ABI
|
||||
)
|
||||
self._account = None
|
||||
log.info(f"EthereumAnchor initialized — contract {KORE_ANCHOR_ADDRESS}")
|
||||
|
||||
def _get_account(self):
|
||||
if self._account:
|
||||
return self._account
|
||||
if not os.path.exists(KEYSTORE_PATH):
|
||||
log.warning(f"Keystore not found at {KEYSTORE_PATH}")
|
||||
return None
|
||||
with open(KEYSTORE_PATH) as f:
|
||||
ks = json.load(f)
|
||||
# Empty string password confirmed working
|
||||
self._account = self.w3.eth.account.from_key(
|
||||
self.w3.eth.account.decrypt(ks, '')
|
||||
)
|
||||
log.info(f"Wallet loaded: {self._account.address}")
|
||||
return self._account
|
||||
|
||||
def payload_hash(self, payload: dict) -> bytes:
|
||||
"""SHA256 of canonical JSON payload."""
|
||||
canonical = json.dumps(payload, separators=(',', ':'), sort_keys=True)
|
||||
return hashlib.sha256(canonical.encode()).digest()
|
||||
|
||||
def anchor(self, kore_id: str, uetr: str, event_type: str,
|
||||
amount: float, currency: str, linked_to: str,
|
||||
payload: dict = None) -> str:
|
||||
"""
|
||||
Anchor a KoreID on Ethereum mainnet.
|
||||
Returns tx hash or empty string on failure.
|
||||
"""
|
||||
account = self._get_account()
|
||||
if not account:
|
||||
log.error("No signing account available")
|
||||
return ''
|
||||
|
||||
try:
|
||||
# Check if already anchored
|
||||
if self.contract.functions.isAnchored(kore_id).call():
|
||||
log.info(f"Already anchored: {kore_id}")
|
||||
return 'already_anchored'
|
||||
|
||||
# Build payload hash
|
||||
ph = self.payload_hash(payload or {
|
||||
'koreId': kore_id, 'uetr': uetr,
|
||||
'amount': amount, 'currency': currency
|
||||
})
|
||||
|
||||
# Convert amount to uint256 (x1e8)
|
||||
amount_int = int(amount * 1e8)
|
||||
|
||||
# Convert currency to bytes3
|
||||
currency_bytes = currency.encode('utf-8').ljust(3, b'\x00')[:3]
|
||||
|
||||
# Build transaction
|
||||
tx = self.contract.functions.anchor(
|
||||
kore_id, uetr, event_type,
|
||||
ph, amount_int, currency_bytes, linked_to
|
||||
).build_transaction({
|
||||
'from': account.address,
|
||||
'nonce': self.w3.eth.get_transaction_count(account.address),
|
||||
'gas': 200000,
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed = account.sign_transaction(tx)
|
||||
tx_hash = self.w3.eth.send_raw_transaction(signed.raw_transaction)
|
||||
tx_hex = tx_hash.hex()
|
||||
log.info(f"Anchored {kore_id} — tx: {tx_hex}")
|
||||
return tx_hex
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Anchor failed for {kore_id}: {e}")
|
||||
return ''
|
||||
|
||||
def total_anchors(self) -> int:
|
||||
try:
|
||||
return self.contract.functions.totalAnchors().call()
|
||||
except Exception as e:
|
||||
log.error(f"totalAnchors error: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
# Singleton
|
||||
_anchor = None
|
||||
|
||||
def get_anchor() -> EthereumAnchor:
|
||||
global _anchor
|
||||
if _anchor is None:
|
||||
_anchor = EthereumAnchor()
|
||||
return _anchor
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
a = EthereumAnchor()
|
||||
print(f"Total anchors on-chain: {a.total_anchors()}")
|
||||
print(f"Contract: {KORE_ANCHOR_ADDRESS}")
|
||||
print(f"Connected: {a.w3.is_connected()}")
|
||||
print(f"Block: {a.w3.eth.block_number}")
|
||||
@@ -16,6 +16,15 @@ import os, json, time, logging, requests, redis, psycopg2, urllib3
|
||||
from datetime import datetime, timezone
|
||||
from lick import build_headers
|
||||
|
||||
# Ethereum anchoring — import conditionally (web3 may not be installed)
|
||||
try:
|
||||
from eth_anchor import get_anchor
|
||||
ETH_ANCHOR_ENABLED = True
|
||||
except ImportError:
|
||||
ETH_ANCHOR_ENABLED = False
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("eth_anchor not available — Ethereum anchoring disabled")
|
||||
|
||||
urllib3.disable_warnings()
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [KORE-POSTER] %(levelname)s %(message)s')
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -145,6 +154,27 @@ class KoreChainPoster:
|
||||
if success:
|
||||
store_posted(self.pg, payload, 'POSTED', response_koreid)
|
||||
log.info(f"✓ {koreid} posted")
|
||||
|
||||
# Anchor to Ethereum mainnet
|
||||
if ETH_ANCHOR_ENABLED:
|
||||
try:
|
||||
anchor = get_anchor()
|
||||
tx_hash = anchor.anchor(
|
||||
kore_id=koreid,
|
||||
uetr=payload.get('uetr', ''),
|
||||
event_type=payload.get('step', 'ANCHOR'),
|
||||
amount=float(payload.get('amount', 0)),
|
||||
currency=payload.get('currency', 'USD'),
|
||||
linked_to=payload.get('linked_to', 'GENESIS-07'),
|
||||
payload=payload,
|
||||
)
|
||||
if tx_hash and tx_hash != 'already_anchored':
|
||||
log.info(f"⛓ Ethereum anchor tx: {tx_hash}")
|
||||
store_posted(self.pg, {**payload, 'ethereum_tx': tx_hash}, 'ETH_ANCHORED', tx_hash)
|
||||
elif tx_hash == 'already_anchored':
|
||||
log.info(f"⛓ Already on Ethereum: {koreid}")
|
||||
except Exception as e:
|
||||
log.error(f"Ethereum anchor error for {koreid}: {e}")
|
||||
else:
|
||||
retries += 1
|
||||
if retries < MAX_RETRIES:
|
||||
|
||||
Reference in New Issue
Block a user