#!/usr/bin/env bash
#
# Check a shielded payment on our Sepolia pool — YOURS, not ours — without our
# machines and without our code.
#
# Give it the three coordinates a receipt already carries: the pool, the
# transaction, and the amount the receipt says is hidden. It reads a chain you
# choose and tells you whether that amount is anywhere in the pool's entire
# event history or in the transaction that carried it.
#
#   ./scripts/strk20-verify-payment.sh <pool> <tx-hash> <hidden-amount>
#   RPC=https://your-own-node/rpc/v0_9 ./scripts/strk20-verify-payment.sh ...
#
# It is curl and jq. Nothing here is Vauban code: running our program to check
# our claim is not checking our claim. Copy this file out of the repository and
# it still runs. Nothing is read from a file the run produced either — an audit
# whose inputs come from a report on the machine that just paid is an audit of
# that machine's opinion, and it breaks the moment /tmp is cleared.
#
# What it establishes: the deposit into the pool is visible on the public chain,
# as entering a pool must be, and the transferred amount appears nowhere.
#
# What it does not establish: unlinkability. Who paid whom is a different claim
# and is not measured here at all. Nor is this rail post-quantum — only the
# proof layer is.
#
set -euo pipefail

POOL="${1:?usage: strk20-verify-payment.sh <pool-address> <tx-hash> <hidden-amount>}"
PAYMENT_TX="${2:?missing transaction hash}"
TRANSFER_WEI="${3:?missing hidden amount (decimal wei, or 0x-prefixed hex)}"

# Defaults to a third-party endpoint on purpose. Verification that runs through
# the audited party's infrastructure is not verification.
RPC="${RPC:-https://starknet-sepolia-rpc.publicnode.com}"

# The class our pool records. An address is worth only as much as the class
# deployed under it, so this is checked rather than assumed — and it stays
# overridable, because a checker that cannot be pointed at a different claim is
# a checker you have to take our word for.
CLASS="${CLASS:-0x067dddd89d80fedadc06b6f160798f94800a4a70164e5a24301cd0d6076b554d}"
# Block our pool was deployed in; the scan starts there so "absent" means absent
# from the pool's whole life, not from a convenient window.
FROM_BLOCK="${FROM_BLOCK:-13457017}"
# The public deposit. Left empty, it is read from the transaction's own Deposit
# events, which is stronger than us asserting it. Set it to assert a specific
# value instead.
DEPOSIT_WEI="${DEPOSIT_WEI:-}"

fail() { echo "STOP: $*" >&2; exit 1; }

# curl and jq are the whole dependency list, and a missing one should say so
# rather than surface as a cryptic failure three calls later.
command -v curl >/dev/null 2>&1 || fail "curl is required and was not found"
command -v jq   >/dev/null 2>&1 || fail "jq is required and was not found (https://jqlang.github.io/jq/)"

rpc() { # method, params json
  local out rc=0
  out=$(curl -sS -m 60 -X POST -H 'content-type: application/json' \
    -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":$2}" "$RPC" 2>&1) || rc=$?
  [ "$rc" -eq 0 ] || fail "cannot reach the RPC at $RPC ($out) ; pass another with RPC=<url>"
  # A wrong endpoint answers with HTML or a proxy page rather than JSON-RPC, and
  # the failure should name that instead of arriving later as a jq parse error.
  printf '%s' "$out" | jq -e . >/dev/null 2>&1 ||
    fail "the endpoint at $RPC did not return JSON ; is it a Starknet JSON-RPC URL?"
  printf '%s' "$out"
}

# Compared as normalized strings, never as shell arithmetic: a felt does not fit
# in 64 bits, and $(( )) would wrap two different hashes into the same number.
strip0() { printf '%s' "${1#0x}" | tr 'A-F' 'a-f' | sed 's/^0*//'; }

# Decimal wei to a bare lowercase hex string.
#
# Bash arithmetic is 64-bit. One STRK is 1e18 wei, so ten STRK already exceeds
# what printf '%x' can carry and it would silently wrap — the same class of
# defect as comparing felts with $(( )). Rather than pull in bc or python, which
# would break the "curl and jq" promise, an amount too large to convert safely
# is REFUSED with the fix in the message. A refusal you can act on beats a
# number nobody sent.
to_hex() {
  local v="$1"
  case "$v" in
    0x*|0X*) strip0 "$v"; return ;;
  esac
  case "$v" in
    ''|*[!0-9]*) fail "amount '$v' is neither decimal digits nor 0x-prefixed hex" ;;
  esac
  # 2^63 - 1. Above this, printf '%x' wraps instead of converting.
  if [ "${#v}" -gt 19 ] || { [ "${#v}" -eq 19 ] && [ "$v" \> "9223372036854775807" ]; }; then
    fail "amount $v exceeds what this script can convert without wrapping; pass it as 0x hex"
  fi
  printf '%x' "$v"
}

echo "rpc   $RPC"

chain=$(rpc starknet_chainId '[]' | jq -r '.result // empty')
[ "$chain" = "0x534e5f5345504f4c4941" ] || fail "chain id ${chain:-none} is not SN_SEPOLIA"
echo "chain $chain (SN_SEPOLIA)"

