mirror of
https://github.com/luxfi/migration.git
synced 2026-10-30 15:25:07 +00:00
182 lines
7.9 KiB
Python
182 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Enumerate 0x9011's V3 positions on Lux mainnet C-Chain (96369) and simulate a
|
|
50% decreaseLiquidity for each, entirely READ-ONLY (eth_call). No broadcast.
|
|
|
|
Reads over `kubectl exec` into a validator (no port-forward needed) — the same
|
|
transport execute.py uses. Writes tools/snapshot.json for build_plan.py.
|
|
"""
|
|
import json, sys, os, subprocess
|
|
from eth_abi import encode as abi_encode, decode as abi_decode
|
|
|
|
KCTX = os.environ.get("KCTX", "do-sfo3-lux-k8s")
|
|
KNS = os.environ.get("KNS", "lux-mainnet")
|
|
KPOD = os.environ.get("KPOD", "luxd-1")
|
|
|
|
# --- canonical addresses (verified on-chain, not trusted from files) ---
|
|
OWNER = "0x9011E888251AB053B7bD1cdB598Db4f9DED94714"
|
|
NPM = "0x7a4C48B9dae0b7c396569b34042fcA604150Ee28" # NonfungiblePositionManager
|
|
FACTORY = "0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84" # UniswapV3Factory
|
|
DEADLINE = 1 << 63
|
|
|
|
_id = [0]
|
|
def _rpc(batch):
|
|
body = json.dumps(batch).encode()
|
|
p = subprocess.run(
|
|
["kubectl", "--context", KCTX, "-n", KNS, "exec", "-i", KPOD, "--",
|
|
"curl", "-s", "-X", "POST", "http://localhost:9630/v1/chain/C/rpc",
|
|
"-H", "content-type:application/json", "-d", "@-"],
|
|
input=body, capture_output=True, timeout=180)
|
|
return json.loads(p.stdout)
|
|
|
|
def call(to, data, frm=None):
|
|
_id[0] += 1
|
|
p = {"to": to, "data": data}
|
|
if frm: p["from"] = frm
|
|
return {"jsonrpc": "2.0", "id": _id[0], "method": "eth_call", "params": [p, "latest"]}
|
|
|
|
def sel(sig):
|
|
from eth_utils import keccak
|
|
return "0x" + keccak(text=sig).hex()[:8]
|
|
|
|
def enc(sig, types, args):
|
|
return sel(sig) + abi_encode(types, args).hex()
|
|
|
|
def batch_call(calls):
|
|
"""calls: list of (key, to, data, frm). Returns {key: raw_hex_or_None}."""
|
|
reqs, keymap = [], {}
|
|
for key, to, data, frm in calls:
|
|
r = call(to, data, frm); keymap[r["id"]] = key; reqs.append(r)
|
|
out = {}
|
|
for i in range(0, len(reqs), 50):
|
|
for resp in _rpc(reqs[i:i+50]):
|
|
k = keymap[resp["id"]]
|
|
out[k] = resp.get("result") if "error" not in resp else ("ERR:"+json.dumps(resp["error"]))
|
|
return out
|
|
|
|
def dec(types, raw):
|
|
return abi_decode(types, bytes.fromhex(raw[2:]))
|
|
|
|
def main():
|
|
# 1) balanceOf(owner)
|
|
n = int(batch_call([("bal", NPM, enc("balanceOf(address)", ["address"], [OWNER]), None)])["bal"], 16)
|
|
print(f"balanceOf(owner) = {n}", file=sys.stderr)
|
|
|
|
# 2) tokenOfOwnerByIndex for all
|
|
idx_calls = [(f"tok{i}", NPM, enc("tokenOfOwnerByIndex(address,uint256)", ["address","uint256"], [OWNER, i]), None)
|
|
for i in range(n)]
|
|
toks_raw = batch_call(idx_calls)
|
|
token_ids = [int(toks_raw[f"tok{i}"], 16) for i in range(n)]
|
|
|
|
# 3) positions(tokenId)
|
|
pos_calls = [(f"pos{t}", NPM, enc("positions(uint256)", ["uint256"], [t]), None) for t in token_ids]
|
|
pos_raw = batch_call(pos_calls)
|
|
ptypes = ["uint96","address","address","address","uint24","int24","int24","uint128","uint256","uint256","uint128","uint128"]
|
|
|
|
positions = []
|
|
for t in token_ids:
|
|
r = pos_raw[f"pos{t}"]
|
|
if r is None or r.startswith("ERR"):
|
|
print(f" position {t}: {r}", file=sys.stderr); continue
|
|
(nonce, op, t0, t1, fee, tl, tu, liq, fg0, fg1, owed0, owed1) = dec(ptypes, r)
|
|
positions.append(dict(tokenId=t, token0=t0.lower(), token1=t1.lower(), fee=fee,
|
|
tickLower=tl, tickUpper=tu, liquidity=liq,
|
|
tokensOwed0=owed0, tokensOwed1=owed1))
|
|
|
|
# 4) simulate decreaseLiquidity(50%) via eth_call from OWNER -> exact (amount0, amount1)
|
|
# struct DecreaseLiquidityParams{tokenId,liquidity,amount0Min,amount1Min,deadline}
|
|
dec_calls = []
|
|
for p in positions:
|
|
half = p["liquidity"] // 2
|
|
p["halfLiquidity"] = half
|
|
if half == 0: # zero-liquidity position (fees only) -> nothing to decrease
|
|
continue
|
|
data = enc("decreaseLiquidity((uint256,uint128,uint256,uint256,uint256))",
|
|
["(uint256,uint128,uint256,uint256,uint256)"],
|
|
[(p["tokenId"], half, 0, 0, DEADLINE)])
|
|
dec_calls.append((f"dec{p['tokenId']}", NPM, data, OWNER))
|
|
dec_raw = batch_call(dec_calls) if dec_calls else {}
|
|
for p in positions:
|
|
k = f"dec{p['tokenId']}"
|
|
if k in dec_raw and dec_raw[k] and not dec_raw[k].startswith("ERR"):
|
|
a0, a1 = dec(["uint256","uint256"], dec_raw[k])
|
|
p["decHalf0"], p["decHalf1"] = a0, a1
|
|
else:
|
|
p["decHalf0"], p["decHalf1"] = 0, 0
|
|
p["decErr"] = dec_raw.get(k)
|
|
|
|
# 5) unique pools; read pool addr, slot0, liquidity, tickSpacing
|
|
pools = {}
|
|
for p in positions:
|
|
key = (p["token0"], p["token1"], p["fee"])
|
|
pools.setdefault(key, {"token0": p["token0"], "token1": p["token1"], "fee": p["fee"], "positions": []})
|
|
pools[key]["positions"].append(p["tokenId"])
|
|
|
|
pk = list(pools.keys())
|
|
getpool = [(f"gp{i}", FACTORY, enc("getPool(address,address,uint24)", ["address","address","uint24"],
|
|
[k[0], k[1], k[2]]), None) for i, k in enumerate(pk)]
|
|
fts = [(f"ts{i}", FACTORY, enc("feeAmountTickSpacing(uint24)", ["uint24"], [k[2]]), None) for i, k in enumerate(pk)]
|
|
gp_raw = batch_call(getpool + fts)
|
|
for i, k in enumerate(pk):
|
|
addr = "0x" + gp_raw[f"gp{i}"][-40:]
|
|
pools[k]["pool"] = addr.lower()
|
|
pools[k]["tickSpacing"] = int(gp_raw[f"ts{i}"], 16)
|
|
|
|
slot_calls, liq_calls = [], []
|
|
for i, k in enumerate(pk):
|
|
pa = pools[k]["pool"]
|
|
slot_calls.append((f"s{i}", pa, enc("slot0()", [], []), None))
|
|
liq_calls.append((f"l{i}", pa, enc("liquidity()", [], []), None))
|
|
sl_raw = batch_call(slot_calls + liq_calls)
|
|
for i, k in enumerate(pk):
|
|
s = sl_raw[f"s{i}"]
|
|
# slot0: sqrtPriceX96 uint160, tick int24, obsIndex u16, obsCard u16, obsCardNext u16, feeProto u8, unlocked bool
|
|
sp, tick, *_ = dec(["uint160","int24","uint16","uint16","uint16","uint8","bool"], s)
|
|
pools[k]["sqrtPriceX96"] = sp
|
|
pools[k]["tick"] = tick
|
|
pools[k]["poolLiquidity"] = int(sl_raw[f"l{i}"], 16)
|
|
|
|
# 6) token metadata: symbol, decimals, and pool token balances
|
|
tokens = set()
|
|
for k in pk:
|
|
tokens.add(k[0]); tokens.add(k[1])
|
|
meta_calls = []
|
|
for tkn in tokens:
|
|
meta_calls.append((f"sym{tkn}", tkn, enc("symbol()", [], []), None))
|
|
meta_calls.append((f"dec{tkn}", tkn, enc("decimals()", [], []), None))
|
|
for i, k in enumerate(pk):
|
|
pa = pools[k]["pool"]
|
|
meta_calls.append((f"b0_{i}", k[0], enc("balanceOf(address)", ["address"], [pa]), None))
|
|
meta_calls.append((f"b1_{i}", k[1], enc("balanceOf(address)", ["address"], [pa]), None))
|
|
m_raw = batch_call(meta_calls)
|
|
tokmeta = {}
|
|
for tkn in tokens:
|
|
raw_sym = m_raw[f"sym{tkn}"]
|
|
try:
|
|
symbol = dec(["string"], raw_sym)[0]
|
|
except Exception:
|
|
# some tokens return a bytes32 (fixed) symbol instead of a string
|
|
symbol = bytes.fromhex(raw_sym[2:]).rstrip(b"\x00").decode("latin1") if raw_sym else "?"
|
|
decimals = int(m_raw[f"dec{tkn}"], 16)
|
|
tokmeta[tkn] = {"symbol": symbol, "decimals": decimals}
|
|
for i, k in enumerate(pk):
|
|
pools[k]["bal0"] = int(m_raw[f"b0_{i}"], 16)
|
|
pools[k]["bal1"] = int(m_raw[f"b1_{i}"], 16)
|
|
|
|
snapshot = dict(
|
|
owner=OWNER, npm=NPM, factory=FACTORY,
|
|
blockRpc=f"kubectl exec -n {KNS} {KPOD} -- curl localhost:9630/v1/chain/C/rpc",
|
|
positions=positions,
|
|
pools=[{**v, "token0sym": tokmeta[v["token0"]]["symbol"], "token1sym": tokmeta[v["token1"]]["symbol"],
|
|
"dec0": tokmeta[v["token0"]]["decimals"], "dec1": tokmeta[v["token1"]]["decimals"]}
|
|
for v in pools.values()],
|
|
tokens=tokmeta,
|
|
)
|
|
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "snapshot.json")
|
|
with open(out, "w") as f:
|
|
json.dump(snapshot, f, indent=2)
|
|
print(f"wrote {out}: {len(positions)} positions, {len(pools)} pools", file=sys.stderr)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|