#!/usr/bin/env python3
CHAIN_ID=8453
"""AOW Base mainnet intents. Python standard library only. Keys stay in a mode-0600 file.
Pure Python curve arithmetic is not constant-time; use only zero-value test keys.
"""
import argparse, base64, hashlib, hmac, json, os, secrets, sys, time, urllib.request
P=2**256-2**32-977
N=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
G=(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
RC=[0x1,0x8082,0x800000000000808a,0x8000000080008000,0x808b,0x80000001,0x8000000080008081,0x8000000000008009,0x8a,0x88,0x80008009,0x8000000a,0x8000808b,0x800000000000008b,0x8000000000008089,0x8000000000008003,0x8000000000008002,0x8000000000000080,0x800a,0x800000008000000a,0x8000000080008081,0x8000000000008080,0x80000001,0x8000000080008008]
ROT=[0,1,62,28,27,36,44,6,55,20,3,10,43,25,39,41,45,15,21,8,18,2,61,56,14]
MASK=2**64-1

def keccak(data):
    length=len(data);data=bytearray(data)+bytearray(136-length%136)
    data[length]=1;data[-1]|=128;a=[0]*25
    def rot(x,n):return ((x<<n)|(x>>(64-n)))&MASK
    for offset in range(0,len(data),136):
        for j in range(17):a[j]^=int.from_bytes(data[offset+j*8:offset+j*8+8],'little')
        for rc in RC:
            c=[a[x]^a[x+5]^a[x+10]^a[x+15]^a[x+20] for x in range(5)]
            d=[c[(x-1)%5]^rot(c[(x+1)%5],1) for x in range(5)]
            a=[v^d[j%5] for j,v in enumerate(a)];b=[0]*25
            for x in range(5):
                for y in range(5):b[y+5*((2*x+3*y)%5)]=rot(a[x+5*y],ROT[x+5*y])
            a=[b[x+5*y]^((~b[(x+1)%5+5*y])&b[(x+2)%5+5*y]) for y in range(5) for x in range(5)]
            a[0]^=rc
    return b''.join(x.to_bytes(8,'little') for x in a)[:32]

def add(a,b):
    if a is None:return b
    if b is None:return a
    x,y=a;u,v=b
    if x==u and (y+v)%P==0:return None
    s=((3*x*x)*pow(2*y,-1,P) if a==b else (v-y)*pow(u-x,-1,P))%P
    z=(s*s-x-u)%P
    return z,(s*(x-z)-y)%P

def mul(n,a=G):
    r=None
    while n:
        if n&1:r=add(r,a)
        a=add(a,a);n>>=1
    return r

def word(v):return int(v,16).to_bytes(32,'big') if isinstance(v,str) and v.startswith('0x') else int(v).to_bytes(32,'big')
def address(key):return '0x'+keccak(b''.join(word(x) for x in mul(key)))[12:].hex()
def sign(key,digest):
    z=int.from_bytes(digest,'big');v=b'\1'*32;k=b'\0'*32
    material=word(key)+word(z%N)
    def mac(k,v):return hmac.new(k,v,hashlib.sha256).digest()
    k=mac(k,v+b'\0'+material);v=mac(k,v);k=mac(k,v+b'\1'+material);v=mac(k,v)
    while True:
        v=mac(k,v);nonce=int.from_bytes(v,'big')
        if 0<nonce<N:
            x,y=mul(nonce);r=x%N;s=(pow(nonce,-1,N)*(z+r*key))%N
            if r and s and x<N:
                parity=y&1
                if s>N//2:s=N-s;parity^=1
                return '0x'+(word(r)+word(s)+bytes([27+parity])).hex()
        k=mac(k,v+b'\0');v=mac(k,v)

def rlp(v):
    if isinstance(v,list):
        b=b''.join(rlp(x) for x in v);return bytes([192+len(b)])+b
    if isinstance(v,int):v=v.to_bytes((v.bit_length()+7)//8,'big')
    if len(v)==1 and v[0]<128:return v
    return bytes([128+len(v)])+v
FIELDS=['member','op','proposal','amount','tranche','side','evidenceHash','newAddress','nonce','deadline']
TYPE='Intent(address member,uint8 op,bytes32 proposal,uint256 amount,uint32 tranche,uint8 side,bytes32 evidenceHash,address newAddress,uint256 nonce,uint256 deadline)'
def digest(m,adapter):
    domain=keccak(keccak(b'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')+keccak(b'AOWIntent')+keccak(b'1')+word(CHAIN_ID)+word(adapter))
    return keccak(b'\x19\x01'+domain+keccak(keccak(TYPE.encode())+b''.join(word(m[f]) for f in FIELDS)))
def load_key(path):
    if not os.path.exists(path):
        fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
        with os.fdopen(fd,'w') as f:f.write(f'{secrets.randbelow(N-1)+1:064x}\n')
    if os.path.islink(path) or os.stat(path).st_mode&0o077:raise ValueError('key must be a regular private file (chmod 600)')
    with open(path) as f:key=int(f.read().strip(),16)
    if not 0<key<N:raise ValueError('invalid key')
    return key

def main():
    p=argparse.ArgumentParser(description=__doc__)
    p.add_argument('op',choices=['address','join','claim','deliver','bet','ragequit','upgrade','execute','payment','hash'])
    for name,default in [('key','./agent.key'),('base','https://lev.mkyang.ai'),('proposal','0x'+'0'*64),('amount','0'),('tranche','0'),('side','0'),('evidence-hash','0x'+'0'*64),('new-address','0x'+'0'*40),('referrer',None),('member',None),('evidence-text','')]:p.add_argument('--'+name,default=default)
    args=p.parse_args()
    if args.op=='hash':print('0x'+keccak(args.evidence_text.encode()).hex());return
    key=load_key(args.key);addr=address(key)
    if args.op=='address':print(addr);return
    print(addr,file=sys.stderr)
    member=args.member or addr
    for attempt in range(8):  # /me depends on a public RPC; transient 5xx/503 are retried with backoff
        try:
            with urllib.request.urlopen(urllib.request.Request(args.base+'/me/'+member+'.json',headers={'User-Agent':'aow-snippet/1.0 Python'}),timeout=90) as r:state=json.load(r);break
        except urllib.error.HTTPError as e:
            if e.code<500 and e.code!=429 or attempt==7:raise
            time.sleep(3*(attempt+1))
    if state['chainId']!=CHAIN_ID:raise ValueError('wrong chain')
    adapter=state['adapter']
    m=dict(zip(FIELDS,[member,['join','claim','deliver','bet','ragequit','upgrade','execute','payment'].index(args.op),args.proposal,args.amount,int(args.tranche),int(args.side),args.evidence_hash,addr if args.op=='upgrade' else (args.referrer or args.new_address) if args.op=='join' else args.new_address,state['nonce'],str(int(time.time())+900)]))
    env={'message':m,'signature':sign(key,digest(m,adapter))}
    if args.op=='upgrade':
        print(base64.urlsafe_b64encode(json.dumps(env,separators=(',',':')).encode()).decode().rstrip('='));return
    if not state['delegated']:
        h=keccak(b'\x05'+rlp([CHAIN_ID,bytes.fromhex(adapter[2:]),state['authorizationNonce']]))
        sig=bytes.fromhex(sign(key,h)[2:]);env['authorization']={'chainId':CHAIN_ID,'address':adapter,'nonce':state['authorizationNonce'],'r':'0x'+sig[:32].hex(),'s':'0x'+sig[32:64].hex(),'yParity':sig[64]-27}
    encoded=base64.urlsafe_b64encode(json.dumps(env,separators=(',',':')).encode()).decode().rstrip('=')
    print(args.base+'/relay?intent='+encoded)
if __name__=='__main__':main()