onchain_class=$(rpc starknet_getClassHashAt "[\"latest\",\"$POOL\"]" | jq -r '.result // empty')
[ -n "$onchain_class" ] || fail "no class at $POOL on this chain"
[ "$(strip0 "$onchain_class")" = "$(strip0 "$CLASS")" ] ||
  fail "class $onchain_class at $POOL is not the recorded $CLASS"
echo "pool  $POOL, class $onchain_class as recorded"

# Every event the pool has emitted since the block it was deployed in, paged to
# the end. Stopping at the first chunk would be a scan that reports clean
# because it stopped early.
events='[]'
token=null
pages=0
while :; do
  cont=$([ "$token" = "null" ] && echo "" || echo ",\"continuation_token\":\"$token\"")
  page=$(rpc starknet_getEvents \
    "[{\"from_block\":{\"block_number\":$FROM_BLOCK},\"to_block\":\"latest\",\"address\":\"$POOL\",\"chunk_size\":1000$cont}]")
  jq -e '.result' >/dev/null <<<"$page" || fail "getEvents: $(jq -c '.error' <<<"$page")"
  events=$(jq -c --argjson a "$events" '$a + .result.events' <<<"$page")
  token=$(jq -r '.result.continuation_token // "null"' <<<"$page")
  pages=$((pages + 1))
  [ "$token" = "null" ] && break
done
echo "events $(jq 'length' <<<"$events") across $pages page(s)"

tx=$(rpc starknet_getTransactionByHash "[\"$PAYMENT_TX\"]")
receipt=$(rpc starknet_getTransactionReceipt "[\"$PAYMENT_TX\"]")
status=$(jq -r '.result.execution_status // "unknown"' <<<"$receipt")
[ "$status" = "SUCCEEDED" ] || fail "payment tx status is $status"
block=$(jq -r '.result.block_number // "unknown"' <<<"$receipt")
echo "tx    $PAYMENT_TX, SUCCEEDED in block $block"

# The transaction must be one of this pool's. A hash that touched some other
# contract would otherwise scan clean for the trivial reason that the pool never
# saw it. Addresses are compared normalized: this pool answers as 0x7889af…
# while our own deployment record writes 0x07889af…, and string equality on the
# raw forms silently matches nothing.
pool_n=$(strip0 "$POOL")
touched=$(jq -r --arg p "$pool_n" \
  '[.result.events[]? | select((.from_address | ltrimstr("0x") | sub("^0+";"")) == $p)] | length' \
  <<<"$receipt")
[ "$touched" -gt 0 ] || fail "transaction $PAYMENT_TX published nothing from pool $POOL"
echo "pool events in this tx: $touched"

haystack="$events$tx$receipt"

# A felt travels in several skins; look for all of them, not just the pretty one.
hits() {
  local hex n=0 f dec="$1"
  hex=$(to_hex "$dec")
  for f in "0x$hex" "0x$(printf '%064s' "$hex" | tr ' ' 0)" "0x$(printf '%063s' "$hex" | tr ' ' 0)"; do
    case "$haystack" in *"$f"*) n=$((n + 1)) ;; esac
  done
  # The decimal spelling only if it is one this script could convert, i.e. one
  # that could plausibly appear as a JSON number rather than a felt string.
  case "$dec" in 0x*|0X*) ;; *) case "$haystack" in *"$dec"*) n=$((n + 1)) ;; esac ;; esac
  echo "$n"
}

# The deposit, read from the transaction's own Deposit events unless asserted.
# Deriving it is stronger: it is the chain telling you what entered the pool,
# not us.
if [ -z "$DEPOSIT_WEI" ]; then
  deposit_sel=0x9149d2123147c5f43d258257fef0b7b969db78269369ebcf5ebb9eef8592f2
  DEPOSIT_WEI=$(jq -r --arg p "$pool_n" --arg s "$(strip0 "$deposit_sel")" \
    '[.result.events[]?
      | select((.from_address | ltrimstr("0x") | sub("^0+";"")) == $p)
      | select((.keys[0] | ltrimstr("0x") | sub("^0+";"")) == $s)
      | .data[0]] | first // empty' <<<"$receipt")
  [ -n "$DEPOSIT_WEI" ] || fail "no Deposit event from $POOL in this transaction; nothing to sanity-check the scan against"
  echo "deposit read from the chain: $DEPOSIT_WEI"
fi

deposit_hits=$(hits "$DEPOSIT_WEI")
transfer_hits=$(hits "$TRANSFER_WEI")
echo
echo "deposit  $DEPOSIT_WEI found in $deposit_hits encoding(s)"
echo "hidden   $TRANSFER_WEI found in $transfer_hits encoding(s)"

# If the deposit is invisible too, this scan is not reading what it thinks it is
# reading, and a clean result on the transfer would mean nothing.
[ "$deposit_hits" -gt 0 ] || fail "the deposit $DEPOSIT_WEI is absent too, so this scan proves nothing: it is not reading what it believes it is reading"
[ "$transfer_hits" -eq 0 ] || fail "the amount claimed hidden, $TRANSFER_WEI, is present on the public chain in $transfer_hits encoding(s)"

echo
echo "OK. Deposit visible, the amount claimed hidden is absent from the pool's whole"
echo "history and from the transaction itself, read through an endpoint of your choosing."
echo
echo "Not established: unlinkability, and nothing about this rail is post-quantum."
