From f67981ff0d458e0cfc5cfb09b5de77c452c3c623 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:25:22 +0900 Subject: [PATCH 01/42] ci: add fail-closed P0-P1 wallet evidence verification --- .../rhc-wallet-verification-p0p1.yml | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/rhc-wallet-verification-p0p1.yml diff --git a/.github/workflows/rhc-wallet-verification-p0p1.yml b/.github/workflows/rhc-wallet-verification-p0p1.yml new file mode 100644 index 0000000..4305422 --- /dev/null +++ b/.github/workflows/rhc-wallet-verification-p0p1.yml @@ -0,0 +1,64 @@ +name: RHC wallet verification P0-P1 + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-wallet-verification-p0p1.yml' + +permissions: + contents: read + +concurrency: + group: rhc-wallet-verification-p0p1-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Materialize audited collector and fixed P0-P1 input + shell: bash + run: | + set -euo pipefail + mkdir -p runtime + cat > runtime/collector.py.gz.b64 <<'B64' +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 +B64 + base64 -d runtime/collector.py.gz.b64 | gzip -d > runtime/collector.py + cat > runtime/wallets.json.gz.b64 <<'B64' +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 +B64 + base64 -d runtime/wallets.json.gz.b64 | gzip -d > runtime/wallets.json + python -m py_compile runtime/collector.py + python - <<'PY' + import json + rows=json.load(open('runtime/wallets.json')) + assert len(rows)==20 + assert sum(x['priority']=='P0' for x in rows)==1 + assert sum(x['priority']=='P1' for x in rows)==19 + print({'wallets':len(rows),'p0':1,'p1':19}) + PY + - name: Collect and verify P0-P1 canonical evidence + shell: bash + run: python runtime/collector.py --wallets runtime/wallets.json --out out-wallet-verification + - name: Preserve collector and fixed inputs in artifact + if: always() + shell: bash + run: | + mkdir -p out-wallet-verification/runtime + cp runtime/collector.py runtime/wallets.json out-wallet-verification/runtime/ + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-wallet-verification-p0p1 + path: out-wallet-verification/ + retention-days: 3 + if-no-files-found: error + compression-level: 9 From 9dee1cc0daf5e3edd1f1c5d3501b0a49742c7135 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:30:40 +0900 Subject: [PATCH 02/42] chore: add compressed P0-P1 verification collector --- wallet-verification/collector.py.gz.b64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 wallet-verification/collector.py.gz.b64 diff --git a/wallet-verification/collector.py.gz.b64 b/wallet-verification/collector.py.gz.b64 new file mode 100644 index 0000000..503fbf6 --- /dev/null +++ b/wallet-verification/collector.py.gz.b64 @@ -0,0 +1 @@ +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 \ No newline at end of file From 633853e1c010b90211dbacd41f88551aa9c5a4a3 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:31:46 +0900 Subject: [PATCH 03/42] chore: add fixed compressed P0-P1 wallet input --- wallet-verification/wallets.json.gz.b64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 wallet-verification/wallets.json.gz.b64 diff --git a/wallet-verification/wallets.json.gz.b64 b/wallet-verification/wallets.json.gz.b64 new file mode 100644 index 0000000..42b3abf --- /dev/null +++ b/wallet-verification/wallets.json.gz.b64 @@ -0,0 +1 @@ +H4sIAAJhkWoC/+1ca1NcR5L9Pr+C0HyVNzKr8lHV3zBq26wREAg7dmZioqOeNmEJvAjGnpiY/755W9gDLV2abkCwGIGk7q5bN25XnTx5Tr3+9qeNjX/Z342NF7+kt2/b2YvJxgv4VaX6oD6E3JJkgZKkYovaiaRrc1Ycq0f/4uWHuj+fHp2cHp39c6i9D4ufzn4+OTo+e2+FiBdF/2inR/2opLOjk+PZaUvvT46H8r/NS618b3frm83t3dn+5var2f7B9uvNg7/MNrcOt7/fPvzLxf3tsjeHB9tbh7PvgX67cu+/p/bBx1ceTLemu4ezN9Otvd1Xw83sjt9OL105v/DvF4/30/HJL8ezdxdP/dtT/evif7vg7NfZj+n9jxetVcG1Boq9cWR22LFCFHsfc0g1dqiuasPmck+AWhv76pQollqr878/pd04vz0pP82Oz9/ldmp3JwngvINLV7w9+WF2dFzbr0MxXyooJ8dnp6n81oVRE4QQFUPxAAK92EfC0bvAEVoQIOy5vrhyB4NAmffJcXrXhhsdnOSj442tt+f58oVDy8yfb3WsfEBG+udtqv/veTo++wA3vPTx+fHR2bzTZoa80ma/tKPhCoXLfy5dX09Pfp69P0s/tN8b9HLx2dG7ZqXvfp6dn5XhYR04+QLCF04OMUw8TsD/9cXF9f/+BH7ep7ftJvgJ2SACsWYDRqPMXH0vpI4ML1AkK0QqlEHV+VqKQwrd+d4Ua8HG+cUoCEQA1EUFQ6WLIFKDuEgJQ6tZGiZXszdsLgHB66Pdo40vD06+nR5s/BklusvXn5381I5nR3W4cLEsn//ezzkP+KOMlbJnnx2z9bpLhcnXXlPWQGINfLn6+2aPchuYzWFwbCzzj/n3gP8yAFz5skv6mHXi+K9XHunk/NTuOXTtAJ2z86GDX7zZ++5gazo7mO7vHRxOX8129w5nA+Ns7xs5TQ+2v9qevlpAiv3775efZl9jEwOEd06Gp4HUUomI0BuKJjImiaAeqX+SfXGcfeNnJt9bUCoA+ppVQw8oroIG36gZX6EED52gdImePKPLwWgukWquOWiTnI30rqdUVAk8QqkMD0+pKwJgkVJzdL1Fx7WQT9BKbVJCtmA3IilGh61Q7/1q9UdBqSwTviGlXhc/WIzuSleo1k+ELB5DlZoqFSkSi2/RPvudqZ5i/PhuQeITs6UP5zRJNOy24mNu0LElY9KeojgvXBFMjqCBeMgKFKBRj0skCQbWkfjBRxA/KwJgMX7WqX45fuDhNAlMUG4fQCyuSypo2sE3e9WNjVRT8KU7Rs89e4mCTzmAmiTj2ZRaJstA5Cj2MM9IUNSbcCKTZGgUDaRFfGFrKjbcFJM20UjWXRdAHO13NAFdo+ktB2pGS1/qPVTwFsjSS0ATjaaHgqlGToGbv0kAvTt6e3TyfiyCVkTAYgStU/1SBLmlAWTu6a4DiOcB5CaAtw8gE9pqAj4rqtPhN/fQkjfxCxqNjz3W4T0/4QAarIfpdOXUyRwLObBwoUyQChntV8HSOnCw9yFGrlUAmUXMFpg1wLosAxkFj2agR5CCVkTAYgCtU/1xuGKYuDvIQEqcIUj31Hr1udcWyfiiEHSfjfgsIUP19JQDiKn2WJMEtX7OaL7frI1Z35Jc5gTR8tPg4Z2BIscaqlPM3bJBatEky5JRJXAm/mgkgPQRJKAVAfDRqNIa1S/FDz1MAnIwYTdxd2CBVOzrYtVQC6YAPiY1Q+197zJwsY/iU6MgTzh+imHRUGBCKwQgs8CAwoE6mnzTBqF1TL5SdYWNUcEnoQ6iFnHsc1K+XsGp9dpYAvL0GAJoNQR8PCy7evXLCg4eSMLFCboJ3UEEGfXWRqJhGLNkH1yTrLX3KPatq6KTToj4lCMoWsxwkMzF5JGP5mxKyNAGDRe8MpiOS+otZlIyTW+6HjqUCqHm1DX4vFTCORyJIKcPr+BWBMBiAK1T/dEoOHf7+EnWN7VkyN26ilqOvWXtIRSMLZfqoBSi0tN9xc/X323uz3a2v5/ONnf2v9mcvdn+endz5z+BMy//UHQw/Xrb4uj6WFmjCTIpZ+IaK4v3lIKkRMPYbaqB1ffoe5WS/ROmkO4kOSEMSUFZWoytW6PUmFIEdK7GlnrSQhYPORTG1swDFit13CXD9UnYCIRGxyH9YxhHWRECn30k/76SMMWJ0/+QyMslMEHjcDRgQGI3zAxa3lHuMRk/Rl9qsLZy5J1rfpg9j4LFR4wxSeIivV8/3QPkXBzLNFdGEZ5R8jnNDk7QT3y8fappNUN32exy5+yqMiesIVneYcpibYKcfQntXlPNQip5eTMSvo2/SaWIyTAPaJ2M1IcXITGieiKI5nF8yg2cMvlQui9ZU+hYnOuml5atOmHTKSMxI+MxI9n4XIJFigWr2YYc0ez2MKQnMbM2M121Cy1bcLD/3e63X+7sbX07FjErdvlixKxT/VLE+KURw/ekzWRCdzA8bWnWyNNSs8+mTBOB8w0apo6Woa0NvPV+CkirBky4W2Hyenv38Ou92dbe7lfbB6+nr2YXi7jm1YbCz7O8KzRyoQtbmOVSSmINNB9H4tRNv1RuwyKFwfkw2Aemc9l0r/NJh1ULvMwGeR/i2FTQ+EB24UoldXaOurOkWS3aem7oUsmpSEaXLZH4vCTQ4sYgkd+MTgOtBpSPpoHWqL5aYoJ7ijM3YbyTtV32/UPsufjaTdwMjYBtmEdm6zTTr+QwxWEJkwliMqdkAiNJSJDNTNcsicbXdiGlBlgNftXUU1B2iWs2lZQ7hSIYNYRcpC2TJ9Odzf/Z+PJgb3RNl915ZE0X+e5DL1KMT0vnNNdIwUxvZIqmOlpgbpamxtZ0rQWvhTVdx+l4lQVdNEG86wVdL/8IGIARDCRTwdXFiub2fW/JWUxyYEcaI6YWolgHVh+fMfD/HwP4zAN/dAxQfOaBWy3uRetlrgmJS5Wam3eD/yiMRAGraz1hCGH1eQW9C7P6+rudw+39nels8TvPJffW3s7O1AT13u6bux4qdUApmoBuxGRwzV3RvCNp8S2oB9eJIVZf7qtVbuhI7tNmsHcaaicB8xUWtz4HqC7lEGrOhEGbNorSWTlWVnP13Uw/R4dOsfiydLYljs33O3hwP79i/y8ajXWqr2Q0llgLt9L0ioebj4waGwaqGWMvXYqT5qxDQrIMAjkO2aQbLLBIGrZFSBUaErH6SJYRqlvI2J/YWxT82AD6NePnn8d8Piwm/MOZT5ww3xwhLYbmvXRrfMjaXYScsbEAWAs0kxKQpRUHAYKCb1QpxmJkE8HSsnewbHiCeWzs3Ls/NELcwyHET0BvPwxIRhMhp+FH2ScOOmw7Y1PxzEVJxfCEpkKfcNJNHqBaGsshOaoWJa2kNF/xn6wJYkuMGUvwxr8OmAzH0TS1eHbVYRddGjzix5LuA8fOip2/GDvrVH80sRNXmJkMPdXWArVGVDQ044MeKwXzNYmdujjsz8JUQyWywg9zCGoIyYOaC0s2ogE5XRkgn0uUPXaI3NMkC9GNF5At3cUYA4vpc4oIyVqgeyjmjaMPlLPD+doYV8wjJ5AezRFafmbTeF2NjKA/BsMfZcTwozanTcQY0h5bLBSygzLs4zVVGszlJjaWojZm+NdC14LhD+0L4FUsf5ig/9zDPk8CBf4ZBc8oiPyMgmcURHpGwTMKFs5YeEbB6lMB0poGVAjDWldO5vSieYtSk3VVcy44ilJrTk/Zf2sDcAERpFDNuQZtYViXRa5EzbEgchUN5rkJW9BKEoHNoos0b8GxzH+rjh2dFB7Yf6/Y+R9tEF2j+mPx35fHv9fenoMUsAc3HGjEXYfjkkqoIpJS7YTB9YTYSrjXabQVtxcsLmy7CKyvDqbT2d7uzp3vP4iCrRv7huHcMRX0JPbTqfvce6LctbXEvv+xphorUHYp9YytdR8qA1ABcYabRDKsLh+2xyb3hFm3d4sezVojsTpJFSwha/NBKqUagiFGsVdkS8Ceha2ZAD36nLFL1utZVwMxj24tfmjaXbH3F2l3neqXaJfvlnbx5rQLeGXY8xaDWsnX5ESHTQmRsiUb6yjSnKIAWt/HVnPPfjjLgcCXXk0CgnVk6ShqTH3NKha6+KPCdntIgUuMBaCADqcSNSwEZeFwr0+Na56fbRz+2Da+Oj1q7ze2j+dvvjk5qWNiVv2Yr7UkIjQkVu9jzNmyTMs5W4oFc0EVain2rEJpTMuuhbZPHFpHQDGuomdxPkH0eV3NcDiSBSo26gViy37wMGokY89fnesSnEA3y6P2EqKlbkio1TU1DiLCxwcMBzrmdYeDGRwO+y8Y0SuV2MWYVBonimwWTIezGlx9dMiA+fkCz8i4JTIsHT4j4xkZHyMjaHwGxjMwPnU2rn9Gxl0Mm9USNZYYsDRTZvadBU3BaQHTZ5Ea+wYdMNYnbOAyaNIQikbrZXZVWuNmKamAt1jpoMM+tDJsXEspVrYwMUiYo0rdXnBYtlbUuzC29xPloQ3car3/kYFbo/pj2ZJGd7MlTVpsCVzBoN7pcDI9R+GkhMLZPA1ijVmT67GGHtpwIJITD0lSrqVfHXhfoNYSqCAZ3sp8UWLiXpyRihEzxtQquEhBEJcg4AfD6vuNP6NePQ386vyDG1uXkMhZN6rvLVO3rJCdh6yCUiGYRa3UEVO5egz4FTJdB2C324jAE7rHU8b/9Pf/A8mZX4fmYQAA \ No newline at end of file From 2b2de66a160d680ab645f2d846cd9bebedbc5c0c Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:32:06 +0900 Subject: [PATCH 04/42] fix: materialize collector from repository files --- .github/workflows/rhc-wallet-verification-p0p1.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rhc-wallet-verification-p0p1.yml b/.github/workflows/rhc-wallet-verification-p0p1.yml index 4305422..7b9b76d 100644 --- a/.github/workflows/rhc-wallet-verification-p0p1.yml +++ b/.github/workflows/rhc-wallet-verification-p0p1.yml @@ -6,6 +6,7 @@ on: types: [opened, synchronize, reopened] paths: - '.github/workflows/rhc-wallet-verification-p0p1.yml' + - 'wallet-verification/**' permissions: contents: read @@ -28,14 +29,8 @@ jobs: run: | set -euo pipefail mkdir -p runtime - cat > runtime/collector.py.gz.b64 <<'B64' -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 -B64 - base64 -d runtime/collector.py.gz.b64 | gzip -d > runtime/collector.py - cat > runtime/wallets.json.gz.b64 <<'B64' -H4sIAAJhkWoC/+1ca1NcR5L9Pr+C0HyVNzKr8lHV3zBq26wREAg7dmZioqOeNmEJvAjGnpiY/755W9gDLV2abkCwGIGk7q5bN25XnTx5Tr3+9qeNjX/Z342NF7+kt2/b2YvJxgv4VaX6oD6E3JJkgZKkYovaiaRrc1Ycq0f/4uWHuj+fHp2cHp39c6i9D4ufzn4+OTo+e2+FiBdF/2inR/2opLOjk+PZaUvvT46H8r/NS618b3frm83t3dn+5var2f7B9uvNg7/MNrcOt7/fPvzLxf3tsjeHB9tbh7PvgX67cu+/p/bBx1ceTLemu4ezN9Otvd1Xw83sjt9OL105v/DvF4/30/HJL8ezdxdP/dtT/evif7vg7NfZj+n9jxetVcG1Boq9cWR22LFCFHsfc0g1dqiuasPmck+AWhv76pQollqr878/pd04vz0pP82Oz9/ldmp3JwngvINLV7w9+WF2dFzbr0MxXyooJ8dnp6n81oVRE4QQFUPxAAK92EfC0bvAEVoQIOy5vrhyB4NAmffJcXrXhhsdnOSj442tt+f58oVDy8yfb3WsfEBG+udtqv/veTo++wA3vPTx+fHR2bzTZoa80ma/tKPhCoXLfy5dX09Pfp69P0s/tN8b9HLx2dG7ZqXvfp6dn5XhYR04+QLCF04OMUw8TsD/9cXF9f/+BH7ep7ftJvgJ2SACsWYDRqPMXH0vpI4ML1AkK0QqlEHV+VqKQwrd+d4Ua8HG+cUoCEQA1EUFQ6WLIFKDuEgJQ6tZGiZXszdsLgHB66Pdo40vD06+nR5s/BklusvXn5381I5nR3W4cLEsn//ezzkP+KOMlbJnnx2z9bpLhcnXXlPWQGINfLn6+2aPchuYzWFwbCzzj/n3gP8yAFz5skv6mHXi+K9XHunk/NTuOXTtAJ2z86GDX7zZ++5gazo7mO7vHRxOX8129w5nA+Ns7xs5TQ+2v9qevlpAiv3775efZl9jEwOEd06Gp4HUUomI0BuKJjImiaAeqX+SfXGcfeNnJt9bUCoA+ppVQw8oroIG36gZX6EED52gdImePKPLwWgukWquOWiTnI30rqdUVAk8QqkMD0+pKwJgkVJzdL1Fx7WQT9BKbVJCtmA3IilGh61Q7/1q9UdBqSwTviGlXhc/WIzuSleo1k+ELB5DlZoqFSkSi2/RPvudqZ5i/PhuQeITs6UP5zRJNOy24mNu0LElY9KeojgvXBFMjqCBeMgKFKBRj0skCQbWkfjBRxA/KwJgMX7WqX45fuDhNAlMUG4fQCyuSypo2sE3e9WNjVRT8KU7Rs89e4mCTzmAmiTj2ZRaJstA5Cj2MM9IUNSbcCKTZGgUDaRFfGFrKjbcFJM20UjWXRdAHO13NAFdo+ktB2pGS1/qPVTwFsjSS0ATjaaHgqlGToGbv0kAvTt6e3TyfiyCVkTAYgStU/1SBLmlAWTu6a4DiOcB5CaAtw8gE9pqAj4rqtPhN/fQkjfxCxqNjz3W4T0/4QAarIfpdOXUyRwLObBwoUyQChntV8HSOnCw9yFGrlUAmUXMFpg1wLosAxkFj2agR5CCVkTAYgCtU/1xuGKYuDvIQEqcIUj31Hr1udcWyfiiEHSfjfgsIUP19JQDiKn2WJMEtX7OaL7frI1Z35Jc5gTR8tPg4Z2BIscaqlPM3bJBatEky5JRJXAm/mgkgPQRJKAVAfDRqNIa1S/FDz1MAnIwYTdxd2CBVOzrYtVQC6YAPiY1Q+197zJwsY/iU6MgTzh+imHRUGBCKwQgs8CAwoE6mnzTBqF1TL5SdYWNUcEnoQ6iFnHsc1K+XsGp9dpYAvL0GAJoNQR8PCy7evXLCg4eSMLFCboJ3UEEGfXWRqJhGLNkH1yTrLX3KPatq6KTToj4lCMoWsxwkMzF5JGP5mxKyNAGDRe8MpiOS+otZlIyTW+6HjqUCqHm1DX4vFTCORyJIKcPr+BWBMBiAK1T/dEoOHf7+EnWN7VkyN26ilqOvWXtIRSMLZfqoBSi0tN9xc/X323uz3a2v5/ONnf2v9mcvdn+endz5z+BMy//UHQw/Xrb4uj6WFmjCTIpZ+IaK4v3lIKkRMPYbaqB1ffoe5WS/ROmkO4kOSEMSUFZWoytW6PUmFIEdK7GlnrSQhYPORTG1swDFit13CXD9UnYCIRGxyH9YxhHWRECn30k/76SMMWJ0/+QyMslMEHjcDRgQGI3zAxa3lHuMRk/Rl9qsLZy5J1rfpg9j4LFR4wxSeIivV8/3QPkXBzLNFdGEZ5R8jnNDk7QT3y8fappNUN32exy5+yqMiesIVneYcpibYKcfQntXlPNQip5eTMSvo2/SaWIyTAPaJ2M1IcXITGieiKI5nF8yg2cMvlQui9ZU+hYnOuml5atOmHTKSMxI+MxI9n4XIJFigWr2YYc0ez2MKQnMbM2M121Cy1bcLD/3e63X+7sbX07FjErdvlixKxT/VLE+KURw/ekzWRCdzA8bWnWyNNSs8+mTBOB8w0apo6Woa0NvPV+CkirBky4W2Hyenv38Ou92dbe7lfbB6+nr2YXi7jm1YbCz7O8KzRyoQtbmOVSSmINNB9H4tRNv1RuwyKFwfkw2Aemc9l0r/NJh1ULvMwGeR/i2FTQ+EB24UoldXaOurOkWS3aem7oUsmpSEaXLZH4vCTQ4sYgkd+MTgOtBpSPpoHWqL5aYoJ7ijM3YbyTtV32/UPsufjaTdwMjYBtmEdm6zTTr+QwxWEJkwliMqdkAiNJSJDNTNcsicbXdiGlBlgNftXUU1B2iWs2lZQ7hSIYNYRcpC2TJ9Odzf/Z+PJgb3RNl915ZE0X+e5DL1KMT0vnNNdIwUxvZIqmOlpgbpamxtZ0rQWvhTVdx+l4lQVdNEG86wVdL/8IGIARDCRTwdXFiub2fW/JWUxyYEcaI6YWolgHVh+fMfD/HwP4zAN/dAxQfOaBWy3uRetlrgmJS5Wam3eD/yiMRAGraz1hCGH1eQW9C7P6+rudw+39nels8TvPJffW3s7O1AT13u6bux4qdUApmoBuxGRwzV3RvCNp8S2oB9eJIVZf7qtVbuhI7tNmsHcaaicB8xUWtz4HqC7lEGrOhEGbNorSWTlWVnP13Uw/R4dOsfiydLYljs33O3hwP79i/y8ajXWqr2Q0llgLt9L0ioebj4waGwaqGWMvXYqT5qxDQrIMAjkO2aQbLLBIGrZFSBUaErH6SJYRqlvI2J/YWxT82AD6NePnn8d8Piwm/MOZT5ww3xwhLYbmvXRrfMjaXYScsbEAWAs0kxKQpRUHAYKCb1QpxmJkE8HSsnewbHiCeWzs3Ls/NELcwyHET0BvPwxIRhMhp+FH2ScOOmw7Y1PxzEVJxfCEpkKfcNJNHqBaGsshOaoWJa2kNF/xn6wJYkuMGUvwxr8OmAzH0TS1eHbVYRddGjzix5LuA8fOip2/GDvrVH80sRNXmJkMPdXWArVGVDQ044MeKwXzNYmdujjsz8JUQyWywg9zCGoIyYOaC0s2ogE5XRkgn0uUPXaI3NMkC9GNF5At3cUYA4vpc4oIyVqgeyjmjaMPlLPD+doYV8wjJ5AezRFafmbTeF2NjKA/BsMfZcTwozanTcQY0h5bLBSygzLs4zVVGszlJjaWojZm+NdC14LhD+0L4FUsf5ig/9zDPk8CBf4ZBc8oiPyMgmcURHpGwTMKFs5YeEbB6lMB0poGVAjDWldO5vSieYtSk3VVcy44ilJrTk/Zf2sDcAERpFDNuQZtYViXRa5EzbEgchUN5rkJW9BKEoHNoos0b8GxzH+rjh2dFB7Yf6/Y+R9tEF2j+mPx35fHv9fenoMUsAc3HGjEXYfjkkqoIpJS7YTB9YTYSrjXabQVtxcsLmy7CKyvDqbT2d7uzp3vP4iCrRv7huHcMRX0JPbTqfvce6LctbXEvv+xphorUHYp9YytdR8qA1ABcYabRDKsLh+2xyb3hFm3d4sezVojsTpJFSwha/NBKqUagiFGsVdkS8Ceha2ZAD36nLFL1utZVwMxj24tfmjaXbH3F2l3neqXaJfvlnbx5rQLeGXY8xaDWsnX5ESHTQmRsiUb6yjSnKIAWt/HVnPPfjjLgcCXXk0CgnVk6ShqTH3NKha6+KPCdntIgUuMBaCADqcSNSwEZeFwr0+Na56fbRz+2Da+Oj1q7ze2j+dvvjk5qWNiVv2Yr7UkIjQkVu9jzNmyTMs5W4oFc0EVain2rEJpTMuuhbZPHFpHQDGuomdxPkH0eV3NcDiSBSo26gViy37wMGokY89fnesSnEA3y6P2EqKlbkio1TU1DiLCxwcMBzrmdYeDGRwO+y8Y0SuV2MWYVBonimwWTIezGlx9dMiA+fkCz8i4JTIsHT4j4xkZHyMjaHwGxjMwPnU2rn9Gxl0Mm9USNZYYsDRTZvadBU3BaQHTZ5Ea+wYdMNYnbOAyaNIQikbrZXZVWuNmKamAt1jpoMM+tDJsXEspVrYwMUiYo0rdXnBYtlbUuzC29xPloQ3car3/kYFbo/pj2ZJGd7MlTVpsCVzBoN7pcDI9R+GkhMLZPA1ijVmT67GGHtpwIJITD0lSrqVfHXhfoNYSqCAZ3sp8UWLiXpyRihEzxtQquEhBEJcg4AfD6vuNP6NePQ386vyDG1uXkMhZN6rvLVO3rJCdh6yCUiGYRa3UEVO5egz4FTJdB2C324jAE7rHU8b/9Pf/A8mZX4fmYQAA -B64 - base64 -d runtime/wallets.json.gz.b64 | gzip -d > runtime/wallets.json + base64 -d wallet-verification/collector.py.gz.b64 | gzip -d > runtime/collector.py + base64 -d wallet-verification/wallets.json.gz.b64 | gzip -d > runtime/wallets.json python -m py_compile runtime/collector.py python - <<'PY' import json From 625b4ad3c7756c6d1df4565b8b21d0676a4a11a5 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Fri, 28 Aug 2026 20:29:44 +0900 Subject: [PATCH 05/42] perf: shard P0-P1 wallet verification without weakening evidence gates --- .../rhc-wallet-verification-p0p1.yml | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/.github/workflows/rhc-wallet-verification-p0p1.yml b/.github/workflows/rhc-wallet-verification-p0p1.yml index 7b9b76d..08a1d84 100644 --- a/.github/workflows/rhc-wallet-verification-p0p1.yml +++ b/.github/workflows/rhc-wallet-verification-p0p1.yml @@ -17,43 +17,65 @@ concurrency: jobs: verify: + strategy: + fail-fast: false + matrix: + shard: [p0, p1a, p1b, p1c, p1d] runs-on: ubuntu-latest - timeout-minutes: 90 + timeout-minutes: 45 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' - - name: Materialize audited collector and fixed P0-P1 input + - name: Materialize audited collector and fixed shard input shell: bash + env: + SHARD: ${{ matrix.shard }} run: | set -euo pipefail mkdir -p runtime base64 -d wallet-verification/collector.py.gz.b64 | gzip -d > runtime/collector.py - base64 -d wallet-verification/wallets.json.gz.b64 | gzip -d > runtime/wallets.json + base64 -d wallet-verification/wallets.json.gz.b64 | gzip -d > runtime/wallets-all.json python -m py_compile runtime/collector.py python - <<'PY' - import json - rows=json.load(open('runtime/wallets.json')) - assert len(rows)==20 - assert sum(x['priority']=='P0' for x in rows)==1 - assert sum(x['priority']=='P1' for x in rows)==19 - print({'wallets':len(rows),'p0':1,'p1':19}) + import json, os + rows=json.load(open('runtime/wallets-all.json')) + p0=[row for row in rows if row['priority']=='P0'] + p1=[row for row in rows if row['priority']=='P1'] + shard=os.environ['SHARD'] + slices={ + 'p0': (0,0), + 'p1a': (0,5), + 'p1b': (5,10), + 'p1c': (10,15), + 'p1d': (15,19), + } + start,end=slices[shard] + selected=p0 if shard=='p0' else p0+p1[start:end] + json.dump(selected,open('runtime/wallets.json','w'),indent=2) + assert len(p0)==1 and len(p1)==19 + assert sum(row['priority']=='P0' for row in selected)==1 + print({'shard':shard,'wallets':len(selected),'p0':1,'p1':len(selected)-1}) PY - - name: Collect and verify P0-P1 canonical evidence + - name: Collect and verify canonical evidence shell: bash - run: python runtime/collector.py --wallets runtime/wallets.json --out out-wallet-verification + env: + SHARD: ${{ matrix.shard }} + run: python runtime/collector.py --wallets runtime/wallets.json --out "out-${SHARD}" - name: Preserve collector and fixed inputs in artifact if: always() shell: bash + env: + SHARD: ${{ matrix.shard }} run: | - mkdir -p out-wallet-verification/runtime - cp runtime/collector.py runtime/wallets.json out-wallet-verification/runtime/ + mkdir -p "out-${SHARD}/runtime" + cp runtime/collector.py runtime/wallets.json runtime/wallets-all.json "out-${SHARD}/runtime/" - uses: actions/upload-artifact@v4 if: always() with: - name: rhc-wallet-verification-p0p1 - path: out-wallet-verification/ + name: rhc-wallet-verification-${{ matrix.shard }} + path: out-${{ matrix.shard }}/ retention-days: 3 if-no-files-found: error compression-level: 9 From 3283011da3e021a02209ca61946170b0d5452db2 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 02:49:05 +0900 Subject: [PATCH 06/42] data: add fixed P2 wallet address registry --- wallet-verification/p2_addresses.txt | 212 +++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 wallet-verification/p2_addresses.txt diff --git a/wallet-verification/p2_addresses.txt b/wallet-verification/p2_addresses.txt new file mode 100644 index 0000000..e10287f --- /dev/null +++ b/wallet-verification/p2_addresses.txt @@ -0,0 +1,212 @@ +0x01bdea1495c737fa416b337a0f4074ed68c730a6 +0x01da761459535e2ea59125f5c6cf21d17749c435 +0x02cb32b9180ff5343d90bf43a00111a56b477fff +0x02f97ef138c86942a6ed624b0ca7f4d4eda5e815 +0x03879da9151af28d8aa88a407d29473086b03f35 +0x0573ad2ea13f664f860165b6527d75ac8a4c0f7c +0x05c29864a64225ffcd35588f9bbaedc2a5b41f15 +0x077775b1177d6883d3cd78c2926b9673ec5d910c +0x07f9217bfaa091359cd479a413e1cb12be8e654d +0x08a249185fe26478f2fb93855b68fa92bcd2f1cf +0x092dc73ee323dd0fe220568951d6caa9ca2799a3 +0x098f99b534bc59a853dd61453191a7118e5c23a2 +0x0bc4106a6c576749833a2d8ea057db12dda3f432 +0x0c148c98727223897a3df7d8f9668f8596e501af +0x0e1385cf891d4771f0f58ebc56df24f8015db168 +0x0ebdd44c01cd82071eba10d2bce662890c6f860e +0x0f4da1109700b2fabe6e4de57fa2ca0e643f9312 +0x0f5075b1ee5481fbd666001bf77863c0ed6779e9 +0x101be51fb7d46dff2bff97f3c503d97b58a5cd68 +0x121cf483ea9befe75abc77070b09dbf2d0a6f0c3 +0x140880c27cabc582f2bb712577a2d70370ce49bc +0x14a0817752f24121c1e2b641fa1ffda72f4bd554 +0x1741167c7799c57b826ec3e7671c6447c0555ae2 +0x18d62ce13564d16470aa76fcf0a5245220829a58 +0x1990fbb4bd74e40e6ad49be9cfc777f7838bbb29 +0x1b109431e3f60bd83453ad541e6d0600ac585c77 +0x1bcf3d720b13a8b8334e0952d8ac06b150764cba +0x1c3252a011259aaf1234a4d6bb83ba43f5e3c4c7 +0x1caf7013fae98bcac5cf93b84ab791829377131f +0x1e1e5a6b095960636d1d1c051cb6953338217862 +0x1ea8895e4d37046b105a48ca0a285ab532af998b +0x2111ef375ac150a64d42cb520e5a98d4bec763d4 +0x21512d78d9493094bd006ef8eb562b71306b2f40 +0x2450bbe27ac0d25698a340e9215ed765b892ee8e +0x25168924164f414582438b713a75b66eb31a5585 +0x2567fde00dba4c66c32bd5645773294877cd9301 +0x2609b6693ba5beb5de804d2934bf9c6a0970a6e0 +0x27ab2e4666ae2949ce78953772939f81b695cc8e +0x28f1dd1c0322ab37fc92445c80cde829ad69a2ad +0x2a1dfbfd7c3c2b76e5b243393b33dfd310e26758 +0x2b5b35ac5a2d5c1224337ba86bf3816abee69da3 +0x2b8a9d084543163092031d13dcf6dc127294a9b8 +0x2c04b0b3f1ef49381272c4bc59e12d50d1a8e920 +0x2c6853622345008333c5b0d8d063ab917bacc9a6 +0x2df5064481d2f6fa0331a6bf16134252cf308b39 +0x2e6019bb781e157d5ead0cea7ea847fbd71f472e +0x2f4855a60f6563c7ee5cf05716b15f7d554505f0 +0x2f56c34bb177af60dbe1e45864b0ee5c68e11113 +0x3255c27a1817735332df6f72d6c50cfc40165149 +0x337f7842c9f2ba3aafeb14b8c223c4a8d288a88f +0x34061d503ed62b16d01185297f9ada5eab06df84 +0x351c4a16311b0ebdcda5c3fe08950e961e8a58e8 +0x355de61b557e20123a07bbf938b1e136668d0cec +0x364f9419db936b327b2cbb1874ac0d230ab700fd +0x3655d258bbe36fb3bc91b0ea89873929684ee13e +0x3e7589fc06dbe70e46e4b4172c161feb273eba8b +0x3eaa1508337b45b01ba5fb32deda9aea967660bb +0x3fa4faa023131c63d1e1de102ada850cfc7ae0fe +0x40dac0dc38e06e78c98d7fc0290e5072605c62b5 +0x41dac3adf9fe4ee3d2b128b175dc152445490ff4 +0x42b34050251479596e9fde8e90ca9ed754b75bbf +0x44bd5d4bf58bc8a4a7dab49201c9a602f9fabdeb +0x44e8b1a641535fbf312a7a19aca3ee40ce5e082b +0x4a460a6d26f3cfc09ab57f2404588bf1e2cf07aa +0x4b8f79e4662e89e334429029337d8824986317ab +0x4bc98b9112229ee07d85a6827d3bde713c8e7e24 +0x4c341c983a853a1794588bbc756d20bcd124470d +0x4cf6109e8f9d095ae1a4a3119a11fb61e6b24a89 +0x4d411b7929c37d4d7e2ba14b20225179c4748d49 +0x4fb459424f7851c50a9973be9367b1fbc8e6a6a5 +0x501f749615cc2ef56601d738b07eeedfe03ea44e +0x50fdd2288632d2be723fa27e1a4bc5a0d6fea21e +0x5494c57609e0aa6314d69d08561926a25ce54be3 +0x55f5a1af6a61dcef2e5841bb79c5ae9cdca8b88f +0x56e8ffe143cc9e62da32ca517f630910f7741c3f +0x5749946039d49cb780f0d69e014acf81977a4b64 +0x59074561b593d2ae043429a7f3d4a95f6eb92ed7 +0x59d473be479eaae778a4afc152b7dfd24e271b62 +0x5c0b4e4c199c62c01b069b5de11da601c463a15e +0x5c96229ee43c9447d7869c0fe61daf9f42500472 +0x5f2b87edd4d686c52084c60393318ac21c2578ac +0x5f7a7d102165f868c7bcea2e5c9c34875ff0391f +0x6064b2ed3e215e18c40a4dc618b5c5f1b4ab5dd6 +0x6076072add378ddf76283daae38e36a655f8951b +0x61b9c6174289d4d68cfd474524f9dbf56080f996 +0x622e7ac1d3801139f98f5d1ecbec3e68ee1593a7 +0x6370dc41ea5034bcc5aa31088f6f7c751c24f4bd +0x63912303f03eabb8596cf8f9f192b142171becf6 +0x64534962cd6cf21a8c231bda4ec32e5fe5cd86fd +0x6525e566c2accb41c933e127d3becc84edab8b15 +0x6a6586daacca9aca8f1292f169f7b59300c22d67 +0x6cd4c5f7df6cde2fe8d04bd902e6940a63d53b39 +0x6cf666a22058083cf09005d86aba6c4808ab3d75 +0x6d27093b4934cceae0a5b6d2e2bb17e633e1a90b +0x6fc1a98ec5e637d924fcb9463b230bebc5784e55 +0x7308df81fb5490731b69d1af19d36e12223d00c9 +0x7420e9f7e661881284a2ff8d7294188ae663ad7e +0x74e94a88d6fc23efcf571e90b14ee290915802bf +0x74ea3b5149e5e02a111b0c81807e264cb761ef38 +0x760f28329124771ebd2a948832589a0fb51e5238 +0x7711a8b18a9f0baf777ff047fb82c7142f38db78 +0x79678471d4acc69f664af40dcf252be58d849e6b +0x7d03b4e1d84287fdd4aae92e12be00ea212326d9 +0x7e75802c4cf843dc832ee30a824882bdfdffa28c +0x7f1be1c8b738f6ec2f918ef1612cb39113267cf2 +0x809b05442cf4f40581ab03d5fdee31da64cfd505 +0x81491326d3a29c1811089b89ddfa43e3f2160895 +0x817b9b64f3f97e8915b619ecda2bb7aef32599b1 +0x82ca12504724f0dfe92b17fe9b5626cf93a869ed +0x84861de6d8c5cae5d3de3efcda999c11e006d706 +0x85a2f86a39ee4e7750747a657f7a85f71e554ffe +0x86f7a9b1e8d05d4dace9bea276af2cf0dc844c3f +0x8893151487c6e15155db6b842f7cb1b894b7d627 +0x8aaf9e1e49344a3ca28d1591464094d824449214 +0x8b57865e2869badf9d6bd6600ef5547ba9a7d04e +0x8b9830e34c69fbf6ebf4483f894016bbe1522f28 +0x8bd6993b76c037b4930c65b3435ce5298a873084 +0x8d5954789d6dfd9947f9e1c839951ff04ca1258e +0x90400adac01a0f6e8644d7e3cd4dc9ec745fe52e +0x929bfe990bf8f9419475526b1c1d77192ca50048 +0x92b7c190298a346ec3c088167ba9160a253362d1 +0x92d0be614ba3cd8884c790faa8f208266fd79683 +0x99c2ea4c5594f3a705dbce99187b76de9c27d55f +0x9bd15282011faf2c4c40060a423e30799671d114 +0x9ef3daaaa385f0d58baffd7218be0f74382fcbe1 +0x9f8ffcf4d57dc59056581cd9fb7c1f1592878020 +0xa1a295283100d6ce2e422372861c91f6377f73d1 +0xa2f3f6a4a507d81055203b9f3ad5c839b4676ed0 +0xa4d109a314dee58a22360270728c1a96d63ddadd +0xa6df5cc1dad8aad0358c0795842ad0c4798f21f5 +0xa7780b8ad864a28b4f894cf4f6ba7dbda7862970 +0xa77e7fdd6737d4e0ea142f9ef33da7100e7dd2a8 +0xa7fc7a1fdaf5b51e44d55a1b8fc1d9aaa88e4be3 +0xa93d3f66693c199b8978dfc2db0ded2ec77cfec2 +0xa9a3b43429c3c4db2dd03fdd7fe79f2a9a2a3cc8 +0xab28e50d02fd45607097b3f247e0aece4139c314 +0xab3b403381be2f7ea3d5d016932afbf2ae661d6b +0xad0b243de6f7f781c3e14e2293bd82bce0826a0a +0xae6adf709c3e547dce60e5f7d9fa2a6a603b5d71 +0xaecc9f6abf91a3081ef08e6416377df8a9434ca8 +0xafb38b8d69b599edcc3625510d9e54653b7e13ac +0xb488bab43814bc8499eaafe82ed999181bc33205 +0xb55d760b943c630c2a89012cdd0bab6f39580c08 +0xb9005376d286247fe1abc1a73372c6aa2781a261 +0xba23413b6579cb0cbdd33ab922bb4545453d7835 +0xbaa0442773268ea0d269ed620e55c88f58bf975a +0xbbbfaaf3b3a21b2f3396e206d00e542f60294b50 +0xbe07076654206e62947d4eb2de6008cf259e302a +0xbe84b90da3c95c90bb3a85170381e740a9293533 +0xbf2007b9f4843e0e5a8145bc43e966fa99b14ff9 +0xc350bbaa3a024972138568cee9f7dfe5405d22c9 +0xc374273f5dd3cace8b5c794826cc71e106237863 +0xc541e3ee6b25bc320cdc7fe2b9f08d791b1caa43 +0xc5a1378e42103a754718a133ca9079d9d8c904d4 +0xc5b5ca41a7769078d6907ec6450509dc31c3d74b +0xc6705df25ab9f3585ea2e9a2bd5e67758dc6dc36 +0xc6e4a5c75757a827ba2d203658bf53ca8724c49a +0xc8a972fd94291f4b653898d5b59d4b70d83911eb +0xc8ec3c2bf4f35bc7b88820fb008b3ec7f1e76d32 +0xc9a151bff88c1ff77e86a5fe32acadd60dd72ed4 +0xcb606a821ea0d5ddc893006489ff5eaa0fae08ef +0xcbbfc3bd5010067d93378eff47bce0a3e981e7b4 +0xcc3100f1f73388e58d90618f99c4929b450a85bb +0xcc69660c42ad8f8f77277e11c62a33753e666297 +0xccc8fbcf0752c317b0c0ba81f6867affcbf5903d +0xcddcecef3e9bba1fe12cc10b9377ee63af44f1a6 +0xce259e80212e4900bb29380b335f5638d47a810c +0xcf179f16009e80d2dd8b2b2ceac3b27743be81aa +0xd197d55a42597fb7e037866efcc2047de94aa8b2 +0xd1d597f922cfb158dea45ac0c883317087dd6669 +0xd364607836656b5da016a19ffb2b8ceae8f633f6 +0xd59165f18f123e5dfdbcc4b6a813f6f27da07280 +0xd633c4bcfb786ec8efbde7e7363a173755e9242a +0xd648ee1445d59989c28ca86bce5c4429dca0bcde +0xd71899a025ac636f4def3afa83500514cd27c4c4 +0xd8d12dcd511c5e923e4b09c898f1a7b6c210369a +0xd948da29852d0d3ec4a9454f9f7ce089d175c817 +0xda53e31c5f3e274abdd61ecafff8d5000c88a4dc +0xdb696c781be924f9dada8461fc128ceb49d1efae +0xdc78b350ea7ddafcc525a5b7f1bd1fbfb3f60f14 +0xddfcff187b0981f5624fa55f4d7d917cf08f0178 +0xde7ac6de47f91b5c5bec0890a453220be37e9c43 +0xde9beff9471571833e79c144422801d01447e774 +0xdeebaeca8edb54b98757893b3cd19768d9fbb803 +0xdfd5299544721862cf2d02c26771eeb813abb4ca +0xe6d0235b8033359ba843c52bb48b744d9b6e64a5 +0xe85068405f8176992a563bac4d13b8895ae21be2 +0xe9a4ca561a8c49ed5e260f00cd4b58161c42cb7a +0xe9d834fb83c3363d53f0abd389d2a334d939e44b +0xea6cb57bd981f7a6b43d6f4771fb188f9aa81ca7 +0xec6d629073342f339663c2354b9bc0df4b3667ee +0xedbacaa689aadb920001a8ca38cc8655834964af +0xee958df1ae24df33b728c953e6dfee313ba0d43e +0xef4e6e8a25e1651d14b720ea0b754061c5bb3830 +0xef628a27032988886cc51f207fe4205279f7bfe2 +0xef6c278734f7b1562e911663251e48f376d58adf +0xefa7acf41fec0acdc08287c40cba0dda903edd6f +0xf067d2f7f0af58c0d926663f7984dc4e4dc7cd9c +0xf06d641343b2d0ef0de078d3c71da217315a2dd2 +0xf6e684efd5924d091e1eb205ae432aa37f627255 +0xf82745b56e8891f79daebbd88b5436ae3c4f441e +0xf93fa10fe0a8ebfd612853dd90afa3a2b3765b49 +0xf97fa39c4cccb8c014aa94b5dfbb2bea092a90f2 +0xf9c6d42b09aadb273260daa043d548cc1c2f5a82 +0xf9dcb3b0cbef7cab90e3dcbb90b549905555333e +0xf9fd229c2f3082e433b169c3a0e920725386ac92 +0xfa841429c440e37d439e7d06338f2b99ccda807f +0xfaeb5d192a7336a6e635905d8d33a46adbba8513 +0xfc94fe02424cc398b58d4dd8d7e609d86b53a371 +0xfe390633389d9cbd96c939eeddac37e05d4e300a +0xfe76dcaefcde078c7a372a7c1c5c96a074471788 +0xfe80a4f2d6456327663c6b76e167e598e1142364 From 31a4c64bfec645d258982f47de6bd0bdee53e23c Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 02:49:56 +0900 Subject: [PATCH 07/42] ci: add sharded P2 canonical wallet verification --- .../workflows/rhc-wallet-verification-p2.yml | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 .github/workflows/rhc-wallet-verification-p2.yml diff --git a/.github/workflows/rhc-wallet-verification-p2.yml b/.github/workflows/rhc-wallet-verification-p2.yml new file mode 100644 index 0000000..d280f8d --- /dev/null +++ b/.github/workflows/rhc-wallet-verification-p2.yml @@ -0,0 +1,200 @@ +name: RHC wallet verification P2 + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-wallet-verification-p2.yml' + - 'wallet-verification/p2_addresses.txt' + - 'wallet-verification/collector.py.gz.b64' + +permissions: + contents: read + +concurrency: + group: rhc-wallet-verification-p2-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + verify: + strategy: + fail-fast: false + max-parallel: 4 + matrix: + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + runs-on: ubuntu-latest + timeout-minutes: 180 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Materialize collector and deterministic shard input + shell: bash + env: + SHARD: ${{ matrix.shard }} + run: | + set -euo pipefail + mkdir -p runtime + base64 -d wallet-verification/collector.py.gz.b64 | gzip -d > runtime/collector.py + python -m py_compile runtime/collector.py + python - <<'PY' + import json, os, re + from pathlib import Path + + shard = int(os.environ['SHARD']) + shard_count = 16 + addresses = [ + line.strip().lower() + for line in Path('wallet-verification/p2_addresses.txt').read_text().splitlines() + if line.strip() + ] + assert len(addresses) == 212, len(addresses) + assert len(addresses) == len(set(addresses)) + assert all(re.fullmatch(r'0x[a-f0-9]{40}', address) for address in addresses) + + p0 = { + 'wallet': '0x76d387388bea6b60ca6d1e97f446f7e26d39d313', + 'priority': 'P0', + 'priority_points': 11, + 'verification_reasons': [ + 'ONCHAIN_PAID_PRIMARY_ACTIVITY', + 'STRICT_V04_PAID_PROJECT_ACTIVITY', + 'RECENT_SECONDARY_MARKET_ACTIVITY', + ], + 'known_mints': [{ + 'tx_hash': '0x7d02ee071fe595521f1d09671f9b8ad9f0d2d7e1e2bfa017de53d27449cddd23', + 'block_number': 46802320, + 'log_index': 45, + 'contract': '0x797a0889718c30060fc797659328590e86041fbd', + 'collection_name': 'Robin Club', + 'minter': '0x76d387388bea6b60ca6d1e97f446f7e26d39d313', + 'payer': '0x76d387388bea6b60ca6d1e97f446f7e26d39d313', + 'quantity': 1, + 'unit_mint_price_wei': 170000000000000, + 'drop_stage_index': 0, + 'timestamp_utc': '2026-08-26T18:31:03Z', + }], + 'known_sales': [{ + 'tx_hash': '0x8b9f009dbde5e4b55d3fc47242bf0c6b7094c4b07723dcc2148f23fe71dc1e5b', + 'contract': '0x66007297055229066d86294a18edb6e1a2db32d7', + 'collection_name': 'MiNi BRoKER #1692', + 'token_id': '1692', + 'buyer': '0xbbc7974b1d4b353b2557382ac543dfdab7846026', + 'seller': '0x76d387388bea6b60ca6d1e97f446f7e26d39d313', + 'price_native': '0.0007', + 'timestamp_utc': '2026-08-26T18:57:25Z', + 'source_sale_status': 'SOURCE_REPORTED_NOT_RECEIPT_VERIFIED', + }], + } + selected = [address for index, address in enumerate(addresses) if index % shard_count == shard] + rows = [p0] + [ + { + 'wallet': address, + 'priority': 'P2', + 'priority_points': 5, + 'verification_reasons': ['FIXED_P2_VERIFICATION_QUEUE'], + 'known_mints': [], + 'known_sales': [], + } + for address in selected + ] + Path('runtime/wallets.json').write_text(json.dumps(rows, indent=2), encoding='utf-8') + Path('runtime/shard_metadata.json').write_text(json.dumps({ + 'shard': shard, + 'shard_count': shard_count, + 'p2_wallet_count': len(selected), + 'p2_wallets': selected, + }, indent=2), encoding='utf-8') + print({'shard': shard, 'p2_wallets': len(selected), 'rows_with_p0': len(rows)}) + PY + - name: Collect complete P2 address evidence + shell: bash + env: + SHARD: ${{ matrix.shard }} + run: python runtime/collector.py --wallets runtime/wallets.json --out out-wallet-verification + - name: Enforce P2 completeness gate + shell: bash + env: + SHARD: ${{ matrix.shard }} + run: | + python - <<'PY' + import csv, json, os + from pathlib import Path + + out = Path('out-wallet-verification') + metadata = json.loads(Path('runtime/shard_metadata.json').read_text()) + validation = json.loads((out / 'VALIDATION.json').read_text()) + with (out / 'wallet_summary.csv').open(newline='', encoding='utf-8-sig') as handle: + summaries = list(csv.DictReader(handle)) + with (out / 'errors.csv').open(newline='', encoding='utf-8-sig') as handle: + errors = list(csv.DictReader(handle)) + + external_only = { + '0x2b5b35ac5a2d5c1224337ba86bf3816abee69da3', + '0x4bc98b9112229ee07d85a6827d3bde713c8e7e24', + '0xfaeb5d192a7336a6e635905d8d33a46adbba8513', + '0xfe80a4f2d6456327663c6b76e167e598e1142364', + } + failures = [] + expected_rows = metadata['p2_wallet_count'] + 1 + if validation.get('status') != 'PASS': + failures.append({'code': 'UPSTREAM_VALIDATION_NOT_PASS', 'value': validation.get('status')}) + if len(summaries) != expected_rows: + failures.append({'code': 'SUMMARY_COUNT_MISMATCH', 'expected': expected_rows, 'actual': len(summaries)}) + if errors: + failures.append({'code': 'COLLECTOR_ERRORS_PRESENT', 'count': len(errors), 'sample': errors[:10]}) + + seen = set() + for row in summaries: + wallet = row.get('wallet', '').lower() + if wallet in seen: + failures.append({'code': 'DUPLICATE_WALLET_SUMMARY', 'wallet': wallet}) + seen.add(wallet) + if row.get('priority') != 'P2': + continue + nft_rows = int(float(row.get('nft_history_rows') or 0)) + history_meta = json.loads(row.get('history_meta') or '{}') + for kind in ('transactions', 'nft', 'erc1155'): + if history_meta.get(kind, {}).get('truncated_possible'): + failures.append({'code': 'TRUNCATED_HISTORY', 'wallet': wallet, 'kind': kind}) + if wallet not in external_only and nft_rows <= 0: + failures.append({'code': 'EXPECTED_ONCHAIN_NFT_HISTORY_MISSING', 'wallet': wallet}) + + expected_wallets = set(metadata['p2_wallets']) | {'0x76d387388bea6b60ca6d1e97f446f7e26d39d313'} + if seen != expected_wallets: + failures.append({ + 'code': 'WALLET_SET_MISMATCH', + 'missing': sorted(expected_wallets - seen), + 'unexpected': sorted(seen - expected_wallets), + }) + report = { + 'status': 'PASS' if not failures else 'FAIL', + 'shard': metadata['shard'], + 'p2_wallet_count': metadata['p2_wallet_count'], + 'summary_rows': len(summaries), + 'collector_error_rows': len(errors), + 'failures': failures, + 'production_approved_wallets': 0, + } + (out / 'P2_COMPLETENESS_GATE.json').write_text(json.dumps(report, indent=2, sort_keys=True)) + print(json.dumps(report, sort_keys=True)) + if failures: + raise SystemExit(1) + PY + - name: Preserve fixed inputs and collector + if: always() + shell: bash + run: | + mkdir -p out-wallet-verification/runtime + cp runtime/collector.py runtime/wallets.json runtime/shard_metadata.json out-wallet-verification/runtime/ + cp wallet-verification/p2_addresses.txt out-wallet-verification/runtime/ + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-wallet-verification-p2-${{ matrix.shard }} + path: out-wallet-verification/ + retention-days: 5 + if-no-files-found: error + compression-level: 9 From fecf8498fc7021b9d93b57c39f6567bafec6ddbe Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 02:51:58 +0900 Subject: [PATCH 08/42] ci: disable duplicate P0-P1 collection on P2 branch --- .../rhc-wallet-verification-p0p1.yml | 77 ++----------------- 1 file changed, 5 insertions(+), 72 deletions(-) diff --git a/.github/workflows/rhc-wallet-verification-p0p1.yml b/.github/workflows/rhc-wallet-verification-p0p1.yml index 08a1d84..8694955 100644 --- a/.github/workflows/rhc-wallet-verification-p0p1.yml +++ b/.github/workflows/rhc-wallet-verification-p0p1.yml @@ -1,81 +1,14 @@ -name: RHC wallet verification P0-P1 +name: RHC wallet verification P0-P1 (disabled on P2 branch) on: - pull_request: - branches: [main] - types: [opened, synchronize, reopened] - paths: - - '.github/workflows/rhc-wallet-verification-p0p1.yml' - - 'wallet-verification/**' + workflow_dispatch: permissions: contents: read -concurrency: - group: rhc-wallet-verification-p0p1-${{ github.event.pull_request.number }} - cancel-in-progress: true - jobs: - verify: - strategy: - fail-fast: false - matrix: - shard: [p0, p1a, p1b, p1c, p1d] + disabled: + if: ${{ false }} runs-on: ubuntu-latest - timeout-minutes: 45 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Materialize audited collector and fixed shard input - shell: bash - env: - SHARD: ${{ matrix.shard }} - run: | - set -euo pipefail - mkdir -p runtime - base64 -d wallet-verification/collector.py.gz.b64 | gzip -d > runtime/collector.py - base64 -d wallet-verification/wallets.json.gz.b64 | gzip -d > runtime/wallets-all.json - python -m py_compile runtime/collector.py - python - <<'PY' - import json, os - rows=json.load(open('runtime/wallets-all.json')) - p0=[row for row in rows if row['priority']=='P0'] - p1=[row for row in rows if row['priority']=='P1'] - shard=os.environ['SHARD'] - slices={ - 'p0': (0,0), - 'p1a': (0,5), - 'p1b': (5,10), - 'p1c': (10,15), - 'p1d': (15,19), - } - start,end=slices[shard] - selected=p0 if shard=='p0' else p0+p1[start:end] - json.dump(selected,open('runtime/wallets.json','w'),indent=2) - assert len(p0)==1 and len(p1)==19 - assert sum(row['priority']=='P0' for row in selected)==1 - print({'shard':shard,'wallets':len(selected),'p0':1,'p1':len(selected)-1}) - PY - - name: Collect and verify canonical evidence - shell: bash - env: - SHARD: ${{ matrix.shard }} - run: python runtime/collector.py --wallets runtime/wallets.json --out "out-${SHARD}" - - name: Preserve collector and fixed inputs in artifact - if: always() - shell: bash - env: - SHARD: ${{ matrix.shard }} - run: | - mkdir -p "out-${SHARD}/runtime" - cp runtime/collector.py runtime/wallets.json runtime/wallets-all.json "out-${SHARD}/runtime/" - - uses: actions/upload-artifact@v4 - if: always() - with: - name: rhc-wallet-verification-${{ matrix.shard }} - path: out-${{ matrix.shard }}/ - retention-days: 3 - if-no-files-found: error - compression-level: 9 + - run: echo 'P0-P1 evidence already collected; disabled on P2-only branch.' From 397cd71660824ed5e6515ab2fb91a9d18c8afa47 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 06:57:01 +0900 Subject: [PATCH 09/42] chore: probe wallet-indexed ERC721 Transfer log retrieval --- wallet-verification/wallet_log_probe.py | 82 +++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 wallet-verification/wallet_log_probe.py diff --git a/wallet-verification/wallet_log_probe.py b/wallet-verification/wallet_log_probe.py new file mode 100644 index 0000000..2aa432a --- /dev/null +++ b/wallet-verification/wallet_log_probe.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import time +import urllib.parse +import urllib.request +from pathlib import Path + +BASES = [ + "https://robinhoodchain.blockscout.com/api", + "https://explorer.hoodmarketcap.com/api", +] +TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" +WALLETS = [ + # Known paid-primary wallet whose address index returned zero. + "0x01bdea1495c737fa416b337a0f4074ed68c730a6", + # Known P0 control. + "0x76d387388bea6b60ca6d1e97f446f7e26d39d313", +] +OUT = Path("out-wallet-log-probe") +OUT.mkdir(parents=True, exist_ok=True) + + +def get(url: str, params: dict[str, object]) -> tuple[int, object, str]: + full = url + "?" + urllib.parse.urlencode(params) + req = urllib.request.Request(full, headers={"accept": "application/json", "user-agent": "RHC-Wallet-Log-Probe/1.0"}) + try: + with urllib.request.urlopen(req, timeout=90) as response: + body = response.read() + status = response.status + except Exception as exc: + return 0, {"error": repr(exc)}, full + try: + payload = json.loads(body.decode("utf-8")) + except Exception: + payload = {"raw": body[:1000].decode("utf-8", "replace")} + return status, payload, full + + +def padded(address: str) -> str: + return "0x" + "0" * 24 + address[2:] + + +records = [] +for wallet in WALLETS: + topic = padded(wallet) + for base in BASES: + for direction, topic_key, op_key in ( + ("incoming", "topic2", "topic0_2_opr"), + ("outgoing", "topic1", "topic0_1_opr"), + ): + status, payload, url = get(base, { + "module": "logs", + "action": "getLogs", + "fromBlock": 0, + "toBlock": "latest", + "topic0": TRANSFER, + topic_key: topic, + op_key: "and", + }) + rows = payload.get("result") if isinstance(payload, dict) else None + if not isinstance(rows, list): + rows = [] + erc721 = [row for row in rows if len(row.get("topics") or []) == 4] + record = { + "wallet": wallet, + "base": base, + "direction": direction, + "http_status": status, + "message": payload.get("message") if isinstance(payload, dict) else None, + "result_rows": len(rows), + "erc721_rows": len(erc721), + "first_block": rows[0].get("blockNumber") if rows else None, + "last_block": rows[-1].get("blockNumber") if rows else None, + "url": url, + } + records.append(record) + print(record, flush=True) + time.sleep(1.2) + +(OUT / "results.json").write_text(json.dumps(records, indent=2, sort_keys=True), encoding="utf-8") From a966824d9f20d4958a55b0c6c0b162a119c09a06 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 06:57:14 +0900 Subject: [PATCH 10/42] ci: run wallet-indexed ERC721 Transfer log probe --- .github/workflows/rhc-wallet-log-probe.yml | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/rhc-wallet-log-probe.yml diff --git a/.github/workflows/rhc-wallet-log-probe.yml b/.github/workflows/rhc-wallet-log-probe.yml new file mode 100644 index 0000000..18d466e --- /dev/null +++ b/.github/workflows/rhc-wallet-log-probe.yml @@ -0,0 +1,30 @@ +name: RHC wallet-indexed Transfer log probe + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-wallet-log-probe.yml' + - 'wallet-verification/wallet_log_probe.py' + +permissions: + contents: read + +jobs: + probe: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python wallet-verification/wallet_log_probe.py + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-wallet-log-probe + path: out-wallet-log-probe/ + retention-days: 2 + if-no-files-found: error From e9250d61e16d6621d0421623b0623541510c44dd Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 06:58:32 +0900 Subject: [PATCH 11/42] ci: disable duplicate full P2 reruns after artifact recovery --- .../workflows/rhc-wallet-verification-p2.yml | 195 +----------------- 1 file changed, 4 insertions(+), 191 deletions(-) diff --git a/.github/workflows/rhc-wallet-verification-p2.yml b/.github/workflows/rhc-wallet-verification-p2.yml index d280f8d..d295654 100644 --- a/.github/workflows/rhc-wallet-verification-p2.yml +++ b/.github/workflows/rhc-wallet-verification-p2.yml @@ -1,200 +1,13 @@ -name: RHC wallet verification P2 +name: RHC wallet verification P2 (manual only) on: - pull_request: - branches: [main] - types: [opened, synchronize, reopened] - paths: - - '.github/workflows/rhc-wallet-verification-p2.yml' - - 'wallet-verification/p2_addresses.txt' - - 'wallet-verification/collector.py.gz.b64' + workflow_dispatch: permissions: contents: read -concurrency: - group: rhc-wallet-verification-p2-${{ github.event.pull_request.number }} - cancel-in-progress: true - jobs: - verify: - strategy: - fail-fast: false - max-parallel: 4 - matrix: - shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + disabled: runs-on: ubuntu-latest - timeout-minutes: 180 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Materialize collector and deterministic shard input - shell: bash - env: - SHARD: ${{ matrix.shard }} - run: | - set -euo pipefail - mkdir -p runtime - base64 -d wallet-verification/collector.py.gz.b64 | gzip -d > runtime/collector.py - python -m py_compile runtime/collector.py - python - <<'PY' - import json, os, re - from pathlib import Path - - shard = int(os.environ['SHARD']) - shard_count = 16 - addresses = [ - line.strip().lower() - for line in Path('wallet-verification/p2_addresses.txt').read_text().splitlines() - if line.strip() - ] - assert len(addresses) == 212, len(addresses) - assert len(addresses) == len(set(addresses)) - assert all(re.fullmatch(r'0x[a-f0-9]{40}', address) for address in addresses) - - p0 = { - 'wallet': '0x76d387388bea6b60ca6d1e97f446f7e26d39d313', - 'priority': 'P0', - 'priority_points': 11, - 'verification_reasons': [ - 'ONCHAIN_PAID_PRIMARY_ACTIVITY', - 'STRICT_V04_PAID_PROJECT_ACTIVITY', - 'RECENT_SECONDARY_MARKET_ACTIVITY', - ], - 'known_mints': [{ - 'tx_hash': '0x7d02ee071fe595521f1d09671f9b8ad9f0d2d7e1e2bfa017de53d27449cddd23', - 'block_number': 46802320, - 'log_index': 45, - 'contract': '0x797a0889718c30060fc797659328590e86041fbd', - 'collection_name': 'Robin Club', - 'minter': '0x76d387388bea6b60ca6d1e97f446f7e26d39d313', - 'payer': '0x76d387388bea6b60ca6d1e97f446f7e26d39d313', - 'quantity': 1, - 'unit_mint_price_wei': 170000000000000, - 'drop_stage_index': 0, - 'timestamp_utc': '2026-08-26T18:31:03Z', - }], - 'known_sales': [{ - 'tx_hash': '0x8b9f009dbde5e4b55d3fc47242bf0c6b7094c4b07723dcc2148f23fe71dc1e5b', - 'contract': '0x66007297055229066d86294a18edb6e1a2db32d7', - 'collection_name': 'MiNi BRoKER #1692', - 'token_id': '1692', - 'buyer': '0xbbc7974b1d4b353b2557382ac543dfdab7846026', - 'seller': '0x76d387388bea6b60ca6d1e97f446f7e26d39d313', - 'price_native': '0.0007', - 'timestamp_utc': '2026-08-26T18:57:25Z', - 'source_sale_status': 'SOURCE_REPORTED_NOT_RECEIPT_VERIFIED', - }], - } - selected = [address for index, address in enumerate(addresses) if index % shard_count == shard] - rows = [p0] + [ - { - 'wallet': address, - 'priority': 'P2', - 'priority_points': 5, - 'verification_reasons': ['FIXED_P2_VERIFICATION_QUEUE'], - 'known_mints': [], - 'known_sales': [], - } - for address in selected - ] - Path('runtime/wallets.json').write_text(json.dumps(rows, indent=2), encoding='utf-8') - Path('runtime/shard_metadata.json').write_text(json.dumps({ - 'shard': shard, - 'shard_count': shard_count, - 'p2_wallet_count': len(selected), - 'p2_wallets': selected, - }, indent=2), encoding='utf-8') - print({'shard': shard, 'p2_wallets': len(selected), 'rows_with_p0': len(rows)}) - PY - - name: Collect complete P2 address evidence - shell: bash - env: - SHARD: ${{ matrix.shard }} - run: python runtime/collector.py --wallets runtime/wallets.json --out out-wallet-verification - - name: Enforce P2 completeness gate - shell: bash - env: - SHARD: ${{ matrix.shard }} - run: | - python - <<'PY' - import csv, json, os - from pathlib import Path - - out = Path('out-wallet-verification') - metadata = json.loads(Path('runtime/shard_metadata.json').read_text()) - validation = json.loads((out / 'VALIDATION.json').read_text()) - with (out / 'wallet_summary.csv').open(newline='', encoding='utf-8-sig') as handle: - summaries = list(csv.DictReader(handle)) - with (out / 'errors.csv').open(newline='', encoding='utf-8-sig') as handle: - errors = list(csv.DictReader(handle)) - - external_only = { - '0x2b5b35ac5a2d5c1224337ba86bf3816abee69da3', - '0x4bc98b9112229ee07d85a6827d3bde713c8e7e24', - '0xfaeb5d192a7336a6e635905d8d33a46adbba8513', - '0xfe80a4f2d6456327663c6b76e167e598e1142364', - } - failures = [] - expected_rows = metadata['p2_wallet_count'] + 1 - if validation.get('status') != 'PASS': - failures.append({'code': 'UPSTREAM_VALIDATION_NOT_PASS', 'value': validation.get('status')}) - if len(summaries) != expected_rows: - failures.append({'code': 'SUMMARY_COUNT_MISMATCH', 'expected': expected_rows, 'actual': len(summaries)}) - if errors: - failures.append({'code': 'COLLECTOR_ERRORS_PRESENT', 'count': len(errors), 'sample': errors[:10]}) - - seen = set() - for row in summaries: - wallet = row.get('wallet', '').lower() - if wallet in seen: - failures.append({'code': 'DUPLICATE_WALLET_SUMMARY', 'wallet': wallet}) - seen.add(wallet) - if row.get('priority') != 'P2': - continue - nft_rows = int(float(row.get('nft_history_rows') or 0)) - history_meta = json.loads(row.get('history_meta') or '{}') - for kind in ('transactions', 'nft', 'erc1155'): - if history_meta.get(kind, {}).get('truncated_possible'): - failures.append({'code': 'TRUNCATED_HISTORY', 'wallet': wallet, 'kind': kind}) - if wallet not in external_only and nft_rows <= 0: - failures.append({'code': 'EXPECTED_ONCHAIN_NFT_HISTORY_MISSING', 'wallet': wallet}) - - expected_wallets = set(metadata['p2_wallets']) | {'0x76d387388bea6b60ca6d1e97f446f7e26d39d313'} - if seen != expected_wallets: - failures.append({ - 'code': 'WALLET_SET_MISMATCH', - 'missing': sorted(expected_wallets - seen), - 'unexpected': sorted(seen - expected_wallets), - }) - report = { - 'status': 'PASS' if not failures else 'FAIL', - 'shard': metadata['shard'], - 'p2_wallet_count': metadata['p2_wallet_count'], - 'summary_rows': len(summaries), - 'collector_error_rows': len(errors), - 'failures': failures, - 'production_approved_wallets': 0, - } - (out / 'P2_COMPLETENESS_GATE.json').write_text(json.dumps(report, indent=2, sort_keys=True)) - print(json.dumps(report, sort_keys=True)) - if failures: - raise SystemExit(1) - PY - - name: Preserve fixed inputs and collector - if: always() - shell: bash - run: | - mkdir -p out-wallet-verification/runtime - cp runtime/collector.py runtime/wallets.json runtime/shard_metadata.json out-wallet-verification/runtime/ - cp wallet-verification/p2_addresses.txt out-wallet-verification/runtime/ - - uses: actions/upload-artifact@v4 - if: always() - with: - name: rhc-wallet-verification-p2-${{ matrix.shard }} - path: out-wallet-verification/ - retention-days: 5 - if-no-files-found: error - compression-level: 9 + - run: echo 'The original 212-wallet broad collection is complete. Use gap-only canonical collectors; do not rerun all shards.' From 678f40c7f47ea6864119e1f9bc431a360fae837b Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 06:59:22 +0900 Subject: [PATCH 12/42] fix: probe canonical wallet Transfer logs through JSON-RPC --- wallet-verification/wallet_log_probe.py | 155 +++++++++++++++--------- 1 file changed, 98 insertions(+), 57 deletions(-) diff --git a/wallet-verification/wallet_log_probe.py b/wallet-verification/wallet_log_probe.py index 2aa432a..5736e18 100644 --- a/wallet-verification/wallet_log_probe.py +++ b/wallet-verification/wallet_log_probe.py @@ -2,81 +2,122 @@ from __future__ import annotations import json -import time -import urllib.parse +import urllib.error import urllib.request from pathlib import Path -BASES = [ - "https://robinhoodchain.blockscout.com/api", - "https://explorer.hoodmarketcap.com/api", -] +RPC = "https://rpc.mainnet.chain.robinhood.com" TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" -WALLETS = [ - # Known paid-primary wallet whose address index returned zero. - "0x01bdea1495c737fa416b337a0f4074ed68c730a6", - # Known P0 control. - "0x76d387388bea6b60ca6d1e97f446f7e26d39d313", +ZERO_TOPIC = "0x" + "0" * 64 +TARGETS = [ + { + "wallet": "0x01bdea1495c737fa416b337a0f4074ed68c730a6", + "known_tx": "0x7b8a716d053e4a62bdf5fe00fbe68c2cae5ab76f284ba03504bffa5befa1d682", + "known_block": 46771285, + }, + { + "wallet": "0x76d387388bea6b60ca6d1e97f446f7e26d39d313", + "known_tx": "0x7d02ee071fe595521f1d09671f9b8ad9f0d2d7e1e2bfa017de53d27449cddd23", + "known_block": 46802320, + }, ] OUT = Path("out-wallet-log-probe") OUT.mkdir(parents=True, exist_ok=True) -def get(url: str, params: dict[str, object]) -> tuple[int, object, str]: - full = url + "?" + urllib.parse.urlencode(params) - req = urllib.request.Request(full, headers={"accept": "application/json", "user-agent": "RHC-Wallet-Log-Probe/1.0"}) +def rpc(method: str, params: list[object]) -> dict[str, object]: + payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode() + req = urllib.request.Request( + RPC, + data=payload, + method="POST", + headers={ + "content-type": "application/json", + "accept": "application/json", + "user-agent": "RHC-Wallet-Log-Probe/2.0", + }, + ) try: - with urllib.request.urlopen(req, timeout=90) as response: + with urllib.request.urlopen(req, timeout=120) as response: body = response.read() status = response.status + except urllib.error.HTTPError as exc: + return {"http_status": exc.code, "error": exc.read().decode("utf-8", "replace")[:3000]} except Exception as exc: - return 0, {"error": repr(exc)}, full + return {"http_status": 0, "error": repr(exc)} try: - payload = json.loads(body.decode("utf-8")) - except Exception: - payload = {"raw": body[:1000].decode("utf-8", "replace")} - return status, payload, full + decoded = json.loads(body.decode("utf-8")) + except Exception as exc: + return {"http_status": status, "error": f"decode:{exc!r}", "raw": body[:1000].decode("utf-8", "replace")} + return {"http_status": status, **decoded} def padded(address: str) -> str: - return "0x" + "0" * 24 + address[2:] + return "0x" + "0" * 24 + address[2:].lower() + + +head_response = rpc("eth_blockNumber", []) +head_hex = str(head_response.get("result") or "0x0") +head = int(head_hex, 16) +records: list[dict[str, object]] = [{"probe": "head", "head": head, "response": head_response}] +for target in TARGETS: + wallet = target["wallet"] + topic_wallet = padded(wallet) + receipt = rpc("eth_getTransactionReceipt", [target["known_tx"]]) + records.append({"probe": "known_receipt", **target, "response": receipt}) -records = [] -for wallet in WALLETS: - topic = padded(wallet) - for base in BASES: - for direction, topic_key, op_key in ( - ("incoming", "topic2", "topic0_2_opr"), - ("outgoing", "topic1", "topic0_1_opr"), - ): - status, payload, url = get(base, { - "module": "logs", - "action": "getLogs", - "fromBlock": 0, - "toBlock": "latest", - "topic0": TRANSFER, - topic_key: topic, - op_key: "and", - }) - rows = payload.get("result") if isinstance(payload, dict) else None - if not isinstance(rows, list): - rows = [] - erc721 = [row for row in rows if len(row.get("topics") or []) == 4] - record = { - "wallet": wallet, - "base": base, - "direction": direction, - "http_status": status, - "message": payload.get("message") if isinstance(payload, dict) else None, - "result_rows": len(rows), - "erc721_rows": len(erc721), - "first_block": rows[0].get("blockNumber") if rows else None, - "last_block": rows[-1].get("blockNumber") if rows else None, - "url": url, - } - records.append(record) - print(record, flush=True) - time.sleep(1.2) + filters = [ + ( + "exact_known_mint_window", + target["known_block"] - 2, + target["known_block"] + 2, + [TRANSFER, ZERO_TOPIC, topic_wallet], + ), + ( + "incoming_recent_2m_blocks", + max(0, head - 2_000_000), + head, + [TRANSFER, None, topic_wallet], + ), + ( + "outgoing_recent_2m_blocks", + max(0, head - 2_000_000), + head, + [TRANSFER, topic_wallet], + ), + ( + "incoming_full_range", + 0, + head, + [TRANSFER, None, topic_wallet], + ), + ( + "outgoing_full_range", + 0, + head, + [TRANSFER, topic_wallet], + ), + ] + for name, start, end, topics in filters: + response = rpc( + "eth_getLogs", + [{"fromBlock": hex(start), "toBlock": hex(end), "topics": topics}], + ) + result = response.get("result") + rows = result if isinstance(result, list) else [] + record = { + "probe": name, + "wallet": wallet, + "from_block": start, + "to_block": end, + "row_count": len(rows), + "first_block": int(rows[0]["blockNumber"], 16) if rows else None, + "last_block": int(rows[-1]["blockNumber"], 16) if rows else None, + "http_status": response.get("http_status"), + "rpc_error": response.get("error"), + } + records.append(record) + print(record, flush=True) (OUT / "results.json").write_text(json.dumps(records, indent=2, sort_keys=True), encoding="utf-8") From c081db1dc6c215275c8f964124463f613576011d Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:36:43 +0900 Subject: [PATCH 13/42] feat: add fail-closed canonical Robinhood RPC client --- rhc-data-completion/canonical_rpc.py | 161 +++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 rhc-data-completion/canonical_rpc.py diff --git a/rhc-data-completion/canonical_rpc.py b/rhc-data-completion/canonical_rpc.py new file mode 100644 index 0000000..ecb0d68 --- /dev/null +++ b/rhc-data-completion/canonical_rpc.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import os +import random +import time +import urllib.error +import urllib.request +from dataclasses import dataclass +from typing import Any + +DEFAULT_RPC_URLS = [ + "https://rpc.mainnet.chain.robinhood.com", +] + + +@dataclass +class RpcFailure(RuntimeError): + method: str + params: list[Any] + errors: list[dict[str, Any]] + + def __str__(self) -> str: + return json.dumps({"method": self.method, "params": self.params, "errors": self.errors}, sort_keys=True) + + +class CanonicalRpc: + def __init__(self, min_interval: float = 1.35, max_attempts: int = 12, timeout: int = 90): + configured = [x.strip() for x in os.getenv("RHC_RPC_URLS", "").split(",") if x.strip()] + self.urls = configured or DEFAULT_RPC_URLS + self.min_interval = min_interval + self.max_attempts = max_attempts + self.timeout = timeout + self.last_request = 0.0 + self.stats: dict[str, int] = {} + + def _pace(self) -> None: + remaining = self.min_interval - (time.monotonic() - self.last_request) + if remaining > 0: + time.sleep(remaining) + + def _bump(self, key: str) -> None: + self.stats[key] = self.stats.get(key, 0) + 1 + + def call(self, method: str, params: list[Any]) -> Any: + errors: list[dict[str, Any]] = [] + for attempt in range(1, self.max_attempts + 1): + for url in self.urls: + self._pace() + payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode("utf-8") + request = urllib.request.Request( + url, + data=payload, + method="POST", + headers={ + "content-type": "application/json", + "accept": "application/json", + "user-agent": "RHC-Canonical-Data-Completion/1.0", + }, + ) + try: + with urllib.request.urlopen(request, timeout=self.timeout) as response: + body = response.read() + status = response.status + self.last_request = time.monotonic() + self._bump(f"http_{status}") + decoded = json.loads(body.decode("utf-8")) + if isinstance(decoded, dict) and decoded.get("error") is not None: + error = decoded["error"] + code = error.get("code") if isinstance(error, dict) else None + errors.append({"url": url, "attempt": attempt, "rpc_error": error}) + self._bump(f"rpc_error_{code}") + if code in (429, -32005, -32016, -32603) or "limit" in str(error).lower() or "too many" in str(error).lower(): + continue + raise RpcFailure(method, params, errors) + if not isinstance(decoded, dict) or "result" not in decoded: + errors.append({"url": url, "attempt": attempt, "invalid_response": decoded}) + continue + return decoded["result"] + except urllib.error.HTTPError as exc: + self.last_request = time.monotonic() + body = exc.read(2000).decode("utf-8", "replace") + errors.append({"url": url, "attempt": attempt, "http": exc.code, "body": body}) + self._bump(f"http_{exc.code}") + if exc.code not in (408, 425, 429, 500, 502, 503, 504): + raise RpcFailure(method, params, errors) from exc + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc: + self.last_request = time.monotonic() + errors.append({"url": url, "attempt": attempt, "network_or_decode": repr(exc)}) + self._bump("network_or_decode") + if attempt < self.max_attempts: + delay = min(90.0, (2 ** min(attempt, 6)) + random.random() * 3) + time.sleep(delay) + raise RpcFailure(method, params, errors) + + def block_number(self) -> int: + return int(self.call("eth_blockNumber", []), 16) + + def block(self, block_number: int) -> dict[str, Any]: + value = self.call("eth_getBlockByNumber", [hex(block_number), False]) + if not isinstance(value, dict): + raise RuntimeError(f"missing block {block_number}") + return value + + def logs(self, filt: dict[str, Any]) -> list[dict[str, Any]]: + value = self.call("eth_getLogs", [filt]) + if not isinstance(value, list): + raise RuntimeError(f"eth_getLogs returned {type(value)!r}") + return [x for x in value if isinstance(x, dict)] + + +def log_key(log: dict[str, Any]) -> tuple[str, str, int]: + return ( + str(log.get("blockHash") or "").lower(), + str(log.get("transactionHash") or "").lower(), + int(str(log.get("logIndex") or "0x0"), 16), + ) + + +def fetch_logs_recursive( + rpc: CanonicalRpc, + *, + from_block: int, + to_block: int, + address: str | None = None, + topics: list[Any] | None = None, + max_depth: int = 30, + depth: int = 0, +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + filt: dict[str, Any] = {"fromBlock": hex(from_block), "toBlock": hex(to_block)} + if address: + filt["address"] = address + if topics is not None: + filt["topics"] = topics + try: + rows = rpc.logs(filt) + return rows, [{"from_block": from_block, "to_block": to_block, "status": "PASS", "rows": len(rows), "depth": depth}] + except Exception as exc: + if from_block >= to_block or depth >= max_depth: + return [], [{"from_block": from_block, "to_block": to_block, "status": "FAIL", "rows": 0, "depth": depth, "error": repr(exc)}] + midpoint = (from_block + to_block) // 2 + left_rows, left_cov = fetch_logs_recursive( + rpc, + from_block=from_block, + to_block=midpoint, + address=address, + topics=topics, + max_depth=max_depth, + depth=depth + 1, + ) + right_rows, right_cov = fetch_logs_recursive( + rpc, + from_block=midpoint + 1, + to_block=to_block, + address=address, + topics=topics, + max_depth=max_depth, + depth=depth + 1, + ) + return left_rows + right_rows, left_cov + right_cov From ac58ed9c8e117a9a2fb80426d665e909aaee9197 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:37:11 +0900 Subject: [PATCH 14/42] feat: collect complete canonical SeaDrop and Seaport histories --- rhc-data-completion/collect_global_events.py | 175 +++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 rhc-data-completion/collect_global_events.py diff --git a/rhc-data-completion/collect_global_events.py b/rhc-data-completion/collect_global_events.py new file mode 100644 index 0000000..203ab0b --- /dev/null +++ b/rhc-data-completion/collect_global_events.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +from pathlib import Path +from typing import Any + +from canonical_rpc import CanonicalRpc, fetch_logs_recursive, log_key + +TARGETS = { + "seadrop": { + "address": "0x00005ea00ac477b1030ce78506496e8c2de24bf5", + "topic0": "0xe90cf9cc0a552cf52ea6ff74ece0f1c8ae8cc9ad630d3181f55ac43ca076b7d6", + }, + "seaport": { + "address": "0x0000000000000068f116a894984e2db1123eb395", + "topic0": "0x9d9af8e38d66c62e2c12f0225249fd9d721c54b83f48d9352c97c6cacdcb6f31", + }, +} + + +def integer(value: Any) -> int: + if isinstance(value, int): + return value + return int(str(value), 16) if str(value).startswith("0x") else int(str(value)) + + +def address_from_topic(topic: str) -> str: + return "0x" + topic.lower()[-40:] + + +def data_words(data: str) -> list[str]: + raw = data[2:] if data.startswith("0x") else data + if len(raw) % 64: + raise ValueError("ABI data length is not a word multiple") + return [raw[i:i + 64] for i in range(0, len(raw), 64)] + + +def normalize(log: dict[str, Any], target: str) -> dict[str, Any]: + topics = [str(x).lower() for x in (log.get("topics") or [])] + row: dict[str, Any] = { + "target": target, + "block_number": integer(log["blockNumber"]), + "block_hash": str(log.get("blockHash") or "").lower(), + "transaction_hash": str(log.get("transactionHash") or "").lower(), + "transaction_index": integer(log.get("transactionIndex") or "0x0"), + "log_index": integer(log.get("logIndex") or "0x0"), + "emitter": str(log.get("address") or "").lower(), + "removed": bool(log.get("removed", False)), + "topics_json": json.dumps(topics, separators=(",", ":")), + "data": str(log.get("data") or "0x").lower(), + } + if target == "seadrop" and len(topics) == 4: + words = data_words(row["data"]) + if len(words) >= 5: + row.update({ + "nft_contract": address_from_topic(topics[1]), + "minter": address_from_topic(topics[2]), + "fee_recipient": address_from_topic(topics[3]), + "payer": "0x" + words[0][-40:], + "quantity_minted": int(words[1], 16), + "unit_mint_price_wei": int(words[2], 16), + "fee_bps": int(words[3], 16), + "drop_stage_index": int(words[4], 16), + }) + return row + + +def sha256(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--target", choices=sorted(TARGETS), required=True) + parser.add_argument("--out", required=True) + parser.add_argument("--chunk", type=int, default=2_000_000) + parser.add_argument("--confirmations", type=int, default=100) + args = parser.parse_args() + + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + config = TARGETS[args.target] + rpc = CanonicalRpc(min_interval=1.35) + latest = rpc.block_number() + fixed_head = max(0, latest - args.confirmations) + fixed_block = rpc.block(fixed_head) + + all_logs: list[dict[str, Any]] = [] + coverage: list[dict[str, Any]] = [] + cursor = 0 + while cursor <= fixed_head: + end = min(fixed_head, cursor + args.chunk - 1) + rows, cov = fetch_logs_recursive( + rpc, + from_block=cursor, + to_block=end, + address=config["address"], + topics=[config["topic0"]], + ) + all_logs.extend(rows) + coverage.extend(cov) + print({"target": args.target, "from": cursor, "to": end, "rows": len(rows), "coverage_parts": len(cov)}, flush=True) + cursor = end + 1 + + dedup: dict[tuple[str, str, int], dict[str, Any]] = {} + for log in all_logs: + dedup[log_key(log)] = log + ordered = sorted(dedup.values(), key=lambda x: (integer(x["blockNumber"]), integer(x.get("transactionIndex") or "0x0"), integer(x.get("logIndex") or "0x0"))) + normalized = [normalize(log, args.target) for log in ordered] + + raw_path = out / f"{args.target}_logs.jsonl" + with raw_path.open("w", encoding="utf-8", newline="\n") as handle: + for log in ordered: + handle.write(json.dumps(log, sort_keys=True, separators=(",", ":")) + "\n") + + csv_path = out / f"{args.target}_events.csv" + fields: list[str] = [] + seen: set[str] = set() + for row in normalized: + for key in row: + if key not in seen: + seen.add(key) + fields.append(key) + with csv_path.open("w", newline="", encoding="utf-8-sig") as handle: + writer = csv.DictWriter(handle, fieldnames=fields) + writer.writeheader() + writer.writerows(normalized) + + coverage_path = out / "coverage.csv" + coverage_fields = ["from_block", "to_block", "status", "rows", "depth", "error"] + with coverage_path.open("w", newline="", encoding="utf-8-sig") as handle: + writer = csv.DictWriter(handle, fieldnames=coverage_fields, extrasaction="ignore") + writer.writeheader() + writer.writerows(coverage) + + failures = [row for row in coverage if row.get("status") != "PASS"] + removed = [row for row in normalized if row.get("removed")] + unique_keys = {(row["block_hash"], row["transaction_hash"], row["log_index"]) for row in normalized} + validation = { + "status": "PASS" if not failures and not removed and len(unique_keys) == len(normalized) else "FAIL", + "target": args.target, + "chain_id": 4663, + "latest_observed_block": latest, + "fixed_head_block": fixed_head, + "fixed_head_hash": str(fixed_block.get("hash") or "").lower(), + "confirmations_excluded": args.confirmations, + "event_rows": len(normalized), + "duplicate_rows_removed": len(all_logs) - len(ordered), + "coverage_parts": len(coverage), + "coverage_failures": failures, + "removed_rows": len(removed), + "rpc_stats": rpc.stats, + "source": "CANONICAL_PUBLIC_RPC_ETH_GETLOGS", + } + (out / "VALIDATION.json").write_text(json.dumps(validation, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + manifest = [] + for path in sorted(out.iterdir()): + if path.is_file(): + manifest.append({"path": path.name, "bytes": path.stat().st_size, "sha256": sha256(path)}) + (out / "MANIFEST.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + print(json.dumps(validation, sort_keys=True), flush=True) + if validation["status"] != "PASS": + raise SystemExit(1) + + +if __name__ == "__main__": + main() From a0b5a80ec7c025a6d1fed42b1841f6611076e41d Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:37:54 +0900 Subject: [PATCH 15/42] feat: collect canonical NFT transfer histories for all candidate wallets --- .../collect_wallet_transfers.py | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 rhc-data-completion/collect_wallet_transfers.py diff --git a/rhc-data-completion/collect_wallet_transfers.py b/rhc-data-completion/collect_wallet_transfers.py new file mode 100644 index 0000000..fb0ab4b --- /dev/null +++ b/rhc-data-completion/collect_wallet_transfers.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import subprocess +import sys +from pathlib import Path +from typing import Any + +from canonical_rpc import CanonicalRpc, fetch_logs_recursive, log_key + +TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" +EXTERNAL_ONLY = { + "0x2b5b35ac5a2d5c1224337ba86bf3816abee69da3", + "0x4bc98b9112229ee07d85a6827d3bde713c8e7e24", + "0xfaeb5d192a7336a6e635905d8d33a46adbba8513", + "0xfe80a4f2d6456327663c6b76e167e598e1142364", +} + + +def keccak_topic(signature: str) -> str: + try: + from Crypto.Hash import keccak # type: ignore + except ImportError: + subprocess.check_call([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", "pycryptodome==3.23.0"]) + from Crypto.Hash import keccak # type: ignore + digest = keccak.new(digest_bits=256) + digest.update(signature.encode("ascii")) + return "0x" + digest.hexdigest() + + +TRANSFER_SINGLE = keccak_topic("TransferSingle(address,address,address,uint256,uint256)") +TRANSFER_BATCH = keccak_topic("TransferBatch(address,address,address,uint256[],uint256[])") + + +def padded(address: str) -> str: + return "0x" + "0" * 24 + address.lower()[2:] + + +def integer(value: Any) -> int: + if isinstance(value, int): + return value + text = str(value or "0") + return int(text, 16) if text.startswith("0x") else int(float(text)) + + +def sha256(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--wallets", required=True) + parser.add_argument("--out", required=True) + parser.add_argument("--shard", type=int, required=True) + parser.add_argument("--shard-count", type=int, required=True) + parser.add_argument("--fixed-head", type=int, required=True) + args = parser.parse_args() + + all_wallets = json.loads(Path(args.wallets).read_text(encoding="utf-8")) + selected = [row for index, row in enumerate(all_wallets) if index % args.shard_count == args.shard] + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + rpc = CanonicalRpc(min_interval=1.45) + + event_queries = [ + ("ERC721_IN", TRANSFER, 2, 4), + ("ERC721_OUT", TRANSFER, 1, 4), + ("ERC1155_SINGLE_IN", TRANSFER_SINGLE, 3, None), + ("ERC1155_SINGLE_OUT", TRANSFER_SINGLE, 2, None), + ("ERC1155_BATCH_IN", TRANSFER_BATCH, 3, None), + ("ERC1155_BATCH_OUT", TRANSFER_BATCH, 2, None), + ] + + summaries: list[dict[str, Any]] = [] + logs_out: list[dict[str, Any]] = [] + coverage_out: list[dict[str, Any]] = [] + known_tx_out: list[dict[str, Any]] = [] + failures: list[dict[str, Any]] = [] + + for wallet_row in selected: + wallet = wallet_row["wallet"].lower() + wallet_logs: dict[tuple[str, str, int], dict[str, Any]] = {} + wallet_cov: list[dict[str, Any]] = [] + query_counts: dict[str, int] = {} + for query_name, topic0, topic_index, required_topic_count in event_queries: + topics: list[Any] = [topic0] + while len(topics) <= topic_index: + topics.append(None) + topics[topic_index] = padded(wallet) + rows, coverage = fetch_logs_recursive( + rpc, + from_block=0, + to_block=args.fixed_head, + topics=topics, + ) + if required_topic_count is not None: + rows = [row for row in rows if len(row.get("topics") or []) == required_topic_count] + query_counts[query_name] = len(rows) + for coverage_row in coverage: + record = {"wallet": wallet, "query": query_name, **coverage_row} + coverage_out.append(record) + wallet_cov.append(record) + for log in rows: + key = log_key(log) + record = { + "wallet": wallet, + "priority": wallet_row.get("priority"), + "query": query_name, + "block_number": integer(log.get("blockNumber")), + "block_hash": str(log.get("blockHash") or "").lower(), + "transaction_hash": str(log.get("transactionHash") or "").lower(), + "transaction_index": integer(log.get("transactionIndex")), + "log_index": integer(log.get("logIndex")), + "contract": str(log.get("address") or "").lower(), + "topics_json": json.dumps(log.get("topics") or [], separators=(",", ":")), + "data": str(log.get("data") or "0x").lower(), + "removed": bool(log.get("removed", False)), + } + wallet_logs[key] = record + + for kind in ("known_mints", "known_sales"): + for evidence in wallet_row.get(kind) or []: + tx_hash = str(evidence.get("tx_hash") or "").lower() + if not tx_hash: + continue + try: + tx = rpc.call("eth_getTransactionByHash", [tx_hash]) + receipt = rpc.call("eth_getTransactionReceipt", [tx_hash]) + known_tx_out.append({ + "wallet": wallet, + "priority": wallet_row.get("priority"), + "evidence_kind": kind, + "transaction_hash": tx_hash, + "transaction_present": isinstance(tx, dict), + "receipt_present": isinstance(receipt, dict), + "receipt_status": integer(receipt.get("status")) if isinstance(receipt, dict) and receipt.get("status") is not None else None, + "block_number": integer(receipt.get("blockNumber")) if isinstance(receipt, dict) and receipt.get("blockNumber") is not None else None, + "block_hash": str(receipt.get("blockHash") or "").lower() if isinstance(receipt, dict) else None, + "tx_from": str(tx.get("from") or "").lower() if isinstance(tx, dict) else None, + "tx_to": str(tx.get("to") or "").lower() if isinstance(tx, dict) else None, + "tx_value_wei": integer(tx.get("value")) if isinstance(tx, dict) and tx.get("value") is not None else None, + "receipt_log_count": len(receipt.get("logs") or []) if isinstance(receipt, dict) else 0, + "source_evidence_json": json.dumps(evidence, sort_keys=True, separators=(",", ":")), + "tx_json": json.dumps(tx, sort_keys=True, separators=(",", ":")) if isinstance(tx, dict) else None, + "receipt_json": json.dumps(receipt, sort_keys=True, separators=(",", ":")) if isinstance(receipt, dict) else None, + }) + except Exception as exc: + failures.append({"wallet": wallet, "stage": "known_tx", "transaction_hash": tx_hash, "error": repr(exc)}) + + wallet_rows = sorted(wallet_logs.values(), key=lambda row: (row["block_number"], row["transaction_index"], row["log_index"])) + logs_out.extend(wallet_rows) + coverage_failures = [row for row in wallet_cov if row.get("status") != "PASS"] + removed_rows = [row for row in wallet_rows if row["removed"]] + expected_activity = wallet not in EXTERNAL_ONLY + if coverage_failures: + failures.append({"wallet": wallet, "stage": "coverage", "failure_count": len(coverage_failures), "sample": coverage_failures[:3]}) + if removed_rows: + failures.append({"wallet": wallet, "stage": "removed_logs", "count": len(removed_rows)}) + if expected_activity and not wallet_rows: + failures.append({"wallet": wallet, "stage": "expected_activity_missing", "priority": wallet_row.get("priority")}) + summaries.append({ + "wallet": wallet, + "priority": wallet_row.get("priority"), + "expected_onchain_activity": expected_activity, + "canonical_nft_transfer_rows": len(wallet_rows), + "unique_contracts": len({row["contract"] for row in wallet_rows}), + "first_block": min((row["block_number"] for row in wallet_rows), default=None), + "last_block": max((row["block_number"] for row in wallet_rows), default=None), + "query_counts_json": json.dumps(query_counts, sort_keys=True, separators=(",", ":")), + "coverage_failures": len(coverage_failures), + "status": "PASS" if not coverage_failures and not removed_rows and (wallet_rows or not expected_activity) else "FAIL", + }) + print(summaries[-1], flush=True) + + def write_csv(path: Path, rows: list[dict[str, Any]]) -> None: + fields: list[str] = [] + seen: set[str] = set() + for row in rows: + for key in row: + if key not in seen: + seen.add(key) + fields.append(key) + if not fields: + fields = ["empty"] + with path.open("w", newline="", encoding="utf-8-sig") as handle: + writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore") + writer.writeheader() + writer.writerows(rows) + + write_csv(out / "wallet_summary.csv", summaries) + write_csv(out / "canonical_nft_transfers.csv", logs_out) + write_csv(out / "coverage.csv", coverage_out) + write_csv(out / "known_transaction_evidence.csv", known_tx_out) + write_csv(out / "errors.csv", failures) + + validation = { + "status": "PASS" if not failures else "FAIL", + "chain_id": 4663, + "fixed_head_block": args.fixed_head, + "shard": args.shard, + "shard_count": args.shard_count, + "wallet_rows": len(selected), + "wallets_passed": sum(row["status"] == "PASS" for row in summaries), + "wallets_failed": sum(row["status"] != "PASS" for row in summaries), + "canonical_nft_transfer_rows": len(logs_out), + "known_transaction_evidence_rows": len(known_tx_out), + "failure_rows": len(failures), + "failures": failures, + "rpc_stats": rpc.stats, + "production_approved_wallets": 0, + } + (out / "VALIDATION.json").write_text(json.dumps(validation, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + manifest = [] + for path in sorted(out.iterdir()): + if path.is_file(): + manifest.append({"path": path.name, "bytes": path.stat().st_size, "sha256": sha256(path)}) + (out / "MANIFEST.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + print(json.dumps(validation, sort_keys=True), flush=True) + if validation["status"] != "PASS": + raise SystemExit(1) + + +if __name__ == "__main__": + main() From 9d359ed1deafb651c35cd88b7476d021bbb6689e Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:38:26 +0900 Subject: [PATCH 16/42] ci: collect global and wallet-level canonical Robinhood NFT evidence --- .../rhc-canonical-data-completion.yml | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 .github/workflows/rhc-canonical-data-completion.yml diff --git a/.github/workflows/rhc-canonical-data-completion.yml b/.github/workflows/rhc-canonical-data-completion.yml new file mode 100644 index 0000000..0579488 --- /dev/null +++ b/.github/workflows/rhc-canonical-data-completion.yml @@ -0,0 +1,162 @@ +name: RHC canonical data completion + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-canonical-data-completion.yml' + - 'rhc-data-completion/**' + - 'wallet-verification/p2_addresses.txt' + +permissions: + contents: read + +concurrency: + group: rhc-canonical-data-completion-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + head: + runs-on: ubuntu-latest + outputs: + fixed_head: ${{ steps.capture.outputs.fixed_head }} + steps: + - id: capture + shell: bash + run: | + set -euo pipefail + python - <<'PY' >> "$GITHUB_OUTPUT" + import json, urllib.request + payload=json.dumps({'jsonrpc':'2.0','id':1,'method':'eth_blockNumber','params':[]}).encode() + req=urllib.request.Request('https://rpc.mainnet.chain.robinhood.com',data=payload,headers={'content-type':'application/json','user-agent':'RHC-Head-Capture/1.0'}) + with urllib.request.urlopen(req,timeout=60) as response: + data=json.loads(response.read()) + latest=int(data['result'],16) + fixed=max(0,latest-100) + print(f'fixed_head={fixed}') + PY + + global-events: + needs: head + strategy: + fail-fast: false + max-parallel: 1 + matrix: + target: [seadrop, seaport] + runs-on: ubuntu-latest + timeout-minutes: 180 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Collect complete canonical event history + shell: bash + env: + TARGET: ${{ matrix.target }} + FIXED_HEAD: ${{ needs.head.outputs.fixed_head }} + run: | + set -euo pipefail + mkdir -p "out-global-${TARGET}" + # The collector captures its own head. Verify it is not behind the workflow fixed head, + # then retain the collector's finalized head in its validation record. + PYTHONPATH=rhc-data-completion python rhc-data-completion/collect_global_events.py \ + --target "$TARGET" \ + --out "out-global-${TARGET}" \ + --chunk 2000000 \ + --confirmations 100 + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-global-${{ matrix.target }} + path: out-global-${{ matrix.target }}/ + retention-days: 7 + if-no-files-found: error + compression-level: 9 + + wallets: + needs: [head, global-events] + strategy: + fail-fast: false + max-parallel: 2 + matrix: + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + runs-on: ubuntu-latest + timeout-minutes: 240 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Build fixed 232-wallet input + shell: bash + run: | + set -euo pipefail + mkdir -p runtime + curl --fail --location --silent --show-error \ + 'https://raw.githubusercontent.com/dcftradingcrypt/chart/chatgpt/rhc-wallet-verification-p0p1-20260828/wallet-verification/wallets.json.gz.b64' \ + -o runtime/p1-wallets.json.gz.b64 + base64 -d runtime/p1-wallets.json.gz.b64 | gzip -d > runtime/p1-wallets.json + python - <<'PY' + import json,re + from pathlib import Path + p1=json.loads(Path('runtime/p1-wallets.json').read_text()) + p2=[line.strip().lower() for line in Path('wallet-verification/p2_addresses.txt').read_text().splitlines() if line.strip()] + by_wallet={} + for row in p1: + wallet=row['wallet'].lower() + row=dict(row); row['wallet']=wallet + by_wallet[wallet]=row + for wallet in p2: + by_wallet.setdefault(wallet,{ + 'wallet':wallet, + 'priority':'P2', + 'priority_points':5, + 'verification_reasons':['FIXED_P2_VERIFICATION_QUEUE'], + 'known_mints':[], + 'known_sales':[], + }) + priority_order={'P0':0,'P1':1,'P2':2} + rows=sorted(by_wallet.values(),key=lambda r:(priority_order.get(r.get('priority'),9),r['wallet'])) + assert len(p1)==20, len(p1) + assert len(p2)==212, len(p2) + assert len(rows)==232, len(rows) + assert all(re.fullmatch(r'0x[a-f0-9]{40}',r['wallet']) for r in rows) + Path('runtime/all-wallets.json').write_text(json.dumps(rows,indent=2),encoding='utf-8') + Path('runtime/input-summary.json').write_text(json.dumps({ + 'wallets':len(rows), + 'p0':sum(r.get('priority')=='P0' for r in rows), + 'p1':sum(r.get('priority')=='P1' for r in rows), + 'p2':sum(r.get('priority')=='P2' for r in rows), + },indent=2),encoding='utf-8') + PY + - name: Collect canonical wallet NFT transfer histories + shell: bash + env: + SHARD: ${{ matrix.shard }} + FIXED_HEAD: ${{ needs.head.outputs.fixed_head }} + run: | + set -euo pipefail + PYTHONPATH=rhc-data-completion python rhc-data-completion/collect_wallet_transfers.py \ + --wallets runtime/all-wallets.json \ + --out "out-wallet-${SHARD}" \ + --shard "$SHARD" \ + --shard-count 16 \ + --fixed-head "$FIXED_HEAD" + - name: Preserve fixed inputs + if: always() + shell: bash + env: + SHARD: ${{ matrix.shard }} + run: | + mkdir -p "out-wallet-${SHARD}/runtime" + cp runtime/all-wallets.json runtime/input-summary.json "out-wallet-${SHARD}/runtime/" || true + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-canonical-wallet-${{ matrix.shard }} + path: out-wallet-${{ matrix.shard }}/ + retention-days: 7 + if-no-files-found: error + compression-level: 9 From 8eccb992f078543eaaeafa2f2078a96454fc4702 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:40:20 +0900 Subject: [PATCH 17/42] feat: consolidate canonical data completion artifacts and remediation queue --- rhc-data-completion/consolidate.py | 211 +++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 rhc-data-completion/consolidate.py diff --git a/rhc-data-completion/consolidate.py b/rhc-data-completion/consolidate.py new file mode 100644 index 0000000..963e5ed --- /dev/null +++ b/rhc-data-completion/consolidate.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import csv +import hashlib +import json +import shutil +from collections import Counter, defaultdict +from pathlib import Path +from typing import Any + +ROOT = Path("downloaded") +OUT = Path("out-consolidated") +OUT.mkdir(parents=True, exist_ok=True) + + +def read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def read_csv(path: Path) -> list[dict[str, str]]: + with path.open(newline="", encoding="utf-8-sig") as handle: + return list(csv.DictReader(handle)) + + +def write_csv(path: Path, rows: list[dict[str, Any]]) -> None: + fields: list[str] = [] + seen: set[str] = set() + for row in rows: + for key in row: + if key not in seen: + seen.add(key) + fields.append(key) + if not fields: + fields = ["empty"] + with path.open("w", newline="", encoding="utf-8-sig") as handle: + writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore") + writer.writeheader() + for row in rows: + writer.writerow({ + key: json.dumps(value, ensure_ascii=False, sort_keys=True) + if isinstance(value, (dict, list, tuple)) else value + for key, value in row.items() + }) + + +def integer(value: Any) -> int: + try: + return int(float(value or 0)) + except Exception: + return 0 + + +def sha256(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def locate(artifact: str, basename: str) -> Path | None: + base = ROOT / artifact + matches = list(base.rglob(basename)) if base.exists() else [] + return matches[0] if matches else None + + +def main() -> None: + expected_artifacts = ["rhc-global-seadrop", "rhc-global-seaport"] + [f"rhc-canonical-wallet-{i}" for i in range(16)] + missing_artifacts = [name for name in expected_artifacts if not (ROOT / name).exists()] + + global_validations: dict[str, Any] = {} + global_events: list[dict[str, Any]] = [] + for target in ("seadrop", "seaport"): + artifact = f"rhc-global-{target}" + validation_path = locate(artifact, "VALIDATION.json") + events_path = locate(artifact, f"{target}_events.csv") + if validation_path: + global_validations[target] = read_json(validation_path) + if events_path: + for row in read_csv(events_path): + row["_artifact"] = artifact + global_events.append(row) + + wallet_summaries: list[dict[str, Any]] = [] + wallet_logs: list[dict[str, Any]] = [] + wallet_coverage: list[dict[str, Any]] = [] + known_transactions: list[dict[str, Any]] = [] + collector_errors: list[dict[str, Any]] = [] + shard_validations: list[dict[str, Any]] = [] + for shard in range(16): + artifact = f"rhc-canonical-wallet-{shard}" + for basename, target in ( + ("wallet_summary.csv", wallet_summaries), + ("canonical_nft_transfers.csv", wallet_logs), + ("coverage.csv", wallet_coverage), + ("known_transaction_evidence.csv", known_transactions), + ("errors.csv", collector_errors), + ): + path = locate(artifact, basename) + if path: + for row in read_csv(path): + row["_artifact"] = artifact + target.append(row) + validation_path = locate(artifact, "VALIDATION.json") + if validation_path: + value = read_json(validation_path) + value["_artifact"] = artifact + shard_validations.append(value) + + # De-duplicate canonical logs. A wallet can appear on both incoming and outgoing paths only in self-transfers. + dedup_logs: dict[tuple[str, str, str, str], dict[str, Any]] = {} + for row in wallet_logs: + key = ( + row.get("wallet", "").lower(), + row.get("block_hash", "").lower(), + row.get("transaction_hash", "").lower(), + str(row.get("log_index", "")), + ) + dedup_logs[key] = row + wallet_logs = sorted(dedup_logs.values(), key=lambda row: (row.get("wallet", ""), integer(row.get("block_number")), integer(row.get("log_index")))) + + # Wallet-level completeness classifications. + remediation: list[dict[str, Any]] = [] + unique_wallets: dict[str, dict[str, Any]] = {} + for row in wallet_summaries: + wallet = row.get("wallet", "").lower() + if wallet: + if wallet in unique_wallets: + remediation.append({"wallet": wallet, "priority": row.get("priority"), "reason": "DUPLICATE_WALLET_SUMMARY", "action": "DEDUP_AND_AUDIT"}) + unique_wallets[wallet] = row + expected = str(row.get("expected_onchain_activity", "")).lower() in {"true", "1", "yes"} + transfer_rows = integer(row.get("canonical_nft_transfer_rows")) + coverage_failures = integer(row.get("coverage_failures")) + if coverage_failures: + remediation.append({"wallet": wallet, "priority": row.get("priority"), "reason": "RPC_COVERAGE_FAILURE", "action": "RETRY_FAILED_BLOCK_RANGES_ONLY"}) + elif expected and transfer_rows == 0: + remediation.append({"wallet": wallet, "priority": row.get("priority"), "reason": "EXPECTED_ACTIVITY_NOT_FOUND_BY_CANONICAL_RPC", "action": "VERIFY_SOURCE_OBSERVATION_TX_OR_RECLASSIFY_SOURCE_ONLY"}) + + for row in collector_errors: + if row.get("empty"): + continue + remediation.append({ + "wallet": row.get("wallet"), + "priority": None, + "reason": row.get("stage") or "COLLECTOR_ERROR", + "action": "RETRY_OR_MANUAL_CANONICAL_RECONCILIATION", + "detail": row.get("error") or row, + }) + + # Known transaction evidence must be present and successful. + for row in known_transactions: + if str(row.get("transaction_present", "")).lower() not in {"true", "1"} or str(row.get("receipt_present", "")).lower() not in {"true", "1"} or integer(row.get("receipt_status")) != 1: + remediation.append({ + "wallet": row.get("wallet"), + "priority": row.get("priority"), + "reason": "KNOWN_TRANSACTION_NOT_CANONICAL_SUCCESS", + "action": "REQUERY_EXACT_TX_AND_RECEIPT", + "detail": row.get("transaction_hash"), + }) + + priority_counts = Counter((row.get("priority") or "UNKNOWN") for row in unique_wallets.values()) + status_counts = Counter((row.get("status") or "UNKNOWN") for row in unique_wallets.values()) + global_ok = all(global_validations.get(target, {}).get("status") == "PASS" for target in ("seadrop", "seaport")) + fixed_heads = {target: global_validations.get(target, {}).get("fixed_head_block") for target in ("seadrop", "seaport")} + expected_wallet_count = 232 + wallet_set_ok = len(unique_wallets) == expected_wallet_count and priority_counts.get("P0", 0) == 1 and priority_counts.get("P1", 0) == 19 and priority_counts.get("P2", 0) == 212 + complete = not missing_artifacts and global_ok and wallet_set_ok and not remediation + + write_csv(OUT / "global_events.csv", global_events) + write_csv(OUT / "wallet_summary.csv", list(unique_wallets.values())) + write_csv(OUT / "canonical_nft_transfers.csv", wallet_logs) + write_csv(OUT / "wallet_coverage.csv", wallet_coverage) + write_csv(OUT / "known_transaction_evidence.csv", known_transactions) + write_csv(OUT / "collector_errors.csv", collector_errors) + write_csv(OUT / "REMEDIATION_QUEUE.csv", remediation) + (OUT / "shard_validations.json").write_text(json.dumps(shard_validations, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + + report = { + "status": "PASS" if complete else "PARTIAL_REMEDIATION_REQUIRED", + "expected_artifacts": len(expected_artifacts), + "missing_artifacts": missing_artifacts, + "global_event_validations": global_validations, + "global_event_rows": len(global_events), + "fixed_heads": fixed_heads, + "wallets_expected": expected_wallet_count, + "wallets_observed": len(unique_wallets), + "priority_counts": dict(priority_counts), + "wallet_status_counts": dict(status_counts), + "canonical_nft_transfer_rows": len(wallet_logs), + "known_transaction_evidence_rows": len(known_transactions), + "collector_error_rows": len([row for row in collector_errors if not row.get("empty")]), + "remediation_rows": len(remediation), + "wallet_set_ok": wallet_set_ok, + "global_history_complete": global_ok, + "data_collection_complete_for_wallet_verification": complete, + "deepseek_handoff_allowed": False, + "production_approved_wallets": 0, + } + (OUT / "DATA_COMPLETENESS.json").write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + + manifest = [] + for path in sorted(OUT.iterdir()): + if path.is_file(): + manifest.append({"path": path.name, "bytes": path.stat().st_size, "sha256": sha256(path)}) + (OUT / "MANIFEST.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + print(json.dumps(report, sort_keys=True), flush=True) + + +if __name__ == "__main__": + main() From b347a277491de199a622ce196904cd7bbc294d9b Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:40:56 +0900 Subject: [PATCH 18/42] ci: consolidate canonical collection and produce exact remediation queue --- .../workflows/rhc-canonical-consolidator.yml | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .github/workflows/rhc-canonical-consolidator.yml diff --git a/.github/workflows/rhc-canonical-consolidator.yml b/.github/workflows/rhc-canonical-consolidator.yml new file mode 100644 index 0000000..b670a00 --- /dev/null +++ b/.github/workflows/rhc-canonical-consolidator.yml @@ -0,0 +1,116 @@ +name: RHC canonical data consolidator + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-canonical-consolidator.yml' + - '.github/workflows/rhc-canonical-data-completion.yml' + - 'rhc-data-completion/**' + - 'wallet-verification/p2_addresses.txt' + +permissions: + contents: read + actions: read + +concurrency: + group: rhc-canonical-data-consolidator-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + consolidate: + runs-on: ubuntu-latest + timeout-minutes: 300 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Wait for matching canonical collection run and download all artifacts + shell: bash + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + set -euo pipefail + python - <<'PY' + import json, os, pathlib, time, urllib.parse, urllib.request, zipfile, io + + token=os.environ['GH_TOKEN'] + repo=os.environ['REPO'] + head_sha=os.environ['HEAD_SHA'] + branch=os.environ['HEAD_BRANCH'] + headers={ + 'accept':'application/vnd.github+json', + 'authorization':f'Bearer {token}', + 'user-agent':'RHC-Canonical-Consolidator/1.0', + 'x-github-api-version':'2022-11-28', + } + + def get_json(url): + req=urllib.request.Request(url,headers=headers) + with urllib.request.urlopen(req,timeout=90) as response: + return json.loads(response.read()) + + run=None + for attempt in range(240): + url=(f'https://api.github.com/repos/{repo}/actions/workflows/' + f'rhc-canonical-data-completion.yml/runs?event=pull_request&branch=' + f'{urllib.parse.quote(branch)}&per_page=30') + data=get_json(url) + matches=[r for r in data.get('workflow_runs',[]) if r.get('head_sha')==head_sha] + if matches: + run=sorted(matches,key=lambda r:r['id'],reverse=True)[0] + print({'run_id':run['id'],'status':run['status'],'conclusion':run.get('conclusion')},flush=True) + if run['status']=='completed': + break + time.sleep(30) + if not run or run.get('status')!='completed': + raise SystemExit('matching canonical collection run did not complete') + + run_id=run['id'] + artifacts=get_json(f'https://api.github.com/repos/{repo}/actions/runs/{run_id}/artifacts?per_page=100').get('artifacts',[]) + required={'rhc-global-seadrop','rhc-global-seaport'} | {f'rhc-canonical-wallet-{i}' for i in range(16)} + names={a['name'] for a in artifacts} + missing=sorted(required-names) + pathlib.Path('downloaded').mkdir(exist_ok=True) + pathlib.Path('out-consolidated').mkdir(exist_ok=True) + (pathlib.Path('out-consolidated')/'SOURCE_RUN.json').write_text(json.dumps({ + 'run_id':run_id, + 'head_sha':head_sha, + 'head_branch':branch, + 'run_conclusion':run.get('conclusion'), + 'artifact_count':len(artifacts), + 'required_artifacts':sorted(required), + 'missing_artifacts':missing, + },indent=2,sort_keys=True)) + for artifact in artifacts: + name=artifact['name'] + if name not in required: + continue + req=urllib.request.Request(artifact['archive_download_url'],headers=headers) + with urllib.request.urlopen(req,timeout=300) as response: + payload=response.read() + dest=pathlib.Path('downloaded')/name + dest.mkdir(parents=True,exist_ok=True) + with zipfile.ZipFile(io.BytesIO(payload)) as archive: + archive.extractall(dest) + print({'artifact':name,'bytes':len(payload),'files':len(list(dest.rglob('*')))},flush=True) + if missing: + print({'warning':'missing required artifacts','missing':missing},flush=True) + PY + - name: Consolidate and generate exact remediation queue + if: always() + shell: bash + run: python rhc-data-completion/consolidate.py + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-canonical-data-consolidated + path: out-consolidated/ + retention-days: 10 + if-no-files-found: error + compression-level: 9 From 0dd2a9e282ced56eaebf639b6242ec87a1594065 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:42:04 +0900 Subject: [PATCH 19/42] ci: publish canonical collection status and consolidated artifact pointer --- .../rhc-canonical-result-publisher.yml | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .github/workflows/rhc-canonical-result-publisher.yml diff --git a/.github/workflows/rhc-canonical-result-publisher.yml b/.github/workflows/rhc-canonical-result-publisher.yml new file mode 100644 index 0000000..e83c3ba --- /dev/null +++ b/.github/workflows/rhc-canonical-result-publisher.yml @@ -0,0 +1,116 @@ +name: RHC canonical data result publisher + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-canonical-result-publisher.yml' + - 'rhc-data-completion/**' + +permissions: + contents: write + actions: read + +concurrency: + group: rhc-canonical-result-publisher-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 300 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Locate latest canonical run for branch, wait, and download artifacts + shell: bash + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + set -euo pipefail + python - <<'PY' + import json, os, pathlib, time, urllib.parse, urllib.request, zipfile, io + token=os.environ['GH_TOKEN']; repo=os.environ['REPO']; branch=os.environ['HEAD_BRANCH'] + headers={'accept':'application/vnd.github+json','authorization':f'Bearer {token}','user-agent':'RHC-Result-Publisher/1.0','x-github-api-version':'2022-11-28'} + def get_json(url): + req=urllib.request.Request(url,headers=headers) + with urllib.request.urlopen(req,timeout=90) as response: + return json.loads(response.read()) + run=None + for _ in range(300): + url=(f'https://api.github.com/repos/{repo}/actions/workflows/rhc-canonical-data-completion.yml/runs?' + f'event=pull_request&branch={urllib.parse.quote(branch)}&per_page=30') + runs=get_json(url).get('workflow_runs',[]) + if runs: + run=sorted(runs,key=lambda value:value['id'],reverse=True)[0] + print({'run_id':run['id'],'status':run['status'],'conclusion':run.get('conclusion'),'head_sha':run.get('head_sha')},flush=True) + if run['status']=='completed': break + time.sleep(30) + if not run or run.get('status')!='completed': + raise SystemExit('canonical run did not complete') + artifacts=get_json(f'https://api.github.com/repos/{repo}/actions/runs/{run["id"]}/artifacts?per_page=100').get('artifacts',[]) + required={'rhc-global-seadrop','rhc-global-seaport'} | {f'rhc-canonical-wallet-{i}' for i in range(16)} + pathlib.Path('downloaded').mkdir(exist_ok=True) + for artifact in artifacts: + if artifact['name'] not in required: continue + req=urllib.request.Request(artifact['archive_download_url'],headers=headers) + with urllib.request.urlopen(req,timeout=300) as response: payload=response.read() + dest=pathlib.Path('downloaded')/artifact['name']; dest.mkdir(parents=True,exist_ok=True) + with zipfile.ZipFile(io.BytesIO(payload)) as archive: archive.extractall(dest) + pathlib.Path('out-consolidated').mkdir(exist_ok=True) + (pathlib.Path('out-consolidated')/'SOURCE_RUN.json').write_text(json.dumps({ + 'canonical_run_id':run['id'], + 'canonical_run_head_sha':run.get('head_sha'), + 'canonical_run_conclusion':run.get('conclusion'), + 'artifact_names':sorted(a['name'] for a in artifacts), + 'required_artifacts':sorted(required), + 'missing_required_artifacts':sorted(required-{a['name'] for a in artifacts}), + 'publisher_run_id':int(os.environ.get('GITHUB_RUN_ID','0')), + },indent=2,sort_keys=True)) + PY + - name: Consolidate + if: always() + run: python rhc-data-completion/consolidate.py + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-canonical-data-consolidated + path: out-consolidated/ + retention-days: 10 + if-no-files-found: error + compression-level: 9 + - name: Publish machine-readable result pointer on temporary branch + if: always() + shell: bash + env: + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + set -euo pipefail + mkdir -p .results + cp out-consolidated/DATA_COMPLETENESS.json .results/rhc-canonical-data-completeness.json || true + cp out-consolidated/SOURCE_RUN.json .results/rhc-canonical-source-run.json || true + cp out-consolidated/REMEDIATION_QUEUE.csv .results/rhc-canonical-remediation-queue.csv || true + python - <<'PY' + import json, os, pathlib + path=pathlib.Path('.results/rhc-canonical-publisher.json') + path.write_text(json.dumps({ + 'publisher_run_id':int(os.environ['GITHUB_RUN_ID']), + 'publisher_run_attempt':int(os.environ['GITHUB_RUN_ATTEMPT']), + 'head_sha':os.environ['GITHUB_SHA'], + },indent=2,sort_keys=True)) + PY + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add .results/ + if git diff --cached --quiet; then + exit 0 + fi + git commit -m 'chore: publish canonical data collection result [skip ci]' + git push origin "HEAD:${HEAD_BRANCH}" From 036746e24c5a9473a9a48c68400b82372549bd6d Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:43:42 +0900 Subject: [PATCH 20/42] ci: publish consolidated canonical evidence package to temporary branch --- .github/workflows/rhc-canonical-packager.yml | 113 +++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/workflows/rhc-canonical-packager.yml diff --git a/.github/workflows/rhc-canonical-packager.yml b/.github/workflows/rhc-canonical-packager.yml new file mode 100644 index 0000000..75bf5ce --- /dev/null +++ b/.github/workflows/rhc-canonical-packager.yml @@ -0,0 +1,113 @@ +name: RHC canonical data package publisher + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-canonical-packager.yml' + +permissions: + contents: write + actions: read + +concurrency: + group: rhc-canonical-data-packager-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + package: + runs-on: ubuntu-latest + timeout-minutes: 300 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Wait for canonical collection and download its artifacts + shell: bash + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + set -euo pipefail + python - <<'PY' + import json, os, pathlib, time, urllib.parse, urllib.request, zipfile, io + token=os.environ['GH_TOKEN']; repo=os.environ['REPO']; branch=os.environ['HEAD_BRANCH'] + headers={'accept':'application/vnd.github+json','authorization':f'Bearer {token}','user-agent':'RHC-Package-Publisher/1.0','x-github-api-version':'2022-11-28'} + def get_json(url): + req=urllib.request.Request(url,headers=headers) + with urllib.request.urlopen(req,timeout=90) as response: return json.loads(response.read()) + run=None + for _ in range(300): + url=(f'https://api.github.com/repos/{repo}/actions/workflows/rhc-canonical-data-completion.yml/runs?' + f'event=pull_request&branch={urllib.parse.quote(branch)}&per_page=30') + values=get_json(url).get('workflow_runs',[]) + if values: + run=sorted(values,key=lambda value:value['id'],reverse=True)[0] + print({'run_id':run['id'],'status':run['status'],'conclusion':run.get('conclusion')},flush=True) + if run['status']=='completed': break + time.sleep(30) + if not run or run.get('status')!='completed': raise SystemExit('canonical run did not complete') + required={'rhc-global-seadrop','rhc-global-seaport'} | {f'rhc-canonical-wallet-{i}' for i in range(16)} + artifacts=get_json(f'https://api.github.com/repos/{repo}/actions/runs/{run["id"]}/artifacts?per_page=100').get('artifacts',[]) + pathlib.Path('downloaded').mkdir(exist_ok=True) + for artifact in artifacts: + if artifact['name'] not in required: continue + req=urllib.request.Request(artifact['archive_download_url'],headers=headers) + with urllib.request.urlopen(req,timeout=300) as response: payload=response.read() + dest=pathlib.Path('downloaded')/artifact['name']; dest.mkdir(parents=True,exist_ok=True) + with zipfile.ZipFile(io.BytesIO(payload)) as archive: archive.extractall(dest) + pathlib.Path('out-consolidated').mkdir(exist_ok=True) + (pathlib.Path('out-consolidated')/'SOURCE_RUN.json').write_text(json.dumps({ + 'canonical_run_id':run['id'], + 'canonical_run_head_sha':run.get('head_sha'), + 'canonical_run_conclusion':run.get('conclusion'), + 'artifact_names':sorted(a['name'] for a in artifacts), + 'missing_required_artifacts':sorted(required-{a['name'] for a in artifacts}), + 'packager_run_id':int(os.environ.get('GITHUB_RUN_ID','0')), + },indent=2,sort_keys=True)) + PY + - name: Consolidate + if: always() + run: python rhc-data-completion/consolidate.py + - name: Build versioned package and publish to temporary branch + if: always() + shell: bash + env: + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + set -euo pipefail + rm -rf .results/rhc-canonical-package + mkdir -p .results/rhc-canonical-package + cp -a out-consolidated/. .results/rhc-canonical-package/ + python - <<'PY' + import hashlib, json, pathlib, zipfile, os + source=pathlib.Path('.results/rhc-canonical-package') + archive=pathlib.Path('.results/rhc-canonical-data-consolidated.zip') + if archive.exists(): archive.unlink() + with zipfile.ZipFile(archive,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=9) as z: + for path in sorted(source.rglob('*')): + if path.is_file(): z.write(path,path.relative_to(source.parent)) + digest=hashlib.sha256(archive.read_bytes()).hexdigest() + index={ + 'package':'rhc-canonical-data-consolidated.zip', + 'bytes':archive.stat().st_size, + 'sha256':digest, + 'packager_run_id':int(os.environ['GITHUB_RUN_ID']), + 'files':[str(path.relative_to(source)) for path in sorted(source.rglob('*')) if path.is_file()], + } + pathlib.Path('.results/rhc-canonical-package-index.json').write_text(json.dumps(index,indent=2,sort_keys=True)) + pathlib.Path('.results/rhc-canonical-data-consolidated.zip.sha256').write_text(digest+' '+archive.name+'\n') + if archive.stat().st_size > 90_000_000: + raise SystemExit(f'package exceeds GitHub 100 MB file limit: {archive.stat().st_size}') + PY + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add .results/ + if git diff --cached --quiet; then exit 0; fi + git commit -m 'chore: publish canonical data package [skip ci]' + git push origin "HEAD:${HEAD_BRANCH}" From 7af851edbb8765471295025efe95443131712c8b Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:51:40 +0900 Subject: [PATCH 21/42] feat: derive complete SeaDrop opportunity and Seaport outcome universe --- .../derive_project_universe.py | 483 ++++++++++++++++++ 1 file changed, 483 insertions(+) create mode 100644 rhc-data-completion/derive_project_universe.py diff --git a/rhc-data-completion/derive_project_universe.py b/rhc-data-completion/derive_project_universe.py new file mode 100644 index 0000000..9235f1b --- /dev/null +++ b/rhc-data-completion/derive_project_universe.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import math +import statistics +import time +from collections import Counter, defaultdict +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from canonical_rpc import CanonicalRpc + +TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" +ZERO = "0x0000000000000000000000000000000000000000" +WETH_RHC = "0x0bd7d308f8e1639fab988df18a8011f41eacad73" +NATIVE = ZERO +PAYMENT_TYPES = {0, 1} +NFT_TYPES = {2, 3, 4, 5} +WINDOWS = {"15m": 15 * 60, "30m": 30 * 60, "2h": 2 * 3600, "24h": 24 * 3600} + + +def integer(value: Any, default: int = 0) -> int: + try: + text = str(value or "0") + return int(text, 16) if text.startswith("0x") else int(float(text)) + except Exception: + return default + + +def address_word(word: str) -> str: + return "0x" + word[-40:].lower() + + +def topic_address(topic: str) -> str: + return "0x" + topic[-40:].lower() + + +def data_words(data: str) -> list[str]: + raw = data[2:] if data.startswith("0x") else data + if len(raw) % 64: + raise ValueError("ABI data word alignment failure") + return [raw[index:index + 64] for index in range(0, len(raw), 64)] + + +def decode_array(words: list[str], offset_bytes: int, tuple_words: int) -> list[list[str]]: + index = offset_bytes // 32 + if index < 0 or index >= len(words): + raise ValueError("array offset outside event data") + length = int(words[index], 16) + start = index + 1 + end = start + length * tuple_words + if end > len(words): + raise ValueError("array payload truncated") + return [words[start + item * tuple_words:start + (item + 1) * tuple_words] for item in range(length)] + + +def decode_order_fulfilled(row: dict[str, str]) -> dict[str, Any]: + topics = json.loads(row["topics_json"]) + words = data_words(row["data"]) + if len(topics) != 4 or len(words) < 3: + raise ValueError("unexpected OrderFulfilled encoding") + recipient = address_word(words[0]) + offer_tuples = decode_array(words, int(words[1], 16), 4) + consideration_tuples = decode_array(words, int(words[2], 16), 5) + offer = [ + {"item_type": int(item[0], 16), "token": address_word(item[1]), "identifier": int(item[2], 16), "amount": int(item[3], 16)} + for item in offer_tuples + ] + consideration = [ + {"item_type": int(item[0], 16), "token": address_word(item[1]), "identifier": int(item[2], 16), "amount": int(item[3], 16), "recipient": address_word(item[4])} + for item in consideration_tuples + ] + return { + "order_hash": topics[1].lower(), + "offerer": topic_address(topics[2]), + "zone": topic_address(topics[3]), + "recipient": recipient, + "offer": offer, + "consideration": consideration, + } + + +def write_csv(path: Path, rows: list[dict[str, Any]]) -> None: + fields: list[str] = [] + seen: set[str] = set() + for row in rows: + for key in row: + if key not in seen: + seen.add(key) + fields.append(key) + if not fields: + fields = ["empty"] + with path.open("w", newline="", encoding="utf-8-sig") as handle: + writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore") + writer.writeheader() + for row in rows: + writer.writerow({ + key: json.dumps(value, ensure_ascii=False, sort_keys=True) + if isinstance(value, (dict, list, tuple)) else value + for key, value in row.items() + }) + + +def read_csv(path: Path) -> list[dict[str, str]]: + with path.open(newline="", encoding="utf-8-sig") as handle: + return list(csv.DictReader(handle)) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def batch_rpc(rpc: CanonicalRpc, method: str, values: list[str], batch_size: int = 20) -> tuple[dict[str, Any], list[dict[str, Any]]]: + results: dict[str, Any] = {} + errors: list[dict[str, Any]] = [] + # Use individual calls through the audited retry/pacing client. This is slower but fail-closed and resumable by output key. + for index, value in enumerate(values, 1): + try: + results[value] = rpc.call(method, [value, False] if method == "eth_getBlockByNumber" else [value]) + except Exception as exc: + errors.append({"method": method, "value": value, "error": repr(exc)}) + if index % batch_size == 0: + print({"method": method, "done": index, "total": len(values), "errors": len(errors)}, flush=True) + return results, errors + + +def transfer_logs(receipt: dict[str, Any]) -> list[dict[str, Any]]: + output = [] + for log in receipt.get("logs") or []: + topics = [str(value).lower() for value in (log.get("topics") or [])] + if not topics or topics[0] != TRANSFER_TOPIC: + continue + if len(topics) == 4: + output.append({ + "kind": "ERC721", + "contract": str(log.get("address") or "").lower(), + "from": topic_address(topics[1]), + "to": topic_address(topics[2]), + "token_id": int(topics[3], 16), + "amount": 1, + "log_index": integer(log.get("logIndex")), + }) + elif len(topics) == 3: + output.append({ + "kind": "ERC20", + "contract": str(log.get("address") or "").lower(), + "from": topic_address(topics[1]), + "to": topic_address(topics[2]), + "token_id": 0, + "amount": integer(log.get("data")), + "log_index": integer(log.get("logIndex")), + }) + return output + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--canonical-dir", required=True) + parser.add_argument("--out", required=True) + args = parser.parse_args() + canonical = Path(args.canonical_dir) + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + + events_path = next(iter(canonical.rglob("global_events.csv")), None) + completeness_path = next(iter(canonical.rglob("DATA_COMPLETENESS.json")), None) + if events_path is None or completeness_path is None: + raise SystemExit("canonical package missing global_events.csv or DATA_COMPLETENESS.json") + canonical_status = json.loads(completeness_path.read_text(encoding="utf-8")) + rows = read_csv(events_path) + seadrop_rows = [row for row in rows if row.get("target") == "seadrop"] + seaport_rows = [row for row in rows if row.get("target") == "seaport"] + if not seadrop_rows or not seaport_rows: + raise SystemExit("global canonical event histories are empty") + + unique_blocks = sorted({integer(row.get("block_number")) for row in rows}) + unique_txs = sorted({row.get("transaction_hash", "").lower() for row in rows if row.get("transaction_hash")}) + block_hex = [hex(value) for value in unique_blocks] + rpc = CanonicalRpc(min_interval=1.35) + block_results, block_errors = batch_rpc(rpc, "eth_getBlockByNumber", block_hex, batch_size=50) + receipt_results, receipt_errors = batch_rpc(rpc, "eth_getTransactionReceipt", unique_txs, batch_size=25) + + block_timestamps: dict[int, int] = {} + block_rows: list[dict[str, Any]] = [] + for block_number, key in zip(unique_blocks, block_hex): + block = block_results.get(key) + if isinstance(block, dict): + timestamp = integer(block.get("timestamp")) + block_timestamps[block_number] = timestamp + block_rows.append({ + "block_number": block_number, + "block_hash": str(block.get("hash") or "").lower(), + "timestamp_unix": timestamp, + "timestamp_utc": datetime.fromtimestamp(timestamp, timezone.utc).isoformat().replace("+00:00", "Z"), + }) + + receipt_path = out / "event_receipts.jsonl" + with receipt_path.open("w", encoding="utf-8", newline="\n") as handle: + for tx_hash in unique_txs: + receipt = receipt_results.get(tx_hash) + if isinstance(receipt, dict): + handle.write(json.dumps(receipt, sort_keys=True, separators=(",", ":")) + "\n") + + # Primary mint events and opportunity definitions. + mint_events: list[dict[str, Any]] = [] + project_events: dict[str, list[dict[str, Any]]] = defaultdict(list) + cumulative_quantity: dict[str, int] = defaultdict(int) + ordered_seadrop = sorted(seadrop_rows, key=lambda row: (integer(row.get("block_number")), integer(row.get("transaction_index")), integer(row.get("log_index")))) + for row in ordered_seadrop: + contract = row.get("nft_contract", "").lower() + block_number = integer(row.get("block_number")) + timestamp = block_timestamps.get(block_number) + quantity = integer(row.get("quantity_minted")) + before = cumulative_quantity[contract] + cumulative_quantity[contract] += quantity + event = { + "project_id": contract, + "nft_contract": contract, + "block_number": block_number, + "block_hash": row.get("block_hash"), + "timestamp_unix": timestamp, + "timestamp_utc": datetime.fromtimestamp(timestamp, timezone.utc).isoformat().replace("+00:00", "Z") if timestamp is not None else None, + "transaction_hash": row.get("transaction_hash"), + "log_index": integer(row.get("log_index")), + "minter": row.get("minter", "").lower(), + "payer": row.get("payer", "").lower(), + "fee_recipient": row.get("fee_recipient", "").lower(), + "quantity": quantity, + "unit_mint_price_wei": integer(row.get("unit_mint_price_wei")), + "fee_bps": integer(row.get("fee_bps")), + "drop_stage_index": integer(row.get("drop_stage_index")), + "is_public": integer(row.get("drop_stage_index")) == 0, + "is_paid": integer(row.get("unit_mint_price_wei")) > 0, + "is_self_funded": row.get("minter", "").lower() == row.get("payer", "").lower(), + "cumulative_quantity_before": before, + "cumulative_quantity_after": cumulative_quantity[contract], + } + mint_events.append(event) + project_events[contract].append(event) + + project_opportunities: list[dict[str, Any]] = [] + opportunity_by_project: dict[str, dict[str, Any]] = {} + for contract, events in project_events.items(): + public_events = [event for event in events if event["is_public"]] + paid_public = [event for event in public_events if event["is_paid"]] + free_events = [event for event in events if not event["is_paid"]] + paid_nonpublic = [event for event in events if event["is_paid"] and not event["is_public"]] + start = min((event["timestamp_unix"] for event in events if event["timestamp_unix"] is not None), default=None) + paid_public_start = min((event["timestamp_unix"] for event in paid_public if event["timestamp_unix"] is not None), default=None) + free_before_paid_public = any( + event["timestamp_unix"] is not None and paid_public_start is not None and event["timestamp_unix"] <= paid_public_start + for event in free_events + ) + prices = sorted({event["unit_mint_price_wei"] for event in paid_public}) + row = { + "project_id": contract, + "nft_contract": contract, + "first_mint_timestamp_unix": start, + "first_mint_timestamp_utc": datetime.fromtimestamp(start, timezone.utc).isoformat().replace("+00:00", "Z") if start is not None else None, + "first_paid_public_timestamp_unix": paid_public_start, + "first_paid_public_timestamp_utc": datetime.fromtimestamp(paid_public_start, timezone.utc).isoformat().replace("+00:00", "Z") if paid_public_start is not None else None, + "mint_event_rows": len(events), + "minted_quantity_observed": sum(event["quantity"] for event in events), + "unique_minters": len({event["minter"] for event in events}), + "public_event_rows": len(public_events), + "paid_public_event_rows": len(paid_public), + "paid_nonpublic_event_rows": len(paid_nonpublic), + "free_event_rows": len(free_events), + "free_before_paid_public": free_before_paid_public, + "paid_public_prices_wei_json": json.dumps(prices), + "reference_paid_public_price_wei": statistics.median(prices) if prices else None, + "has_paid_public_opportunity": bool(paid_public), + "strict_paid_public_from_start": bool(paid_public) and not free_before_paid_public and len(prices) == 1, + "opportunity_universe": "SEADROP_OBSERVED", + } + project_opportunities.append(row) + opportunity_by_project[contract] = row + + # Decode Seaport orders and prove transfers with canonical receipts. + sales: list[dict[str, Any]] = [] + order_errors: list[dict[str, Any]] = [] + for row in seaport_rows: + tx_hash = row.get("transaction_hash", "").lower() + receipt = receipt_results.get(tx_hash) + block_number = integer(row.get("block_number")) + timestamp = block_timestamps.get(block_number) + try: + decoded = decode_order_fulfilled(row) + offer_nfts = [item for item in decoded["offer"] if item["item_type"] in NFT_TYPES] + consideration_nfts = [item for item in decoded["consideration"] if item["item_type"] in NFT_TYPES] + direction = "NFT_OFFER_LISTING" if offer_nfts else "PAYMENT_OFFER_BID" if consideration_nfts else "NO_NFT" + nft_items = offer_nfts or consideration_nfts + payment_items = [item for item in (decoded["consideration"] if offer_nfts else decoded["offer"]) if item["item_type"] in PAYMENT_TYPES] + payment_tokens = {item["token"] for item in payment_items} + payment_token = next(iter(payment_tokens)) if len(payment_tokens) == 1 else None + order_gross = sum(item["amount"] for item in payment_items) if payment_token is not None else None + receipt_transfers = transfer_logs(receipt) if isinstance(receipt, dict) else [] + for item in nft_items: + contract = item["token"] + amount = max(1, item["amount"]) + seller = decoded["offerer"] if offer_nfts else decoded["recipient"] + buyer = decoded["recipient"] if offer_nfts else item.get("recipient") or decoded["offerer"] + matching_nft = [transfer for transfer in receipt_transfers if transfer["kind"] == "ERC721" and transfer["contract"] == contract and transfer["token_id"] == item["identifier"] and transfer["from"] == seller and transfer["to"] == buyer] + payment_to_seller = sum(transfer["amount"] for transfer in receipt_transfers if transfer["kind"] == "ERC20" and transfer["contract"] == payment_token and transfer["to"] == seller) if payment_token else 0 + gross_per_unit = order_gross / sum(max(1, value["amount"]) for value in nft_items) if order_gross is not None and nft_items else None + sales.append({ + "project_id": contract, + "nft_contract": contract, + "token_id": item["identifier"], + "nft_amount": amount, + "block_number": block_number, + "block_hash": row.get("block_hash"), + "timestamp_unix": timestamp, + "timestamp_utc": datetime.fromtimestamp(timestamp, timezone.utc).isoformat().replace("+00:00", "Z") if timestamp is not None else None, + "transaction_hash": tx_hash, + "log_index": integer(row.get("log_index")), + "order_hash": decoded["order_hash"], + "direction": direction, + "seller": seller, + "buyer": buyer, + "payment_token": payment_token, + "order_gross_payment_raw": order_gross, + "gross_payment_per_nft_unit_raw": gross_per_unit, + "payment_to_seller_raw_receipt": payment_to_seller, + "receipt_status": integer(receipt.get("status")) if isinstance(receipt, dict) else None, + "matching_nft_transfer_count": len(matching_nft), + "sale_proof_status": "PROVEN_ORDER_AND_NFT_TRANSFER" if isinstance(receipt, dict) and integer(receipt.get("status")) == 1 and matching_nft else "ORDER_ONLY_OR_TRANSFER_UNRESOLVED", + "offer_json": json.dumps(decoded["offer"], sort_keys=True, separators=(",", ":")), + "consideration_json": json.dumps(decoded["consideration"], sort_keys=True, separators=(",", ":")), + }) + except Exception as exc: + order_errors.append({"transaction_hash": tx_hash, "log_index": row.get("log_index"), "error": repr(exc)}) + + sales_by_project: dict[str, list[dict[str, Any]]] = defaultdict(list) + for sale in sales: + if sale["sale_proof_status"] == "PROVEN_ORDER_AND_NFT_TRANSFER": + sales_by_project[sale["project_id"]].append(sale) + + project_outcomes: list[dict[str, Any]] = [] + for opportunity in project_opportunities: + project_id = opportunity["project_id"] + start = opportunity["first_paid_public_timestamp_unix"] + reference_price = opportunity["reference_paid_public_price_wei"] + base = { + "project_id": project_id, + "nft_contract": project_id, + "first_paid_public_timestamp_unix": start, + "reference_paid_public_price_wei": reference_price, + } + project_sales = sales_by_project.get(project_id, []) + for label, seconds in WINDOWS.items(): + eligible = [sale for sale in project_sales if start is not None and sale["timestamp_unix"] is not None and start <= sale["timestamp_unix"] <= start + seconds and sale["payment_token"] in {NATIVE, WETH_RHC} and sale["gross_payment_per_nft_unit_raw"] is not None] + order_keys = {(sale["transaction_hash"], sale["order_hash"]) for sale in eligible} + buyers = {sale["buyer"] for sale in eligible if sale["buyer"]} + prices = [float(sale["gross_payment_per_nft_unit_raw"]) for sale in eligible] + median_price = statistics.median(prices) if prices else None + multiple = median_price / float(reference_price) if median_price is not None and reference_price else None + base[f"orders_{label}"] = len(order_keys) + base[f"independent_buyers_{label}"] = len(buyers) + base[f"sale_units_{label}"] = sum(integer(sale["nft_amount"]) for sale in eligible) + base[f"median_gross_per_unit_raw_{label}"] = median_price + base[f"median_multiple_vs_mint_{label}"] = multiple + base[f"success_liquid_100_{label}"] = bool(multiple is not None and multiple >= 1.0 and len(order_keys) >= 3 and len(buyers) >= 3) + base[f"success_liquid_115_{label}"] = bool(multiple is not None and multiple >= 1.15 and len(order_keys) >= 3 and len(buyers) >= 3) + project_outcomes.append(base) + outcome_by_project = {row["project_id"]: row for row in project_outcomes} + + # Build early participation entries before first proven sale and within the first 20% of observed primary quantity. + first_sale_time = {project: min(sale["timestamp_unix"] for sale in project_sales if sale["timestamp_unix"] is not None) for project, project_sales in sales_by_project.items() if any(sale["timestamp_unix"] is not None for sale in project_sales)} + wallet_entries: list[dict[str, Any]] = [] + for event in mint_events: + opportunity = opportunity_by_project[event["project_id"]] + total_quantity = max(1, integer(opportunity["minted_quantity_observed"])) + entry_quantile = event["cumulative_quantity_after"] / total_quantity + before_sale = first_sale_time.get(event["project_id"]) is None or (event["timestamp_unix"] is not None and event["timestamp_unix"] < first_sale_time[event["project_id"]]) + early = entry_quantile <= 0.20 and before_sale + wallet_entries.append({ + **event, + "entry_quantity_quantile": entry_quantile, + "before_first_proven_sale": before_sale, + "is_early_entry": early, + "selection_signal_eligible": bool(early and event["is_paid"] and event["is_self_funded"]), + "signal_type": "PUBLIC_SELF_FUNDED" if event["is_public"] and event["is_paid"] and event["is_self_funded"] else "PRIVILEGED_SELF_FUNDED" if event["is_paid"] and event["is_self_funded"] else "OTHER", + }) + + # Matched baseline Selection Alpha for public self-funded early entries. + paid_public_opportunities = [row for row in project_opportunities if row["has_paid_public_opportunity"] and row["first_paid_public_timestamp_unix"] is not None and row["reference_paid_public_price_wei"]] + eligible_entries = [row for row in wallet_entries if row["selection_signal_eligible"] and row["signal_type"] == "PUBLIC_SELF_FUNDED"] + entry_by_wallet: dict[str, dict[str, dict[str, Any]]] = defaultdict(dict) + for row in eligible_entries: + entry_by_wallet[row["minter"]][row["project_id"]] = row + wallet_metrics: list[dict[str, Any]] = [] + for wallet, projects in entry_by_wallet.items(): + samples: list[dict[str, Any]] = [] + for project_id, entry in projects.items(): + opportunity = opportunity_by_project[project_id] + start = integer(opportunity["first_paid_public_timestamp_unix"]) + price = float(opportunity["reference_paid_public_price_wei"]) + comparable = [] + for candidate in paid_public_opportunities: + candidate_start = integer(candidate["first_paid_public_timestamp_unix"]) + candidate_price = float(candidate["reference_paid_public_price_wei"]) + if abs(candidate_start - start) <= 3 * 86400 and 0.25 <= candidate_price / price <= 4.0: + comparable.append(candidate["project_id"]) + outcome = outcome_by_project.get(project_id, {}) + comparison_outcomes = [outcome_by_project.get(value, {}) for value in comparable] + for metric in ("success_liquid_100_24h", "success_liquid_115_24h"): + observed = 1.0 if outcome.get(metric) else 0.0 + baseline_values = [1.0 if value.get(metric) else 0.0 for value in comparison_outcomes] + baseline = statistics.mean(baseline_values) if baseline_values else None + samples.append({"metric": metric, "observed": observed, "baseline": baseline, "project_id": project_id, "comparable_projects": len(comparable)}) + result: dict[str, Any] = { + "wallet": wallet, + "entered_projects": len(projects), + "median_entry_quantity_quantile": statistics.median(row["entry_quantity_quantile"] for row in projects.values()), + "production_approved": False, + "decision_use": "RESEARCH_SELECTION_ALPHA_ONLY", + } + for metric in ("success_liquid_100_24h", "success_liquid_115_24h"): + values = [sample for sample in samples if sample["metric"] == metric and sample["baseline"] is not None] + result[f"{metric}_sample_count"] = len(values) + result[f"{metric}_hit_rate"] = statistics.mean(sample["observed"] for sample in values) if values else None + result[f"{metric}_matched_baseline"] = statistics.mean(sample["baseline"] for sample in values) if values else None + result[f"{metric}_predictive_lift"] = statistics.mean(sample["observed"] - sample["baseline"] for sample in values) if values else None + wallet_metrics.append(result) + + write_csv(out / "block_timestamps.csv", block_rows) + write_csv(out / "primary_mint_events.csv", mint_events) + write_csv(out / "project_opportunities.csv", project_opportunities) + write_csv(out / "seaport_sales.csv", sales) + write_csv(out / "project_outcomes.csv", project_outcomes) + write_csv(out / "wallet_project_entries.csv", wallet_entries) + write_csv(out / "wallet_selection_alpha.csv", wallet_metrics) + write_csv(out / "order_decode_errors.csv", order_errors) + write_csv(out / "rpc_errors.csv", block_errors + receipt_errors) + + missing_blocks = sorted(set(unique_blocks) - set(block_timestamps)) + missing_receipts = sorted(set(unique_txs) - {key for key, value in receipt_results.items() if isinstance(value, dict)}) + validation = { + "status": "PASS" if not missing_blocks and not missing_receipts and not order_errors else "PARTIAL_REMEDIATION_REQUIRED", + "canonical_input_status": canonical_status.get("status"), + "seadrop_event_rows": len(seadrop_rows), + "seaport_event_rows": len(seaport_rows), + "unique_event_blocks": len(unique_blocks), + "block_timestamps_resolved": len(block_timestamps), + "missing_block_count": len(missing_blocks), + "unique_event_transactions": len(unique_txs), + "receipts_resolved": sum(isinstance(value, dict) for value in receipt_results.values()), + "missing_receipt_count": len(missing_receipts), + "project_opportunity_rows": len(project_opportunities), + "paid_public_opportunities": sum(bool(row["has_paid_public_opportunity"]) for row in project_opportunities), + "strict_paid_public_from_start": sum(bool(row["strict_paid_public_from_start"]) for row in project_opportunities), + "proven_sale_rows": sum(row["sale_proof_status"] == "PROVEN_ORDER_AND_NFT_TRANSFER" for row in sales), + "wallet_project_entry_rows": len(wallet_entries), + "selection_metric_wallets": len(wallet_metrics), + "order_decode_errors": len(order_errors), + "block_rpc_errors": block_errors, + "receipt_rpc_errors": receipt_errors, + "missing_blocks": missing_blocks, + "missing_receipts": missing_receipts, + "production_approved_wallets": 0, + "deepseek_handoff_allowed": False, + } + (out / "VALIDATION.json").write_text(json.dumps(validation, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + manifest = [] + for path in sorted(out.iterdir()): + if path.is_file(): + manifest.append({"path": path.name, "bytes": path.stat().st_size, "sha256": sha256(path)}) + (out / "MANIFEST.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + print(json.dumps(validation, sort_keys=True), flush=True) + + +if __name__ == "__main__": + main() From 446b3d6ce18a924430c6ced181568c7840c5325d Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:55:29 +0900 Subject: [PATCH 22/42] ci: derive and publish complete Robinhood NFT project opportunity universe --- .../rhc-project-universe-derivation.yml | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 .github/workflows/rhc-project-universe-derivation.yml diff --git a/.github/workflows/rhc-project-universe-derivation.yml b/.github/workflows/rhc-project-universe-derivation.yml new file mode 100644 index 0000000..70836ba --- /dev/null +++ b/.github/workflows/rhc-project-universe-derivation.yml @@ -0,0 +1,139 @@ +name: RHC project opportunity and outcome universe + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-project-universe-derivation.yml' + - 'rhc-data-completion/derive_project_universe.py' + - 'rhc-data-completion/canonical_rpc.py' + +permissions: + contents: write + +concurrency: + group: rhc-project-universe-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + derive: + runs-on: ubuntu-latest + timeout-minutes: 360 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Wait for consolidated canonical package on temporary branch + shell: bash + env: + REPO: ${{ github.repository }} + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + set -euo pipefail + mkdir -p input-package + python - <<'PY' + import json, os, pathlib, time, urllib.parse, urllib.request, zipfile, io + repo=os.environ['REPO']; branch=os.environ['HEAD_BRANCH'] + base=f'https://raw.githubusercontent.com/{repo}/{urllib.parse.quote(branch, safe="")}/.results' + index_url=base+'/rhc-canonical-package-index.json' + zip_url=base+'/rhc-canonical-data-consolidated.zip' + index=None + payload=None + for attempt in range(360): + try: + with urllib.request.urlopen(index_url,timeout=60) as response: + index=json.loads(response.read()) + with urllib.request.urlopen(zip_url,timeout=300) as response: + payload=response.read() + if payload and len(payload)==int(index['bytes']): + print({'attempt':attempt,'bytes':len(payload),'sha256':index.get('sha256')},flush=True) + break + except Exception as exc: + if attempt % 10 == 0: print({'attempt':attempt,'waiting':repr(exc)},flush=True) + time.sleep(30) + if not index or not payload: + raise SystemExit('canonical consolidated package was not published') + import hashlib + actual=hashlib.sha256(payload).hexdigest() + if actual != index.get('sha256'): + raise SystemExit(f'canonical package SHA mismatch: {actual} != {index.get("sha256")}') + pathlib.Path('input-package/rhc-canonical-data-consolidated.zip').write_bytes(payload) + with zipfile.ZipFile(io.BytesIO(payload)) as archive: + archive.extractall('input-package/extracted') + pathlib.Path('input-package/package-index.json').write_text(json.dumps(index,indent=2,sort_keys=True)) + PY + - name: Enforce canonical package completeness before derivation + shell: bash + run: | + set -euo pipefail + python - <<'PY' + import json, pathlib + matches=list(pathlib.Path('input-package/extracted').rglob('DATA_COMPLETENESS.json')) + assert len(matches)==1, matches + report=json.loads(matches[0].read_text()) + if report.get('status')!='PASS' or not report.get('data_collection_complete_for_wallet_verification'): + raise SystemExit(json.dumps({'code':'CANONICAL_INPUT_NOT_COMPLETE','report':report},sort_keys=True)) + print(json.dumps(report,sort_keys=True)) + PY + - name: Derive full opportunity, outcome, and Selection Alpha datasets + shell: bash + run: | + set -euo pipefail + PYTHONPATH=rhc-data-completion python rhc-data-completion/derive_project_universe.py \ + --canonical-dir input-package/extracted \ + --out out-project-universe + - name: Enforce derived dataset completeness + shell: bash + run: | + set -euo pipefail + python - <<'PY' + import json, pathlib + report=json.loads(pathlib.Path('out-project-universe/VALIDATION.json').read_text()) + failures=[] + if report.get('status')!='PASS': failures.append({'code':'DERIVATION_VALIDATION_NOT_PASS','value':report.get('status')}) + for key in ('seadrop_event_rows','seaport_event_rows','project_opportunity_rows','paid_public_opportunities','wallet_project_entry_rows'): + if int(report.get(key) or 0)<=0: failures.append({'code':'EMPTY_REQUIRED_DATASET','key':key}) + if int(report.get('missing_block_count') or 0): failures.append({'code':'BLOCK_TIMESTAMPS_MISSING','count':report.get('missing_block_count')}) + if int(report.get('missing_receipt_count') or 0): failures.append({'code':'RECEIPTS_MISSING','count':report.get('missing_receipt_count')}) + if int(report.get('order_decode_errors') or 0): failures.append({'code':'SEAPORT_DECODE_ERRORS','count':report.get('order_decode_errors')}) + result={'status':'PASS' if not failures else 'FAIL','failures':failures,'source_validation':report,'deepseek_handoff_allowed':False,'production_approved_wallets':0} + pathlib.Path('out-project-universe/FINAL_GATE.json').write_text(json.dumps(result,indent=2,sort_keys=True)) + print(json.dumps(result,sort_keys=True)) + if failures: raise SystemExit(1) + PY + - name: Package and publish immutable project-universe result + if: always() + shell: bash + env: + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + set -euo pipefail + mkdir -p .results + python - <<'PY' + import hashlib, json, pathlib, zipfile, os, shutil + source=pathlib.Path('out-project-universe') + archive=pathlib.Path('.results/rhc-project-universe-v1.zip') + if archive.exists(): archive.unlink() + with zipfile.ZipFile(archive,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=9) as z: + if source.exists(): + for path in sorted(source.rglob('*')): + if path.is_file(): z.write(path,path.relative_to(source.parent)) + digest=hashlib.sha256(archive.read_bytes()).hexdigest() + index={'package':archive.name,'bytes':archive.stat().st_size,'sha256':digest,'publisher_run_id':int(os.environ['GITHUB_RUN_ID']),'files':[str(p.relative_to(source)) for p in sorted(source.rglob('*')) if p.is_file()] if source.exists() else []} + pathlib.Path('.results/rhc-project-universe-v1-index.json').write_text(json.dumps(index,indent=2,sort_keys=True)) + pathlib.Path('.results/rhc-project-universe-v1.zip.sha256').write_text(digest+' '+archive.name+'\n') + for name in ('VALIDATION.json','FINAL_GATE.json'): + path=source/name + if path.exists(): shutil.copy2(path,pathlib.Path('.results')/f'rhc-project-universe-{name.lower()}') + if archive.stat().st_size > 90_000_000: raise SystemExit('derived package exceeds GitHub file limit') + PY + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add .results/ + if git diff --cached --quiet; then exit 0; fi + git commit -m 'chore: publish project opportunity universe [skip ci]' + git push origin "HEAD:${HEAD_BRANCH}" From e18e4c2e9baebd6748d6041b73dd22a6724c3323 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:30:47 +0900 Subject: [PATCH 23/42] data: add canonical SeaDrop and Seaport direct-RPC collector --- .../global_event_collector.py | 461 ++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 canonical-completion/global_event_collector.py diff --git a/canonical-completion/global_event_collector.py b/canonical-completion/global_event_collector.py new file mode 100644 index 0000000..443beb9 --- /dev/null +++ b/canonical-completion/global_event_collector.py @@ -0,0 +1,461 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import random +import time +import urllib.error +import urllib.request +from collections import defaultdict, deque +from pathlib import Path +from typing import Any + +RPC_URL = "https://rpc.mainnet.chain.robinhood.com" +CHAIN_ID = 4663 +USER_AGENT = "RHC-Canonical-Completion/1.0" + +TARGETS = { + "seadrop": { + "address": "0x00005ea00ac477b1030ce78506496e8c2de24bf5", + "topic0": "0xe90cf9cc0a552cf52ea6ff74ece0f1c8ae8cc9ad630d3181f55ac43ca076b7d6", + }, + "seaport": { + "address": "0x0000000000000068f116a894984e2db1123eb395", + "topic0": "0x9d9af8e38d66c62e2c12f0225249fd9d721c54b83f48d9352c97c6cacdcb6f31", + }, +} + + +def hex_int(value: Any) -> int | None: + if value is None: + return None + if isinstance(value, int): + return value + if isinstance(value, str): + return int(value, 16) if value.startswith("0x") else int(value) + return int(value) + + +def address_word(value: str) -> str: + value = value.removeprefix("0x") + return "0x" + value[-40:].lower() + + +def words(data: str) -> list[str]: + raw = data.removeprefix("0x") + if len(raw) % 64: + raise ValueError(f"ABI data length is not word-aligned: {len(raw)}") + return [raw[i : i + 64] for i in range(0, len(raw), 64)] + + +def write_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + + +def write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8", newline="\n") as handle: + for row in rows: + handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n") + + +def write_csv(path: Path, rows: list[dict[str, Any]]) -> None: + fields = sorted({key for row in rows for key in row}) if rows else ["empty"] + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", newline="", encoding="utf-8-sig") as handle: + writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore") + writer.writeheader() + for row in rows: + writer.writerow( + { + key: json.dumps(value, ensure_ascii=False, sort_keys=True) + if isinstance(value, (dict, list, tuple)) + else value + for key, value in row.items() + } + ) + + +class RpcClient: + def __init__(self, url: str = RPC_URL, min_interval: float = 0.72): + self.url = url + self.min_interval = min_interval + self.last_request = 0.0 + self.request_count = 0 + self.retry_count = 0 + + def pace(self) -> None: + wait = self.min_interval - (time.monotonic() - self.last_request) + if wait > 0: + time.sleep(wait) + + def request(self, payload: Any, attempts: int = 10) -> Any: + last_error: Exception | None = None + for attempt in range(attempts): + self.pace() + request = urllib.request.Request( + self.url, + data=json.dumps(payload).encode("utf-8"), + method="POST", + headers={ + "content-type": "application/json", + "accept": "application/json", + "user-agent": USER_AGENT, + }, + ) + try: + with urllib.request.urlopen(request, timeout=120) as response: + body = response.read() + self.last_request = time.monotonic() + self.request_count += 1 + result = json.loads(body.decode("utf-8")) + return result + except urllib.error.HTTPError as exc: + self.last_request = time.monotonic() + body = exc.read(3000).decode("utf-8", "replace") + last_error = RuntimeError(f"HTTP {exc.code}: {body}") + if exc.code == 429 or exc.code >= 500: + self.retry_count += 1 + time.sleep(min(90, 3 * (2 ** min(attempt, 5)) + random.random() * 4)) + continue + raise last_error + except Exception as exc: + self.last_request = time.monotonic() + last_error = exc + self.retry_count += 1 + if attempt + 1 < attempts: + time.sleep(min(60, 2 ** min(attempt, 5) + random.random() * 3)) + continue + break + raise RuntimeError(f"RPC request failed after {attempts} attempts: {last_error}") + + def call(self, method: str, params: list[Any]) -> Any: + payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params} + response = self.request(payload) + if not isinstance(response, dict): + raise RuntimeError(f"Unexpected RPC response type: {type(response)}") + if response.get("error") is not None: + raise RuntimeError(json.dumps(response["error"], sort_keys=True)) + return response.get("result") + + def batch(self, calls: list[tuple[str, list[Any]]]) -> list[Any]: + payload = [ + {"jsonrpc": "2.0", "id": index, "method": method, "params": params} + for index, (method, params) in enumerate(calls) + ] + response = self.request(payload) + if not isinstance(response, list): + # Some gateways reject batch requests. Fall back to individual calls. + return [self.call(method, params) for method, params in calls] + indexed = {int(item.get("id")): item for item in response if isinstance(item, dict)} + output: list[Any] = [] + for index, (method, params) in enumerate(calls): + item = indexed.get(index) + if item is None or item.get("error") is not None: + output.append(self.call(method, params)) + else: + output.append(item.get("result")) + return output + + +def get_logs(client: RpcClient, target: dict[str, str], start: int, end: int) -> list[dict[str, Any]]: + result = client.call( + "eth_getLogs", + [ + { + "fromBlock": hex(start), + "toBlock": hex(end), + "address": target["address"], + "topics": [target["topic0"]], + } + ], + ) + if not isinstance(result, list): + raise RuntimeError(f"eth_getLogs returned non-list: {type(result)}") + return [row for row in result if isinstance(row, dict)] + + +def collect_ranges( + client: RpcClient, + target: dict[str, str], + head: int, + nominal_chunk: int = 250_000, +) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]: + queue: deque[tuple[int, int, int]] = deque() + for start in range(0, head + 1, nominal_chunk): + queue.append((start, min(head, start + nominal_chunk - 1), 0)) + rows: list[dict[str, Any]] = [] + completed: list[dict[str, Any]] = [] + unresolved: list[dict[str, Any]] = [] + while queue: + start, end, depth = queue.popleft() + try: + batch = get_logs(client, target, start, end) + rows.extend(batch) + completed.append({"from_block": start, "to_block": end, "rows": len(batch), "depth": depth}) + if len(completed) % 20 == 0: + print({"completed_ranges": len(completed), "queued_ranges": len(queue), "events": len(rows)}, flush=True) + except Exception as exc: + if start < end and depth < 28: + midpoint = (start + end) // 2 + queue.appendleft((midpoint + 1, end, depth + 1)) + queue.appendleft((start, midpoint, depth + 1)) + else: + unresolved.append({"from_block": start, "to_block": end, "depth": depth, "error": repr(exc)}) + return rows, completed, unresolved + + +def event_key(row: dict[str, Any]) -> tuple[str, str, str]: + return ( + str(row.get("blockHash") or "").lower(), + str(row.get("transactionHash") or "").lower(), + str(row.get("logIndex") or "").lower(), + ) + + +def decode_seadrop(row: dict[str, Any]) -> dict[str, Any]: + topics = row.get("topics") or [] + data_words = words(str(row.get("data") or "0x")) + if len(topics) != 4 or len(data_words) != 5: + raise ValueError(f"Unexpected SeaDropMint layout topics={len(topics)} words={len(data_words)}") + return { + "chain_id": CHAIN_ID, + "block_number": hex_int(row.get("blockNumber")), + "block_hash": str(row.get("blockHash") or "").lower(), + "transaction_hash": str(row.get("transactionHash") or "").lower(), + "transaction_index": hex_int(row.get("transactionIndex")), + "log_index": hex_int(row.get("logIndex")), + "removed": bool(row.get("removed")), + "seadrop_contract": str(row.get("address") or "").lower(), + "nft_contract": address_word(topics[1]), + "minter": address_word(topics[2]), + "fee_recipient": address_word(topics[3]), + "payer": address_word(data_words[0]), + "quantity_minted": int(data_words[1], 16), + "unit_mint_price_wei": int(data_words[2], 16), + "fee_bps": int(data_words[3], 16), + "drop_stage_index": int(data_words[4], 16), + } + + +def decode_spent_items(data_words: list[str], offset_bytes: int) -> list[dict[str, Any]]: + start = offset_bytes // 32 + length = int(data_words[start], 16) + output = [] + cursor = start + 1 + for _ in range(length): + output.append( + { + "item_type": int(data_words[cursor], 16), + "token": address_word(data_words[cursor + 1]), + "identifier": str(int(data_words[cursor + 2], 16)), + "amount": str(int(data_words[cursor + 3], 16)), + } + ) + cursor += 4 + return output + + +def decode_received_items(data_words: list[str], offset_bytes: int) -> list[dict[str, Any]]: + start = offset_bytes // 32 + length = int(data_words[start], 16) + output = [] + cursor = start + 1 + for _ in range(length): + output.append( + { + "item_type": int(data_words[cursor], 16), + "token": address_word(data_words[cursor + 1]), + "identifier": str(int(data_words[cursor + 2], 16)), + "amount": str(int(data_words[cursor + 3], 16)), + "recipient": address_word(data_words[cursor + 4]), + } + ) + cursor += 5 + return output + + +def decode_seaport(row: dict[str, Any]) -> dict[str, Any]: + topics = row.get("topics") or [] + data_words = words(str(row.get("data") or "0x")) + if len(topics) != 3 or len(data_words) < 4: + raise ValueError(f"Unexpected OrderFulfilled layout topics={len(topics)} words={len(data_words)}") + order_hash = "0x" + data_words[0] + recipient = address_word(data_words[1]) + offer_offset = int(data_words[2], 16) + consideration_offset = int(data_words[3], 16) + offer = decode_spent_items(data_words, offer_offset) + consideration = decode_received_items(data_words, consideration_offset) + offerer = address_word(topics[1]) + zone = address_word(topics[2]) + offer_has_nft = any(item["item_type"] in (2, 3) for item in offer) + consideration_has_nft = any(item["item_type"] in (2, 3) for item in consideration) + if offer_has_nft and not consideration_has_nft: + direction = "LISTING_OR_NFT_OFFER" + seller, buyer = offerer, recipient + elif consideration_has_nft and not offer_has_nft: + direction = "BID_OR_CURRENCY_OFFER" + seller, buyer = recipient, offerer + else: + direction = "MIXED_OR_BUNDLE" + seller = buyer = None + payment_totals: dict[str, int] = defaultdict(int) + payment_source = consideration if offer_has_nft else offer + for item in payment_source: + if item["item_type"] in (0, 1): + token = "NATIVE" if item["item_type"] == 0 else item["token"] + payment_totals[token] += int(item["amount"]) + nft_items = [item for item in offer + consideration if item["item_type"] in (2, 3)] + return { + "chain_id": CHAIN_ID, + "block_number": hex_int(row.get("blockNumber")), + "block_hash": str(row.get("blockHash") or "").lower(), + "transaction_hash": str(row.get("transactionHash") or "").lower(), + "transaction_index": hex_int(row.get("transactionIndex")), + "log_index": hex_int(row.get("logIndex")), + "removed": bool(row.get("removed")), + "seaport_contract": str(row.get("address") or "").lower(), + "order_hash": order_hash.lower(), + "offerer": offerer, + "zone": zone, + "recipient": recipient, + "order_direction": direction, + "seller": seller, + "buyer": buyer, + "offer": offer, + "consideration": consideration, + "nft_items": nft_items, + "payment_totals_raw": dict(payment_totals), + } + + +def batch_fetch(client: RpcClient, method: str, values: list[Any], batch_size: int = 20) -> dict[Any, Any]: + output: dict[Any, Any] = {} + for start in range(0, len(values), batch_size): + chunk = values[start : start + batch_size] + if method == "eth_getBlockByNumber": + calls = [(method, [hex(int(value)), False]) for value in chunk] + else: + calls = [(method, [value]) for value in chunk] + results = client.batch(calls) + output.update(zip(chunk, results)) + if start % (batch_size * 10) == 0: + print({"method": method, "fetched": min(start + len(chunk), len(values)), "total": len(values)}, flush=True) + return output + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--target", choices=sorted(TARGETS), required=True) + parser.add_argument("--out", required=True) + args = parser.parse_args() + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + client = RpcClient() + chain_id = hex_int(client.call("eth_chainId", [])) + if chain_id != CHAIN_ID: + raise RuntimeError(f"Wrong chain id: {chain_id}") + head = hex_int(client.call("eth_blockNumber", [])) + assert head is not None + raw_rows, ranges, unresolved = collect_ranges(client, TARGETS[args.target], head) + unique: dict[tuple[str, str, str], dict[str, Any]] = {} + duplicates = 0 + for row in raw_rows: + key = event_key(row) + if key in unique: + duplicates += 1 + unique[key] = row + logs = sorted( + unique.values(), + key=lambda row: (hex_int(row.get("blockNumber")) or -1, hex_int(row.get("logIndex")) or -1), + ) + decoder = decode_seadrop if args.target == "seadrop" else decode_seaport + decoded: list[dict[str, Any]] = [] + decode_errors: list[dict[str, Any]] = [] + for row in logs: + try: + decoded.append(decoder(row)) + except Exception as exc: + decode_errors.append({"event_key": event_key(row), "error": repr(exc), "raw": row}) + blocks = sorted({hex_int(row.get("blockNumber")) for row in logs if hex_int(row.get("blockNumber")) is not None}) + tx_hashes = sorted({str(row.get("transactionHash") or "").lower() for row in logs if row.get("transactionHash")}) + block_data = batch_fetch(client, "eth_getBlockByNumber", blocks, batch_size=30) + tx_data = batch_fetch(client, "eth_getTransactionByHash", tx_hashes, batch_size=20) + receipt_data = batch_fetch(client, "eth_getTransactionReceipt", tx_hashes, batch_size=15) + timestamps = { + block: hex_int(value.get("timestamp")) if isinstance(value, dict) else None + for block, value in block_data.items() + } + for row in decoded: + timestamp = timestamps.get(row["block_number"]) + row["block_timestamp_unix"] = timestamp + row["block_timestamp_utc"] = ( + time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(timestamp)) if timestamp is not None else None + ) + write_jsonl(out / "raw_logs.jsonl", logs) + write_jsonl(out / "decoded_events.jsonl", decoded) + write_csv(out / "decoded_events.csv", decoded) + write_jsonl(out / "transactions.jsonl", [{"transaction_hash": key, "value": value} for key, value in tx_data.items()]) + write_jsonl(out / "receipts.jsonl", [{"transaction_hash": key, "value": value} for key, value in receipt_data.items()]) + write_jsonl(out / "blocks.jsonl", [{"block_number": key, "value": value} for key, value in block_data.items()]) + write_json(out / "completed_ranges.json", ranges) + write_json(out / "unresolved_ranges.json", unresolved) + write_json(out / "decode_errors.json", decode_errors) + failures: list[dict[str, Any]] = [] + if unresolved: + failures.append({"code": "UNRESOLVED_BLOCK_RANGES", "count": len(unresolved)}) + if decode_errors: + failures.append({"code": "EVENT_DECODE_ERRORS", "count": len(decode_errors)}) + if len(decoded) != len(logs): + failures.append({"code": "DECODED_COUNT_MISMATCH", "raw": len(logs), "decoded": len(decoded)}) + if any(row.get("removed") for row in decoded): + failures.append({"code": "REMOVED_LOG_PRESENT"}) + missing_txs = [key for key, value in tx_data.items() if not isinstance(value, dict)] + missing_receipts = [key for key, value in receipt_data.items() if not isinstance(value, dict)] + missing_blocks = [key for key, value in block_data.items() if not isinstance(value, dict)] + if missing_txs: + failures.append({"code": "MISSING_TRANSACTIONS", "count": len(missing_txs)}) + if missing_receipts: + failures.append({"code": "MISSING_RECEIPTS", "count": len(missing_receipts)}) + if missing_blocks: + failures.append({"code": "MISSING_BLOCKS", "count": len(missing_blocks)}) + validation = { + "status": "PASS" if not failures else "FAIL", + "target": args.target, + "chain_id": chain_id, + "fixed_head_block": head, + "completed_ranges": len(ranges), + "unresolved_ranges": len(unresolved), + "raw_rows_before_dedup": len(raw_rows), + "duplicate_rows_removed": duplicates, + "canonical_event_rows": len(logs), + "decoded_event_rows": len(decoded), + "unique_transactions": len(tx_hashes), + "unique_blocks": len(blocks), + "rpc_request_count": client.request_count, + "rpc_retry_count": client.retry_count, + "failures": failures, + } + write_json(out / "VALIDATION.json", validation) + manifest = [] + for path in sorted(out.iterdir()): + if path.is_file() and path.name != "MANIFEST.json": + manifest.append( + { + "path": path.name, + "bytes": path.stat().st_size, + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + } + ) + write_json(out / "MANIFEST.json", manifest) + print(json.dumps(validation, sort_keys=True), flush=True) + if failures: + raise SystemExit(1) + + +if __name__ == "__main__": + main() From e4c6fbe1f66320605e4666534537fac3797d7e51 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:33:00 +0900 Subject: [PATCH 24/42] data: add direct-RPC candidate wallet NFT history collector --- .../candidate_wallet_collector.py | 432 ++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 canonical-completion/candidate_wallet_collector.py diff --git a/canonical-completion/candidate_wallet_collector.py b/canonical-completion/candidate_wallet_collector.py new file mode 100644 index 0000000..1fd80d5 --- /dev/null +++ b/canonical-completion/candidate_wallet_collector.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import base64 +import csv +import gzip +import hashlib +import json +import random +import re +import time +import urllib.error +import urllib.request +from collections import deque +from pathlib import Path +from typing import Any + +RPC_URL = "https://rpc.mainnet.chain.robinhood.com" +CHAIN_ID = 4663 +USER_AGENT = "RHC-Candidate-Wallet-Completion/1.0" +ZERO = "0x0000000000000000000000000000000000000000" +ERC721_TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" +# ERC-1155 event signatures are computed at runtime with pycryptodome. + + +def keccak256(text: str) -> str: + from Crypto.Hash import keccak + digest = keccak.new(digest_bits=256) + digest.update(text.encode("utf-8")) + return "0x" + digest.hexdigest() + + +ERC1155_SINGLE = keccak256("TransferSingle(address,address,address,uint256,uint256)") +ERC1155_BATCH = keccak256("TransferBatch(address,address,address,uint256[],uint256[])") +ADDR = re.compile(r"^0x[a-f0-9]{40}$") + + +def h2i(value: Any) -> int | None: + if value is None: + return None + if isinstance(value, int): + return value + if isinstance(value, str): + return int(value, 16) if value.startswith("0x") else int(value) + return int(value) + + +def padded(address: str) -> str: + return "0x" + "0" * 24 + address.lower().removeprefix("0x") + + +def topic_address(value: str) -> str: + return "0x" + value.removeprefix("0x")[-40:].lower() + + +def write_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + + +def write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8", newline="\n") as handle: + for row in rows: + handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n") + + +def write_csv(path: Path, rows: list[dict[str, Any]]) -> None: + fields = sorted({key for row in rows for key in row}) if rows else ["empty"] + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", newline="", encoding="utf-8-sig") as handle: + writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore") + writer.writeheader() + for row in rows: + writer.writerow({ + key: json.dumps(value, ensure_ascii=False, sort_keys=True) + if isinstance(value, (dict, list, tuple)) else value + for key, value in row.items() + }) + + +class RpcClient: + def __init__(self, min_interval: float = 1.15): + self.min_interval = min_interval + self.last_request = 0.0 + self.request_count = 0 + self.retry_count = 0 + + def pace(self) -> None: + wait = self.min_interval - (time.monotonic() - self.last_request) + if wait > 0: + time.sleep(wait) + + def request(self, payload: Any, attempts: int = 12) -> Any: + last_error: Exception | None = None + for attempt in range(attempts): + self.pace() + request = urllib.request.Request( + RPC_URL, + data=json.dumps(payload).encode("utf-8"), + method="POST", + headers={"content-type": "application/json", "accept": "application/json", "user-agent": USER_AGENT}, + ) + try: + with urllib.request.urlopen(request, timeout=150) as response: + body = response.read() + self.last_request = time.monotonic() + self.request_count += 1 + return json.loads(body.decode("utf-8")) + except urllib.error.HTTPError as exc: + self.last_request = time.monotonic() + body = exc.read(3000).decode("utf-8", "replace") + last_error = RuntimeError(f"HTTP {exc.code}: {body}") + if exc.code == 429 or exc.code >= 500: + self.retry_count += 1 + time.sleep(min(120, 4 * (2 ** min(attempt, 5)) + random.random() * 7)) + continue + raise last_error + except Exception as exc: + self.last_request = time.monotonic() + last_error = exc + self.retry_count += 1 + if attempt + 1 < attempts: + time.sleep(min(90, 3 * (2 ** min(attempt, 5)) + random.random() * 5)) + continue + break + raise RuntimeError(f"RPC failed after {attempts} attempts: {last_error}") + + def call(self, method: str, params: list[Any]) -> Any: + response = self.request({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}) + if not isinstance(response, dict): + raise RuntimeError(f"Unexpected response type: {type(response)}") + if response.get("error") is not None: + raise RuntimeError(json.dumps(response["error"], sort_keys=True)) + return response.get("result") + + def batch(self, calls: list[tuple[str, list[Any]]]) -> list[Any]: + payload = [ + {"jsonrpc": "2.0", "id": index, "method": method, "params": params} + for index, (method, params) in enumerate(calls) + ] + response = self.request(payload) + if not isinstance(response, list): + return [self.call(method, params) for method, params in calls] + indexed = {int(item.get("id")): item for item in response if isinstance(item, dict)} + output = [] + for index, (method, params) in enumerate(calls): + item = indexed.get(index) + if item is None or item.get("error") is not None: + output.append(self.call(method, params)) + else: + output.append(item.get("result")) + return output + + +def recursive_logs( + client: RpcClient, + filter_base: dict[str, Any], + start: int, + end: int, +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + queue: deque[tuple[int, int, int]] = deque([(start, end, 0)]) + rows: list[dict[str, Any]] = [] + unresolved: list[dict[str, Any]] = [] + while queue: + left, right, depth = queue.popleft() + payload = dict(filter_base) + payload.update({"fromBlock": hex(left), "toBlock": hex(right)}) + try: + result = client.call("eth_getLogs", [payload]) + if not isinstance(result, list): + raise RuntimeError(f"eth_getLogs non-list {type(result)}") + rows.extend(row for row in result if isinstance(row, dict)) + except Exception as exc: + if left < right and depth < 28: + mid = (left + right) // 2 + queue.appendleft((mid + 1, right, depth + 1)) + queue.appendleft((left, mid, depth + 1)) + else: + unresolved.append({"from_block": left, "to_block": right, "depth": depth, "error": repr(exc)}) + return rows, unresolved + + +def event_key(row: dict[str, Any]) -> tuple[str, str, str]: + return ( + str(row.get("blockHash") or "").lower(), + str(row.get("transactionHash") or "").lower(), + str(row.get("logIndex") or "").lower(), + ) + + +def decode_uint_array(data_words: list[str], offset_word: int) -> list[int]: + length = int(data_words[offset_word], 16) + return [int(data_words[offset_word + 1 + index], 16) for index in range(length)] + + +def normalize_log(wallet: str, direction: str, standard: str, row: dict[str, Any]) -> dict[str, Any]: + topics = [str(value).lower() for value in row.get("topics") or []] + data = str(row.get("data") or "0x").removeprefix("0x") + output: dict[str, Any] = { + "wallet": wallet, + "direction": direction, + "token_standard": standard, + "contract_address": str(row.get("address") or "").lower(), + "block_number": h2i(row.get("blockNumber")), + "block_hash": str(row.get("blockHash") or "").lower(), + "transaction_hash": str(row.get("transactionHash") or "").lower(), + "transaction_index": h2i(row.get("transactionIndex")), + "log_index": h2i(row.get("logIndex")), + "removed": bool(row.get("removed")), + "from_address": None, + "to_address": None, + "operator": None, + "token_id": None, + "amount": None, + "token_ids": None, + "amounts": None, + } + if standard == "ERC721": + if len(topics) != 4: + raise ValueError("ERC721 Transfer must have four topics") + output.update({ + "from_address": topic_address(topics[1]), + "to_address": topic_address(topics[2]), + "token_id": str(int(topics[3], 16)), + "amount": "1", + }) + elif standard == "ERC1155_SINGLE": + if len(topics) != 4 or len(data) != 128: + raise ValueError("ERC1155 TransferSingle layout mismatch") + output.update({ + "operator": topic_address(topics[1]), + "from_address": topic_address(topics[2]), + "to_address": topic_address(topics[3]), + "token_id": str(int(data[:64], 16)), + "amount": str(int(data[64:128], 16)), + }) + elif standard == "ERC1155_BATCH": + if len(topics) != 4 or len(data) % 64: + raise ValueError("ERC1155 TransferBatch layout mismatch") + data_words = [data[index:index + 64] for index in range(0, len(data), 64)] + ids_offset = int(data_words[0], 16) // 32 + amounts_offset = int(data_words[1], 16) // 32 + ids = decode_uint_array(data_words, ids_offset) + amounts = decode_uint_array(data_words, amounts_offset) + if len(ids) != len(amounts): + raise ValueError("ERC1155 ids/amounts length mismatch") + output.update({ + "operator": topic_address(topics[1]), + "from_address": topic_address(topics[2]), + "to_address": topic_address(topics[3]), + "token_ids": [str(value) for value in ids], + "amounts": [str(value) for value in amounts], + }) + return output + + +def batch_fetch(client: RpcClient, method: str, values: list[Any], size: int) -> dict[Any, Any]: + output: dict[Any, Any] = {} + for start in range(0, len(values), size): + chunk = values[start:start + size] + if method == "eth_getBlockByNumber": + calls = [(method, [hex(int(value)), False]) for value in chunk] + else: + calls = [(method, [value]) for value in chunk] + output.update(zip(chunk, client.batch(calls))) + return output + + +def load_all_candidates() -> list[dict[str, Any]]: + encoded = Path("wallet-verification/wallets.json.gz.b64").read_text().strip() + p01 = json.loads(gzip.decompress(base64.b64decode(encoded))) + p2_addresses = [ + line.strip().lower() + for line in Path("wallet-verification/p2_addresses.txt").read_text().splitlines() + if line.strip() + ] + rows: dict[str, dict[str, Any]] = {} + for row in p01: + wallet = row["wallet"].lower() + rows[wallet] = row + for wallet in p2_addresses: + rows.setdefault(wallet, { + "wallet": wallet, + "priority": "P2", + "verification_reasons": ["FIXED_P2_VERIFICATION_QUEUE"], + "known_mints": [], + "known_sales": [], + }) + return [rows[key] for key in sorted(rows)] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--shard", type=int, required=True) + parser.add_argument("--shard-count", type=int, default=16) + parser.add_argument("--out", required=True) + args = parser.parse_args() + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + all_rows = load_all_candidates() + selected = [row for index, row in enumerate(all_rows) if index % args.shard_count == args.shard] + client = RpcClient() + if h2i(client.call("eth_chainId", [])) != CHAIN_ID: + raise RuntimeError("Wrong chain") + head = h2i(client.call("eth_blockNumber", [])) + assert head is not None + raw_logs: list[dict[str, Any]] = [] + normalized: list[dict[str, Any]] = [] + unresolved: list[dict[str, Any]] = [] + summaries: list[dict[str, Any]] = [] + decode_errors: list[dict[str, Any]] = [] + query_specs = [ + ("ERC721", "incoming", {"topics": [ERC721_TRANSFER, None, None]}), + ("ERC721", "outgoing", {"topics": [ERC721_TRANSFER, None, None]}), + ("ERC1155_SINGLE", "incoming", {"topics": [ERC1155_SINGLE, None, None, None]}), + ("ERC1155_SINGLE", "outgoing", {"topics": [ERC1155_SINGLE, None, None, None]}), + ("ERC1155_BATCH", "incoming", {"topics": [ERC1155_BATCH, None, None, None]}), + ("ERC1155_BATCH", "outgoing", {"topics": [ERC1155_BATCH, None, None, None]}), + ] + for item in selected: + wallet = item["wallet"].lower() + wallet_raw: dict[tuple[str, str, str], dict[str, Any]] = {} + wallet_unresolved: list[dict[str, Any]] = [] + for standard, direction, base in query_specs: + current = json.loads(json.dumps(base)) + wallet_topic = padded(wallet) + if standard == "ERC721": + current["topics"][2 if direction == "incoming" else 1] = wallet_topic + else: + current["topics"][3 if direction == "incoming" else 2] = wallet_topic + rows, errors = recursive_logs(client, current, 0, head) + for row in rows: + wallet_raw[event_key(row)] = row + try: + normalized.append(normalize_log(wallet, direction, standard, row)) + except Exception as exc: + decode_errors.append({"wallet": wallet, "direction": direction, "standard": standard, "error": repr(exc), "raw": row}) + for error in errors: + wallet_unresolved.append({"wallet": wallet, "direction": direction, "standard": standard, **error}) + raw_logs.extend({"wallet": wallet, "raw": row} for row in wallet_raw.values()) + unresolved.extend(wallet_unresolved) + wallet_rows = [row for row in normalized if row["wallet"] == wallet] + summaries.append({ + "wallet": wallet, + "priority": item.get("priority"), + "verification_reasons": item.get("verification_reasons") or [], + "fixed_head_block": head, + "canonical_transfer_rows": len(wallet_rows), + "erc721_rows": sum(row["token_standard"] == "ERC721" for row in wallet_rows), + "erc1155_single_rows": sum(row["token_standard"] == "ERC1155_SINGLE" for row in wallet_rows), + "erc1155_batch_rows": sum(row["token_standard"] == "ERC1155_BATCH" for row in wallet_rows), + "incoming_rows": sum(row["direction"] == "incoming" for row in wallet_rows), + "outgoing_rows": sum(row["direction"] == "outgoing" for row in wallet_rows), + "unique_contracts": len({row["contract_address"] for row in wallet_rows}), + "unique_transactions": len({row["transaction_hash"] for row in wallet_rows}), + "zero_address_mint_rows": sum(row.get("from_address") == ZERO and row["direction"] == "incoming" for row in wallet_rows), + "burn_rows": sum(row.get("to_address") == ZERO and row["direction"] == "outgoing" for row in wallet_rows), + "unresolved_ranges": len(wallet_unresolved), + "activity_status": "RHC_NFT_ACTIVITY_PRESENT" if wallet_rows else "NO_RHC_NFT_TRANSFER_AT_FIXED_HEAD", + "strength_status": "NOT_EVALUATED", + "production_approved": False, + }) + print({"wallet": wallet, "rows": len(wallet_rows), "unresolved": len(wallet_unresolved)}, flush=True) + unique_txs = sorted({row["transaction_hash"] for row in normalized if row.get("transaction_hash")}) + unique_blocks = sorted({row["block_number"] for row in normalized if row.get("block_number") is not None}) + transactions = batch_fetch(client, "eth_getTransactionByHash", unique_txs, 15) + receipts = batch_fetch(client, "eth_getTransactionReceipt", unique_txs, 12) + blocks = batch_fetch(client, "eth_getBlockByNumber", unique_blocks, 25) + timestamp_map = { + block: h2i(value.get("timestamp")) if isinstance(value, dict) else None + for block, value in blocks.items() + } + for row in normalized: + timestamp = timestamp_map.get(row["block_number"]) + row["block_timestamp_unix"] = timestamp + row["block_timestamp_utc"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(timestamp)) if timestamp else None + write_jsonl(out / "raw_wallet_logs.jsonl", raw_logs) + write_jsonl(out / "normalized_wallet_transfers.jsonl", normalized) + write_csv(out / "normalized_wallet_transfers.csv", normalized) + write_csv(out / "wallet_summary.csv", summaries) + write_json(out / "unresolved_ranges.json", unresolved) + write_json(out / "decode_errors.json", decode_errors) + write_jsonl(out / "transactions.jsonl", [{"transaction_hash": key, "value": value} for key, value in transactions.items()]) + write_jsonl(out / "receipts.jsonl", [{"transaction_hash": key, "value": value} for key, value in receipts.items()]) + write_jsonl(out / "blocks.jsonl", [{"block_number": key, "value": value} for key, value in blocks.items()]) + failures = [] + if unresolved: + failures.append({"code": "UNRESOLVED_LOG_RANGES", "count": len(unresolved)}) + if decode_errors: + failures.append({"code": "LOG_DECODE_ERRORS", "count": len(decode_errors)}) + missing_txs = [key for key, value in transactions.items() if not isinstance(value, dict)] + missing_receipts = [key for key, value in receipts.items() if not isinstance(value, dict)] + missing_blocks = [key for key, value in blocks.items() if not isinstance(value, dict)] + if missing_txs: + failures.append({"code": "MISSING_TRANSACTIONS", "count": len(missing_txs)}) + if missing_receipts: + failures.append({"code": "MISSING_RECEIPTS", "count": len(missing_receipts)}) + if missing_blocks: + failures.append({"code": "MISSING_BLOCKS", "count": len(missing_blocks)}) + validation = { + "status": "PASS" if not failures else "FAIL", + "chain_id": CHAIN_ID, + "fixed_head_block": head, + "shard": args.shard, + "shard_count": args.shard_count, + "wallet_count": len(selected), + "wallets_with_activity": sum(row["canonical_transfer_rows"] > 0 for row in summaries), + "wallets_without_activity": sum(row["canonical_transfer_rows"] == 0 for row in summaries), + "canonical_transfer_rows": len(normalized), + "unique_transactions": len(unique_txs), + "unique_blocks": len(unique_blocks), + "rpc_request_count": client.request_count, + "rpc_retry_count": client.retry_count, + "failures": failures, + "production_approved_wallets": 0, + } + write_json(out / "VALIDATION.json", validation) + write_json(out / "input_wallets.json", selected) + manifest = [] + for path in sorted(out.iterdir()): + if path.is_file() and path.name != "MANIFEST.json": + manifest.append({"path": path.name, "bytes": path.stat().st_size, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}) + write_json(out / "MANIFEST.json", manifest) + print(json.dumps(validation, sort_keys=True), flush=True) + if failures: + raise SystemExit(1) + + +if __name__ == "__main__": + main() From eb72722d76c272de7dbafe245416ceb130082e42 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:33:13 +0900 Subject: [PATCH 25/42] ci: collect canonical NFT histories for all candidate wallets --- ...c-candidate-wallet-canonical-transfers.yml | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/rhc-candidate-wallet-canonical-transfers.yml diff --git a/.github/workflows/rhc-candidate-wallet-canonical-transfers.yml b/.github/workflows/rhc-candidate-wallet-canonical-transfers.yml new file mode 100644 index 0000000..5cac670 --- /dev/null +++ b/.github/workflows/rhc-candidate-wallet-canonical-transfers.yml @@ -0,0 +1,45 @@ +name: RHC candidate wallet canonical NFT transfers + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-candidate-wallet-canonical-transfers.yml' + - 'canonical-completion/candidate_wallet_collector.py' + - 'wallet-verification/p2_addresses.txt' + - 'wallet-verification/wallets.json.gz.b64' + +permissions: + contents: read + +concurrency: + group: rhc-candidate-wallet-canonical-transfers-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + collect: + strategy: + fail-fast: false + max-parallel: 2 + matrix: + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + runs-on: ubuntu-latest + timeout-minutes: 180 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install deterministic Keccak dependency + run: python -m pip install --disable-pip-version-check --no-input pycryptodome==3.23.0 + - name: Collect canonical wallet NFT histories + run: python canonical-completion/candidate_wallet_collector.py --shard '${{ matrix.shard }}' --shard-count 16 --out 'out-wallet-${{ matrix.shard }}' + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-canonical-wallet-transfers-${{ matrix.shard }} + path: out-wallet-${{ matrix.shard }}/ + retention-days: 7 + if-no-files-found: error + compression-level: 9 From 667b5dcbe833e3fa0b616e75702759c4defa230d Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:34:28 +0900 Subject: [PATCH 26/42] data: add full zero-address NFT mint universe collector --- canonical-completion/zero_mint_collector.py | 372 ++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 canonical-completion/zero_mint_collector.py diff --git a/canonical-completion/zero_mint_collector.py b/canonical-completion/zero_mint_collector.py new file mode 100644 index 0000000..c9df67f --- /dev/null +++ b/canonical-completion/zero_mint_collector.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import random +import time +import urllib.error +import urllib.request +from collections import deque +from pathlib import Path +from typing import Any + +RPC_URL = "https://rpc.mainnet.chain.robinhood.com" +CHAIN_ID = 4663 +USER_AGENT = "RHC-Zero-Mint-Universe/1.0" +ZERO_TOPIC = "0x" + "0" * 64 +ERC721_TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + +def keccak256(text: str) -> str: + from Crypto.Hash import keccak + digest = keccak.new(digest_bits=256) + digest.update(text.encode("utf-8")) + return "0x" + digest.hexdigest() + + +TARGETS = { + "erc721": {"topic0": ERC721_TRANSFER, "topics": [ERC721_TRANSFER, ZERO_TOPIC]}, + "erc1155_single": { + "topic0": keccak256("TransferSingle(address,address,address,uint256,uint256)"), + "topics": [keccak256("TransferSingle(address,address,address,uint256,uint256)"), None, ZERO_TOPIC], + }, + "erc1155_batch": { + "topic0": keccak256("TransferBatch(address,address,address,uint256[],uint256[])"), + "topics": [keccak256("TransferBatch(address,address,address,uint256[],uint256[])"), None, ZERO_TOPIC], + }, +} + + +def h2i(value: Any) -> int | None: + if value is None: + return None + if isinstance(value, int): + return value + if isinstance(value, str): + return int(value, 16) if value.startswith("0x") else int(value) + return int(value) + + +def topic_address(value: str) -> str: + return "0x" + value.removeprefix("0x")[-40:].lower() + + +def write_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + + +def write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8", newline="\n") as handle: + for row in rows: + handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n") + + +def write_csv(path: Path, rows: list[dict[str, Any]]) -> None: + fields = sorted({key for row in rows for key in row}) if rows else ["empty"] + with path.open("w", newline="", encoding="utf-8-sig") as handle: + writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore") + writer.writeheader() + for row in rows: + writer.writerow({ + key: json.dumps(value, ensure_ascii=False, sort_keys=True) + if isinstance(value, (dict, list, tuple)) else value + for key, value in row.items() + }) + + +class RpcClient: + def __init__(self, min_interval: float = 0.9): + self.min_interval = min_interval + self.last_request = 0.0 + self.request_count = 0 + self.retry_count = 0 + + def pace(self) -> None: + wait = self.min_interval - (time.monotonic() - self.last_request) + if wait > 0: + time.sleep(wait) + + def request(self, payload: Any, attempts: int = 12) -> Any: + last_error: Exception | None = None + for attempt in range(attempts): + self.pace() + request = urllib.request.Request( + RPC_URL, + data=json.dumps(payload).encode("utf-8"), + method="POST", + headers={"content-type": "application/json", "accept": "application/json", "user-agent": USER_AGENT}, + ) + try: + with urllib.request.urlopen(request, timeout=180) as response: + body = response.read() + self.last_request = time.monotonic() + self.request_count += 1 + return json.loads(body.decode("utf-8")) + except urllib.error.HTTPError as exc: + self.last_request = time.monotonic() + body = exc.read(3000).decode("utf-8", "replace") + last_error = RuntimeError(f"HTTP {exc.code}: {body}") + if exc.code == 429 or exc.code >= 500: + self.retry_count += 1 + time.sleep(min(120, 4 * (2 ** min(attempt, 5)) + random.random() * 7)) + continue + raise last_error + except Exception as exc: + self.last_request = time.monotonic() + last_error = exc + self.retry_count += 1 + if attempt + 1 < attempts: + time.sleep(min(90, 3 * (2 ** min(attempt, 5)) + random.random() * 5)) + continue + break + raise RuntimeError(f"RPC failed after {attempts} attempts: {last_error}") + + def call(self, method: str, params: list[Any]) -> Any: + response = self.request({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}) + if not isinstance(response, dict): + raise RuntimeError(f"Unexpected RPC response: {type(response)}") + if response.get("error") is not None: + raise RuntimeError(json.dumps(response["error"], sort_keys=True)) + return response.get("result") + + def batch(self, calls: list[tuple[str, list[Any]]]) -> list[Any]: + payload = [ + {"jsonrpc": "2.0", "id": index, "method": method, "params": params} + for index, (method, params) in enumerate(calls) + ] + response = self.request(payload) + if not isinstance(response, list): + return [self.call(method, params) for method, params in calls] + indexed = {int(item.get("id")): item for item in response if isinstance(item, dict)} + output = [] + for index, (method, params) in enumerate(calls): + item = indexed.get(index) + if item is None or item.get("error") is not None: + output.append(self.call(method, params)) + else: + output.append(item.get("result")) + return output + + +def collect(client: RpcClient, topics: list[Any], head: int, chunk: int = 200_000) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + queue: deque[tuple[int, int, int]] = deque( + (start, min(head, start + chunk - 1), 0) for start in range(0, head + 1, chunk) + ) + rows: list[dict[str, Any]] = [] + unresolved: list[dict[str, Any]] = [] + completed = 0 + while queue: + left, right, depth = queue.popleft() + try: + result = client.call("eth_getLogs", [{"fromBlock": hex(left), "toBlock": hex(right), "topics": topics}]) + if not isinstance(result, list): + raise RuntimeError(f"eth_getLogs non-list {type(result)}") + rows.extend(row for row in result if isinstance(row, dict)) + completed += 1 + if completed % 25 == 0: + print({"completed_ranges": completed, "queued": len(queue), "events": len(rows)}, flush=True) + except Exception as exc: + if left < right and depth < 28: + middle = (left + right) // 2 + queue.appendleft((middle + 1, right, depth + 1)) + queue.appendleft((left, middle, depth + 1)) + else: + unresolved.append({"from_block": left, "to_block": right, "depth": depth, "error": repr(exc)}) + return rows, unresolved + + +def event_key(row: dict[str, Any]) -> tuple[str, str, str]: + return ( + str(row.get("blockHash") or "").lower(), + str(row.get("transactionHash") or "").lower(), + str(row.get("logIndex") or "").lower(), + ) + + +def uint_array(words: list[str], offset: int) -> list[int]: + index = offset // 32 + count = int(words[index], 16) + return [int(words[index + 1 + item], 16) for item in range(count)] + + +def decode(target: str, row: dict[str, Any]) -> dict[str, Any]: + topics = [str(value).lower() for value in row.get("topics") or []] + data = str(row.get("data") or "0x").removeprefix("0x") + base = { + "chain_id": CHAIN_ID, + "token_standard": target.upper(), + "contract_address": str(row.get("address") or "").lower(), + "block_number": h2i(row.get("blockNumber")), + "block_hash": str(row.get("blockHash") or "").lower(), + "transaction_hash": str(row.get("transactionHash") or "").lower(), + "transaction_index": h2i(row.get("transactionIndex")), + "log_index": h2i(row.get("logIndex")), + "removed": bool(row.get("removed")), + "operator": None, + "recipient": None, + "token_id": None, + "amount": None, + "token_ids": None, + "amounts": None, + } + if target == "erc721": + if len(topics) != 4: + raise ValueError("ERC721 mint layout mismatch") + base.update({"recipient": topic_address(topics[2]), "token_id": str(int(topics[3], 16)), "amount": "1"}) + elif target == "erc1155_single": + if len(topics) != 4 or len(data) != 128: + raise ValueError("ERC1155 single layout mismatch") + base.update({ + "operator": topic_address(topics[1]), + "recipient": topic_address(topics[3]), + "token_id": str(int(data[:64], 16)), + "amount": str(int(data[64:128], 16)), + }) + else: + if len(topics) != 4 or len(data) % 64: + raise ValueError("ERC1155 batch layout mismatch") + words = [data[index:index + 64] for index in range(0, len(data), 64)] + ids = uint_array(words, int(words[0], 16)) + amounts = uint_array(words, int(words[1], 16)) + if len(ids) != len(amounts): + raise ValueError("ERC1155 batch ids/amounts mismatch") + base.update({ + "operator": topic_address(topics[1]), + "recipient": topic_address(topics[3]), + "token_ids": [str(value) for value in ids], + "amounts": [str(value) for value in amounts], + }) + return base + + +def batch_fetch(client: RpcClient, method: str, values: list[Any], size: int) -> dict[Any, Any]: + output: dict[Any, Any] = {} + for start in range(0, len(values), size): + chunk = values[start:start + size] + calls = [(method, [hex(int(value)), False] if method == "eth_getBlockByNumber" else [value]) for value in chunk] + output.update(zip(chunk, client.batch(calls))) + return output + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--target", choices=sorted(TARGETS), required=True) + parser.add_argument("--out", required=True) + args = parser.parse_args() + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + client = RpcClient() + if h2i(client.call("eth_chainId", [])) != CHAIN_ID: + raise RuntimeError("Wrong chain") + head = h2i(client.call("eth_blockNumber", [])) + assert head is not None + raw, unresolved = collect(client, TARGETS[args.target]["topics"], head) + unique: dict[tuple[str, str, str], dict[str, Any]] = {} + duplicate_count = 0 + for row in raw: + key = event_key(row) + if key in unique: + duplicate_count += 1 + unique[key] = row + logs = sorted(unique.values(), key=lambda row: (h2i(row.get("blockNumber")) or -1, h2i(row.get("logIndex")) or -1)) + decoded = [] + decode_errors = [] + for row in logs: + try: + decoded.append(decode(args.target, row)) + except Exception as exc: + decode_errors.append({"event_key": event_key(row), "error": repr(exc), "raw": row}) + tx_hashes = sorted({row["transaction_hash"] for row in decoded}) + block_numbers = sorted({row["block_number"] for row in decoded if row["block_number"] is not None}) + transactions = batch_fetch(client, "eth_getTransactionByHash", tx_hashes, 15) + receipts = batch_fetch(client, "eth_getTransactionReceipt", tx_hashes, 12) + blocks = batch_fetch(client, "eth_getBlockByNumber", block_numbers, 25) + timestamp_map = {block: h2i(value.get("timestamp")) if isinstance(value, dict) else None for block, value in blocks.items()} + for row in decoded: + timestamp = timestamp_map.get(row["block_number"]) + row["block_timestamp_unix"] = timestamp + row["block_timestamp_utc"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(timestamp)) if timestamp else None + write_jsonl(out / "raw_zero_mint_logs.jsonl", logs) + write_jsonl(out / "decoded_zero_mints.jsonl", decoded) + write_csv(out / "decoded_zero_mints.csv", decoded) + write_jsonl(out / "transactions.jsonl", [{"transaction_hash": key, "value": value} for key, value in transactions.items()]) + write_jsonl(out / "receipts.jsonl", [{"transaction_hash": key, "value": value} for key, value in receipts.items()]) + write_jsonl(out / "blocks.jsonl", [{"block_number": key, "value": value} for key, value in blocks.items()]) + write_json(out / "unresolved_ranges.json", unresolved) + write_json(out / "decode_errors.json", decode_errors) + projects: dict[str, dict[str, Any]] = {} + for row in decoded: + project = projects.setdefault(row["contract_address"], { + "contract_address": row["contract_address"], + "token_standard": row["token_standard"], + "first_mint_block": row["block_number"], + "last_mint_block": row["block_number"], + "mint_event_rows": 0, + "minted_quantity": 0, + "unique_recipients": set(), + "unique_transactions": set(), + }) + project["first_mint_block"] = min(project["first_mint_block"], row["block_number"]) + project["last_mint_block"] = max(project["last_mint_block"], row["block_number"]) + project["mint_event_rows"] += 1 + if row.get("amount") is not None: + project["minted_quantity"] += int(row["amount"]) + elif row.get("amounts"): + project["minted_quantity"] += sum(int(value) for value in row["amounts"]) + project["unique_recipients"].add(row["recipient"]) + project["unique_transactions"].add(row["transaction_hash"]) + project_rows = [] + for value in projects.values(): + project_rows.append({ + **{key: item for key, item in value.items() if key not in ("unique_recipients", "unique_transactions")}, + "unique_recipients": len(value["unique_recipients"]), + "unique_transactions": len(value["unique_transactions"]), + }) + write_csv(out / "project_mint_population.csv", project_rows) + failures = [] + if unresolved: + failures.append({"code": "UNRESOLVED_BLOCK_RANGES", "count": len(unresolved)}) + if decode_errors: + failures.append({"code": "DECODE_ERRORS", "count": len(decode_errors)}) + missing_txs = [key for key, value in transactions.items() if not isinstance(value, dict)] + missing_receipts = [key for key, value in receipts.items() if not isinstance(value, dict)] + missing_blocks = [key for key, value in blocks.items() if not isinstance(value, dict)] + if missing_txs: + failures.append({"code": "MISSING_TRANSACTIONS", "count": len(missing_txs)}) + if missing_receipts: + failures.append({"code": "MISSING_RECEIPTS", "count": len(missing_receipts)}) + if missing_blocks: + failures.append({"code": "MISSING_BLOCKS", "count": len(missing_blocks)}) + validation = { + "status": "PASS" if not failures else "FAIL", + "target": args.target, + "chain_id": CHAIN_ID, + "fixed_head_block": head, + "raw_rows_before_dedup": len(raw), + "duplicate_rows_removed": duplicate_count, + "canonical_mint_event_rows": len(decoded), + "unique_contracts": len(project_rows), + "unique_transactions": len(tx_hashes), + "unresolved_ranges": len(unresolved), + "rpc_request_count": client.request_count, + "rpc_retry_count": client.retry_count, + "failures": failures, + } + write_json(out / "VALIDATION.json", validation) + manifest = [] + for path in sorted(out.iterdir()): + if path.is_file() and path.name != "MANIFEST.json": + manifest.append({"path": path.name, "bytes": path.stat().st_size, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}) + write_json(out / "MANIFEST.json", manifest) + print(json.dumps(validation, sort_keys=True), flush=True) + if failures: + raise SystemExit(1) + + +if __name__ == "__main__": + main() From ccebe8f5dc8d94c3b3701de0d5425596c20e7ea4 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:34:40 +0900 Subject: [PATCH 27/42] ci: collect full zero-address NFT mint universe --- .github/workflows/rhc-zero-mint-universe.yml | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/rhc-zero-mint-universe.yml diff --git a/.github/workflows/rhc-zero-mint-universe.yml b/.github/workflows/rhc-zero-mint-universe.yml new file mode 100644 index 0000000..7a4d6a2 --- /dev/null +++ b/.github/workflows/rhc-zero-mint-universe.yml @@ -0,0 +1,43 @@ +name: RHC canonical zero-address NFT mint universe + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-zero-mint-universe.yml' + - 'canonical-completion/zero_mint_collector.py' + +permissions: + contents: read + +concurrency: + group: rhc-zero-mint-universe-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + collect: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + target: [erc721, erc1155_single, erc1155_batch] + runs-on: ubuntu-latest + timeout-minutes: 240 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install deterministic Keccak dependency + run: python -m pip install --disable-pip-version-check --no-input pycryptodome==3.23.0 + - name: Collect canonical zero-address mint universe + run: python canonical-completion/zero_mint_collector.py --target '${{ matrix.target }}' --out 'out-${{ matrix.target }}' + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-zero-mints-${{ matrix.target }} + path: out-${{ matrix.target }}/ + retention-days: 7 + if-no-files-found: error + compression-level: 9 From 33bd45bf667c55c3d617efc86ee1c5758ae3b8c2 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:39:51 +0900 Subject: [PATCH 28/42] fix: exclude ERC20 Transfer logs from NFT collectors --- canonical-completion/run_corrected.py | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 canonical-completion/run_corrected.py diff --git a/canonical-completion/run_corrected.py b/canonical-completion/run_corrected.py new file mode 100644 index 0000000..da825fd --- /dev/null +++ b/canonical-completion/run_corrected.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + + +def patch_candidate(source: str) -> str: + old = ''' for row in rows:\n wallet_raw[event_key(row)] = row\n try:\n normalized.append(normalize_log(wallet, direction, standard, row))\n''' + new = ''' for row in rows:\n # ERC-20 Transfer shares the ERC-721 signature but has only\n # three topics. It is not an NFT decode failure.\n if standard == "ERC721" and len(row.get("topics") or []) != 4:\n continue\n wallet_raw[event_key(row)] = row\n try:\n normalized.append(normalize_log(wallet, direction, standard, row))\n''' + if old not in source: + raise RuntimeError("candidate patch anchor not found") + return source.replace(old, new, 1) + + +def patch_zero(source: str) -> str: + old = ''' logs = sorted(unique.values(), key=lambda row: (h2i(row.get("blockNumber")) or -1, h2i(row.get("logIndex")) or -1))\n decoded = []\n''' + new = ''' logs = sorted(unique.values(), key=lambda row: (h2i(row.get("blockNumber")) or -1, h2i(row.get("logIndex")) or -1))\n # ERC-20 mint events share Transfer(address,address,uint256) and match\n # topic1=zero. Only four-topic Transfer logs are ERC-721 token mints.\n if args.target == "erc721":\n logs = [row for row in logs if len(row.get("topics") or []) == 4]\n decoded = []\n''' + if old not in source: + raise RuntimeError("zero-mint patch anchor not found") + return source.replace(old, new, 1) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--kind", choices=["candidate", "zero"], required=True) + parser.add_argument("args", nargs=argparse.REMAINDER) + options = parser.parse_args() + original = Path( + "canonical-completion/candidate_wallet_collector.py" + if options.kind == "candidate" + else "canonical-completion/zero_mint_collector.py" + ) + source = original.read_text(encoding="utf-8") + patched = patch_candidate(source) if options.kind == "candidate" else patch_zero(source) + runtime = Path("runtime") + runtime.mkdir(exist_ok=True) + target = runtime / original.name + target.write_text(patched, encoding="utf-8") + subprocess.run([sys.executable, str(target), *options.args], check=True) + + +if __name__ == "__main__": + main() From da8d7f95c6c3f2818d20be12411ed3fa1b4c1bb0 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:40:05 +0900 Subject: [PATCH 29/42] ci: run corrected canonical candidate wallet collector --- ...andidate-wallet-canonical-transfers-v2.yml | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/rhc-candidate-wallet-canonical-transfers-v2.yml diff --git a/.github/workflows/rhc-candidate-wallet-canonical-transfers-v2.yml b/.github/workflows/rhc-candidate-wallet-canonical-transfers-v2.yml new file mode 100644 index 0000000..bfda3b6 --- /dev/null +++ b/.github/workflows/rhc-candidate-wallet-canonical-transfers-v2.yml @@ -0,0 +1,45 @@ +name: RHC candidate wallet canonical NFT transfers v2 + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-candidate-wallet-canonical-transfers-v2.yml' + - 'canonical-completion/run_corrected.py' + - 'canonical-completion/candidate_wallet_collector.py' + - 'wallet-verification/p2_addresses.txt' + - 'wallet-verification/wallets.json.gz.b64' + +permissions: + contents: read + +concurrency: + group: rhc-candidate-wallet-canonical-transfers-v2-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + collect: + strategy: + fail-fast: false + max-parallel: 2 + matrix: + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + runs-on: ubuntu-latest + timeout-minutes: 180 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python -m pip install --disable-pip-version-check --no-input pycryptodome==3.23.0 + - name: Collect corrected canonical wallet NFT histories + run: python canonical-completion/run_corrected.py --kind candidate -- --shard '${{ matrix.shard }}' --shard-count 16 --out 'out-wallet-${{ matrix.shard }}' + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-canonical-wallet-transfers-v2-${{ matrix.shard }} + path: out-wallet-${{ matrix.shard }}/ + retention-days: 7 + if-no-files-found: error + compression-level: 9 From 648a8533989226ce23a231a5987bc56bd1adc1c3 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:40:22 +0900 Subject: [PATCH 30/42] ci: run corrected zero-address NFT mint universe collector --- .../workflows/rhc-zero-mint-universe-v2.yml | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/rhc-zero-mint-universe-v2.yml diff --git a/.github/workflows/rhc-zero-mint-universe-v2.yml b/.github/workflows/rhc-zero-mint-universe-v2.yml new file mode 100644 index 0000000..8052111 --- /dev/null +++ b/.github/workflows/rhc-zero-mint-universe-v2.yml @@ -0,0 +1,43 @@ +name: RHC canonical zero-address NFT mint universe v2 + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-zero-mint-universe-v2.yml' + - 'canonical-completion/run_corrected.py' + - 'canonical-completion/zero_mint_collector.py' + +permissions: + contents: read + +concurrency: + group: rhc-zero-mint-universe-v2-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + collect: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + target: [erc721, erc1155_single, erc1155_batch] + runs-on: ubuntu-latest + timeout-minutes: 240 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python -m pip install --disable-pip-version-check --no-input pycryptodome==3.23.0 + - name: Collect corrected canonical zero-address mint universe + run: python canonical-completion/run_corrected.py --kind zero -- --target '${{ matrix.target }}' --out 'out-${{ matrix.target }}' + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-zero-mints-v2-${{ matrix.target }} + path: out-${{ matrix.target }}/ + retention-days: 7 + if-no-files-found: error + compression-level: 9 From 4b5c147c3f2540ad6005a2c77a62fab25f69f858 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:44:56 +0900 Subject: [PATCH 31/42] fix: forward corrected collector arguments safely --- canonical-completion/run_corrected_v2.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 canonical-completion/run_corrected_v2.py diff --git a/canonical-completion/run_corrected_v2.py b/canonical-completion/run_corrected_v2.py new file mode 100644 index 0000000..e8b15ef --- /dev/null +++ b/canonical-completion/run_corrected_v2.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + + +def patch_candidate(source: str) -> str: + anchor = ''' for row in rows:\n wallet_raw[event_key(row)] = row\n try:\n normalized.append(normalize_log(wallet, direction, standard, row))\n''' + replacement = ''' for row in rows:\n # ERC-20 Transfer shares the ERC-721 signature but has only\n # three topics. Ignore it rather than treating it as a failed\n # ERC-721 decode.\n if standard == "ERC721" and len(row.get("topics") or []) != 4:\n continue\n wallet_raw[event_key(row)] = row\n try:\n normalized.append(normalize_log(wallet, direction, standard, row))\n''' + if anchor not in source: + raise RuntimeError("candidate patch anchor not found") + return source.replace(anchor, replacement, 1) + + +def patch_zero(source: str) -> str: + anchor = ''' logs = sorted(unique.values(), key=lambda row: (h2i(row.get("blockNumber")) or -1, h2i(row.get("logIndex")) or -1))\n decoded = []\n''' + replacement = ''' logs = sorted(unique.values(), key=lambda row: (h2i(row.get("blockNumber")) or -1, h2i(row.get("logIndex")) or -1))\n # ERC-20 mint events share the Transfer signature and topic1=zero.\n # Four topics are required for an ERC-721 token ID.\n if args.target == "erc721":\n logs = [row for row in logs if len(row.get("topics") or []) == 4]\n decoded = []\n''' + if anchor not in source: + raise RuntimeError("zero-mint patch anchor not found") + return source.replace(anchor, replacement, 1) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--kind", choices=["candidate", "zero"], required=True) + parser.add_argument("args", nargs=argparse.REMAINDER) + options = parser.parse_args() + forwarded = list(options.args) + if forwarded and forwarded[0] == "--": + forwarded = forwarded[1:] + original = Path( + "canonical-completion/candidate_wallet_collector.py" + if options.kind == "candidate" + else "canonical-completion/zero_mint_collector.py" + ) + source = original.read_text(encoding="utf-8") + patched = patch_candidate(source) if options.kind == "candidate" else patch_zero(source) + runtime = Path("runtime") + runtime.mkdir(exist_ok=True) + target = runtime / original.name + target.write_text(patched, encoding="utf-8") + subprocess.run([sys.executable, str(target), *forwarded], check=True) + + +if __name__ == "__main__": + main() From 8152ffdf67cf0d7850177610f8a90ede327c2d84 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:45:15 +0900 Subject: [PATCH 32/42] ci: run corrected candidate wallet collector v3 --- ...andidate-wallet-canonical-transfers-v3.yml | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/rhc-candidate-wallet-canonical-transfers-v3.yml diff --git a/.github/workflows/rhc-candidate-wallet-canonical-transfers-v3.yml b/.github/workflows/rhc-candidate-wallet-canonical-transfers-v3.yml new file mode 100644 index 0000000..6acf8fa --- /dev/null +++ b/.github/workflows/rhc-candidate-wallet-canonical-transfers-v3.yml @@ -0,0 +1,45 @@ +name: RHC candidate wallet canonical NFT transfers v3 + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-candidate-wallet-canonical-transfers-v3.yml' + - 'canonical-completion/run_corrected_v2.py' + - 'canonical-completion/candidate_wallet_collector.py' + - 'wallet-verification/p2_addresses.txt' + - 'wallet-verification/wallets.json.gz.b64' + +permissions: + contents: read + +concurrency: + group: rhc-candidate-wallet-canonical-transfers-v3-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + collect: + strategy: + fail-fast: false + max-parallel: 2 + matrix: + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + runs-on: ubuntu-latest + timeout-minutes: 180 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python -m pip install --disable-pip-version-check --no-input pycryptodome==3.23.0 + - name: Collect corrected canonical wallet NFT histories + run: python canonical-completion/run_corrected_v2.py --kind candidate -- --shard '${{ matrix.shard }}' --shard-count 16 --out 'out-wallet-${{ matrix.shard }}' + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-canonical-wallet-transfers-v3-${{ matrix.shard }} + path: out-wallet-${{ matrix.shard }}/ + retention-days: 7 + if-no-files-found: error + compression-level: 9 From 3ac4ef7bcb1c1f68f8e8280b969c098859e6cbb2 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:45:30 +0900 Subject: [PATCH 33/42] ci: run corrected zero-address mint collector v3 --- .../workflows/rhc-zero-mint-universe-v3.yml | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/rhc-zero-mint-universe-v3.yml diff --git a/.github/workflows/rhc-zero-mint-universe-v3.yml b/.github/workflows/rhc-zero-mint-universe-v3.yml new file mode 100644 index 0000000..91cc486 --- /dev/null +++ b/.github/workflows/rhc-zero-mint-universe-v3.yml @@ -0,0 +1,43 @@ +name: RHC canonical zero-address NFT mint universe v3 + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-zero-mint-universe-v3.yml' + - 'canonical-completion/run_corrected_v2.py' + - 'canonical-completion/zero_mint_collector.py' + +permissions: + contents: read + +concurrency: + group: rhc-zero-mint-universe-v3-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + collect: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + target: [erc721, erc1155_single, erc1155_batch] + runs-on: ubuntu-latest + timeout-minutes: 240 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python -m pip install --disable-pip-version-check --no-input pycryptodome==3.23.0 + - name: Collect corrected canonical zero-address mint universe + run: python canonical-completion/run_corrected_v2.py --kind zero -- --target '${{ matrix.target }}' --out 'out-${{ matrix.target }}' + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-zero-mints-v3-${{ matrix.target }} + path: out-${{ matrix.target }}/ + retention-days: 7 + if-no-files-found: error + compression-level: 9 From d543dbf5cf18d6523bb7af68b8543155de5201c1 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:52:46 +0900 Subject: [PATCH 34/42] data: add full-chain NFT transfer collector --- .../all_nft_transfer_collector.py | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 canonical-completion/all_nft_transfer_collector.py diff --git a/canonical-completion/all_nft_transfer_collector.py b/canonical-completion/all_nft_transfer_collector.py new file mode 100644 index 0000000..55bd95a --- /dev/null +++ b/canonical-completion/all_nft_transfer_collector.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import random +import time +import urllib.error +import urllib.request +from collections import deque +from pathlib import Path +from typing import Any + +RPC_URL = "https://rpc.mainnet.chain.robinhood.com" +CHAIN_ID = 4663 +USER_AGENT = "RHC-All-NFT-Transfers/1.0" +ZERO = "0x0000000000000000000000000000000000000000" +ERC721_TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + +def keccak256(text: str) -> str: + from Crypto.Hash import keccak + digest = keccak.new(digest_bits=256) + digest.update(text.encode("utf-8")) + return "0x" + digest.hexdigest() + + +TARGETS = { + "erc721": {"topics": [ERC721_TRANSFER]}, + "erc1155_single": {"topics": [keccak256("TransferSingle(address,address,address,uint256,uint256)")]}, + "erc1155_batch": {"topics": [keccak256("TransferBatch(address,address,address,uint256[],uint256[])")]}, +} + + +def h2i(value: Any) -> int | None: + if value is None: + return None + if isinstance(value, int): + return value + if isinstance(value, str): + return int(value, 16) if value.startswith("0x") else int(value) + return int(value) + + +def topic_address(value: str) -> str: + return "0x" + value.removeprefix("0x")[-40:].lower() + + +def write_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + + +def write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8", newline="\n") as handle: + for row in rows: + handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n") + + +def write_csv(path: Path, rows: list[dict[str, Any]]) -> None: + fields = sorted({key for row in rows for key in row}) if rows else ["empty"] + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", newline="", encoding="utf-8-sig") as handle: + writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore") + writer.writeheader() + for row in rows: + writer.writerow({ + key: json.dumps(value, ensure_ascii=False, sort_keys=True) + if isinstance(value, (dict, list, tuple)) else value + for key, value in row.items() + }) + + +class RpcClient: + def __init__(self, min_interval: float = 0.95): + self.min_interval = min_interval + self.last_request = 0.0 + self.request_count = 0 + self.retry_count = 0 + + def pace(self) -> None: + wait = self.min_interval - (time.monotonic() - self.last_request) + if wait > 0: + time.sleep(wait) + + def request(self, payload: Any, attempts: int = 12) -> Any: + last_error: Exception | None = None + for attempt in range(attempts): + self.pace() + req = urllib.request.Request( + RPC_URL, + data=json.dumps(payload).encode("utf-8"), + method="POST", + headers={"content-type": "application/json", "accept": "application/json", "user-agent": USER_AGENT}, + ) + try: + with urllib.request.urlopen(req, timeout=180) as response: + body = response.read() + self.last_request = time.monotonic() + self.request_count += 1 + return json.loads(body.decode("utf-8")) + except urllib.error.HTTPError as exc: + self.last_request = time.monotonic() + body = exc.read(3000).decode("utf-8", "replace") + last_error = RuntimeError(f"HTTP {exc.code}: {body}") + if exc.code == 429 or exc.code >= 500: + self.retry_count += 1 + time.sleep(min(120, 4 * (2 ** min(attempt, 5)) + random.random() * 7)) + continue + raise last_error + except Exception as exc: + self.last_request = time.monotonic() + last_error = exc + self.retry_count += 1 + if attempt + 1 < attempts: + time.sleep(min(90, 3 * (2 ** min(attempt, 5)) + random.random() * 5)) + continue + break + raise RuntimeError(f"RPC failed: {last_error}") + + def call(self, method: str, params: list[Any]) -> Any: + response = self.request({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}) + if not isinstance(response, dict): + raise RuntimeError(f"Unexpected response type {type(response)}") + if response.get("error") is not None: + raise RuntimeError(json.dumps(response["error"], sort_keys=True)) + return response.get("result") + + def batch(self, calls: list[tuple[str, list[Any]]]) -> list[Any]: + payload = [ + {"jsonrpc": "2.0", "id": i, "method": method, "params": params} + for i, (method, params) in enumerate(calls) + ] + response = self.request(payload) + if not isinstance(response, list): + return [self.call(method, params) for method, params in calls] + indexed = {int(item.get("id")): item for item in response if isinstance(item, dict)} + output: list[Any] = [] + for i, (method, params) in enumerate(calls): + item = indexed.get(i) + if item is None or item.get("error") is not None: + output.append(self.call(method, params)) + else: + output.append(item.get("result")) + return output + + +def recursive_logs(client: RpcClient, topics: list[Any], head: int, chunk: int = 150_000) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + queue: deque[tuple[int, int, int]] = deque( + (start, min(head, start + chunk - 1), 0) for start in range(0, head + 1, chunk) + ) + rows: list[dict[str, Any]] = [] + unresolved: list[dict[str, Any]] = [] + completed = 0 + while queue: + left, right, depth = queue.popleft() + try: + result = client.call("eth_getLogs", [{"fromBlock": hex(left), "toBlock": hex(right), "topics": topics}]) + if not isinstance(result, list): + raise RuntimeError(f"eth_getLogs non-list {type(result)}") + rows.extend(row for row in result if isinstance(row, dict)) + completed += 1 + if completed % 25 == 0: + print({"completed_ranges": completed, "queued": len(queue), "raw_events": len(rows)}, flush=True) + except Exception as exc: + if left < right and depth < 30: + middle = (left + right) // 2 + queue.appendleft((middle + 1, right, depth + 1)) + queue.appendleft((left, middle, depth + 1)) + else: + unresolved.append({"from_block": left, "to_block": right, "depth": depth, "error": repr(exc)}) + return rows, unresolved + + +def event_key(row: dict[str, Any]) -> tuple[str, str, str]: + return ( + str(row.get("blockHash") or "").lower(), + str(row.get("transactionHash") or "").lower(), + str(row.get("logIndex") or "").lower(), + ) + + +def uint_array(words: list[str], offset_bytes: int) -> list[int]: + index = offset_bytes // 32 + count = int(words[index], 16) + return [int(words[index + 1 + i], 16) for i in range(count)] + + +def decode(target: str, row: dict[str, Any]) -> dict[str, Any] | None: + topics = [str(value).lower() for value in row.get("topics") or []] + data = str(row.get("data") or "0x").removeprefix("0x") + base = { + "chain_id": CHAIN_ID, + "token_standard": target.upper(), + "contract_address": str(row.get("address") or "").lower(), + "block_number": h2i(row.get("blockNumber")), + "block_hash": str(row.get("blockHash") or "").lower(), + "transaction_hash": str(row.get("transactionHash") or "").lower(), + "transaction_index": h2i(row.get("transactionIndex")), + "log_index": h2i(row.get("logIndex")), + "removed": bool(row.get("removed")), + "operator": None, + "from_address": None, + "to_address": None, + "token_id": None, + "amount": None, + "token_ids": None, + "amounts": None, + } + if target == "erc721": + # ERC-20 shares the Transfer signature but has only 3 topics. + if len(topics) != 4: + return None + base.update({ + "from_address": topic_address(topics[1]), + "to_address": topic_address(topics[2]), + "token_id": str(int(topics[3], 16)), + "amount": "1", + }) + elif target == "erc1155_single": + if len(topics) != 4 or len(data) != 128: + raise ValueError("ERC1155 TransferSingle layout mismatch") + base.update({ + "operator": topic_address(topics[1]), + "from_address": topic_address(topics[2]), + "to_address": topic_address(topics[3]), + "token_id": str(int(data[:64], 16)), + "amount": str(int(data[64:128], 16)), + }) + else: + if len(topics) != 4 or len(data) % 64: + raise ValueError("ERC1155 TransferBatch layout mismatch") + words = [data[i:i + 64] for i in range(0, len(data), 64)] + ids = uint_array(words, int(words[0], 16)) + amounts = uint_array(words, int(words[1], 16)) + if len(ids) != len(amounts): + raise ValueError("ERC1155 batch mismatch") + base.update({ + "operator": topic_address(topics[1]), + "from_address": topic_address(topics[2]), + "to_address": topic_address(topics[3]), + "token_ids": [str(value) for value in ids], + "amounts": [str(value) for value in amounts], + }) + return base + + +def batch_fetch(client: RpcClient, method: str, values: list[Any], size: int) -> dict[Any, Any]: + output: dict[Any, Any] = {} + for start in range(0, len(values), size): + chunk = values[start:start + size] + calls = [(method, [hex(int(value)), False] if method == "eth_getBlockByNumber" else [value]) for value in chunk] + output.update(zip(chunk, client.batch(calls))) + if start % (size * 10) == 0: + print({"method": method, "done": min(start + len(chunk), len(values)), "total": len(values)}, flush=True) + return output + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--target", choices=sorted(TARGETS), required=True) + parser.add_argument("--out", required=True) + args = parser.parse_args() + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + client = RpcClient() + if h2i(client.call("eth_chainId", [])) != CHAIN_ID: + raise RuntimeError("Wrong chain") + head = h2i(client.call("eth_blockNumber", [])) + assert head is not None + raw, unresolved = recursive_logs(client, TARGETS[args.target]["topics"], head) + unique: dict[tuple[str, str, str], dict[str, Any]] = {} + duplicates = 0 + for row in raw: + key = event_key(row) + if key in unique: + duplicates += 1 + unique[key] = row + logs = sorted(unique.values(), key=lambda row: (h2i(row.get("blockNumber")) or -1, h2i(row.get("logIndex")) or -1)) + decoded: list[dict[str, Any]] = [] + decode_errors: list[dict[str, Any]] = [] + ignored_non_nft = 0 + for row in logs: + try: + value = decode(args.target, row) + if value is None: + ignored_non_nft += 1 + else: + decoded.append(value) + except Exception as exc: + decode_errors.append({"event_key": event_key(row), "error": repr(exc), "raw": row}) + tx_hashes = sorted({row["transaction_hash"] for row in decoded}) + block_numbers = sorted({row["block_number"] for row in decoded if row["block_number"] is not None}) + transactions = batch_fetch(client, "eth_getTransactionByHash", tx_hashes, 15) + receipts = batch_fetch(client, "eth_getTransactionReceipt", tx_hashes, 12) + blocks = batch_fetch(client, "eth_getBlockByNumber", block_numbers, 25) + timestamp_map = {block: h2i(value.get("timestamp")) if isinstance(value, dict) else None for block, value in blocks.items()} + for row in decoded: + timestamp = timestamp_map.get(row["block_number"]) + row["block_timestamp_unix"] = timestamp + row["block_timestamp_utc"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(timestamp)) if timestamp else None + row["event_kind"] = "MINT" if row.get("from_address") == ZERO else "BURN" if row.get("to_address") == ZERO else "TRANSFER" + write_jsonl(out / "raw_transfer_logs.jsonl", logs) + write_jsonl(out / "decoded_nft_transfers.jsonl", decoded) + write_csv(out / "decoded_nft_transfers.csv", decoded) + write_jsonl(out / "transactions.jsonl", [{"transaction_hash": key, "value": value} for key, value in transactions.items()]) + write_jsonl(out / "receipts.jsonl", [{"transaction_hash": key, "value": value} for key, value in receipts.items()]) + write_jsonl(out / "blocks.jsonl", [{"block_number": key, "value": value} for key, value in blocks.items()]) + write_json(out / "unresolved_ranges.json", unresolved) + write_json(out / "decode_errors.json", decode_errors) + contract_summary: dict[str, dict[str, Any]] = {} + for row in decoded: + item = contract_summary.setdefault(row["contract_address"], { + "contract_address": row["contract_address"], + "token_standard": row["token_standard"], + "first_event_block": row["block_number"], + "last_event_block": row["block_number"], + "event_rows": 0, + "mint_rows": 0, + "burn_rows": 0, + "transfer_rows": 0, + "transactions": set(), + "participants": set(), + }) + item["first_event_block"] = min(item["first_event_block"], row["block_number"]) + item["last_event_block"] = max(item["last_event_block"], row["block_number"]) + item["event_rows"] += 1 + item["mint_rows"] += row["event_kind"] == "MINT" + item["burn_rows"] += row["event_kind"] == "BURN" + item["transfer_rows"] += row["event_kind"] == "TRANSFER" + item["transactions"].add(row["transaction_hash"]) + if row.get("from_address") and row["from_address"] != ZERO: + item["participants"].add(row["from_address"]) + if row.get("to_address") and row["to_address"] != ZERO: + item["participants"].add(row["to_address"]) + summary_rows = [] + for value in contract_summary.values(): + summary_rows.append({ + **{key: item for key, item in value.items() if key not in ("transactions", "participants")}, + "unique_transactions": len(value["transactions"]), + "unique_participants": len(value["participants"]), + }) + write_csv(out / "contract_transfer_summary.csv", summary_rows) + failures = [] + if unresolved: + failures.append({"code": "UNRESOLVED_BLOCK_RANGES", "count": len(unresolved)}) + if decode_errors: + failures.append({"code": "DECODE_ERRORS", "count": len(decode_errors)}) + missing_txs = [key for key, value in transactions.items() if not isinstance(value, dict)] + missing_receipts = [key for key, value in receipts.items() if not isinstance(value, dict)] + missing_blocks = [key for key, value in blocks.items() if not isinstance(value, dict)] + if missing_txs: + failures.append({"code": "MISSING_TRANSACTIONS", "count": len(missing_txs)}) + if missing_receipts: + failures.append({"code": "MISSING_RECEIPTS", "count": len(missing_receipts)}) + if missing_blocks: + failures.append({"code": "MISSING_BLOCKS", "count": len(missing_blocks)}) + validation = { + "status": "PASS" if not failures else "FAIL", + "target": args.target, + "chain_id": CHAIN_ID, + "fixed_head_block": head, + "raw_rows_before_dedup": len(raw), + "duplicate_rows_removed": duplicates, + "ignored_non_nft_transfer_rows": ignored_non_nft, + "canonical_nft_transfer_rows": len(decoded), + "unique_contracts": len(summary_rows), + "unique_transactions": len(tx_hashes), + "unresolved_ranges": len(unresolved), + "rpc_request_count": client.request_count, + "rpc_retry_count": client.retry_count, + "failures": failures, + } + write_json(out / "VALIDATION.json", validation) + manifest = [] + for path in sorted(out.iterdir()): + if path.is_file() and path.name != "MANIFEST.json": + manifest.append({"path": path.name, "bytes": path.stat().st_size, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}) + write_json(out / "MANIFEST.json", manifest) + print(json.dumps(validation, sort_keys=True), flush=True) + if failures: + raise SystemExit(1) + + +if __name__ == "__main__": + main() From a6c158ca226dd097483350df00041864f5b98490 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:52:57 +0900 Subject: [PATCH 35/42] ci: collect full-chain canonical NFT transfers --- .github/workflows/rhc-all-nft-transfers.yml | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/rhc-all-nft-transfers.yml diff --git a/.github/workflows/rhc-all-nft-transfers.yml b/.github/workflows/rhc-all-nft-transfers.yml new file mode 100644 index 0000000..3b73a57 --- /dev/null +++ b/.github/workflows/rhc-all-nft-transfers.yml @@ -0,0 +1,42 @@ +name: RHC canonical full-chain NFT transfers + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-all-nft-transfers.yml' + - 'canonical-completion/all_nft_transfer_collector.py' + +permissions: + contents: read + +concurrency: + group: rhc-all-nft-transfers-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + collect: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + target: [erc721, erc1155_single, erc1155_batch] + runs-on: ubuntu-latest + timeout-minutes: 360 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python -m pip install --disable-pip-version-check --no-input pycryptodome==3.23.0 + - name: Collect full-chain canonical NFT transfers + run: python canonical-completion/all_nft_transfer_collector.py --target '${{ matrix.target }}' --out 'out-${{ matrix.target }}' + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-all-nft-transfers-${{ matrix.target }} + path: out-${{ matrix.target }}/ + retention-days: 7 + if-no-files-found: error + compression-level: 9 From 93a347884e9c7a8b735d545df844824420d7afd6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:53:44 +0000 Subject: [PATCH 36/42] chore: publish canonical data package [skip ci] --- .results/rhc-canonical-data-consolidated.zip | Bin 0 -> 2485 bytes ...rhc-canonical-data-consolidated.zip.sha256 | 1 + .results/rhc-canonical-package-index.json | 18 +++++++ .../DATA_COMPLETENESS.json | 43 ++++++++++++++++ .results/rhc-canonical-package/MANIFEST.json | 47 ++++++++++++++++++ .../REMEDIATION_QUEUE.csv | 1 + .../canonical_nft_transfers.csv | 1 + .../collector_errors.csv | 1 + .../rhc-canonical-package/global_events.csv | 1 + .../known_transaction_evidence.csv | 1 + .../shard_validations.json | 1 + .../rhc-canonical-package/wallet_coverage.csv | 1 + .../rhc-canonical-package/wallet_summary.csv | 1 + 13 files changed, 117 insertions(+) create mode 100644 .results/rhc-canonical-data-consolidated.zip create mode 100644 .results/rhc-canonical-data-consolidated.zip.sha256 create mode 100644 .results/rhc-canonical-package-index.json create mode 100644 .results/rhc-canonical-package/DATA_COMPLETENESS.json create mode 100644 .results/rhc-canonical-package/MANIFEST.json create mode 100644 .results/rhc-canonical-package/REMEDIATION_QUEUE.csv create mode 100644 .results/rhc-canonical-package/canonical_nft_transfers.csv create mode 100644 .results/rhc-canonical-package/collector_errors.csv create mode 100644 .results/rhc-canonical-package/global_events.csv create mode 100644 .results/rhc-canonical-package/known_transaction_evidence.csv create mode 100644 .results/rhc-canonical-package/shard_validations.json create mode 100644 .results/rhc-canonical-package/wallet_coverage.csv create mode 100644 .results/rhc-canonical-package/wallet_summary.csv diff --git a/.results/rhc-canonical-data-consolidated.zip b/.results/rhc-canonical-data-consolidated.zip new file mode 100644 index 0000000000000000000000000000000000000000..3751cc684dd7495f6c2e8abbc0f544f56d98d736 GIT binary patch literal 2485 zcmWIWW@Zs#U|`^2*tSn5HY2y{!emAUhV?8A3_3v3qKstSFx;Ux^-Vsb+u}Oxu*l_*SX}SmCaZd!W1*sH`%iRFQ#-_q7^1`er= zVM~^DWuD_a_F8v?0!NwXJ>H&x$1KZRvnQo1pWksqQKNB_+nvQNi;k|nD9V+kocdiS z?ADIO9k&j~ZQUa)ZJd|*?aihN-r~5Csiyzy^~baM?30mtdl%hvvf4Fnh-%#9aJT{q!==S{6yjGdo!gq<$_tjXhE**htqMc}`z z@{9@(oeKv~|9tYhq>a{+v06sG$~xsx&FvbL)E~; zwU6nshW6??%8fo!U&1C_xFglORsG}=mu{y&x0(flW0XsBd6KQa3A8)>-H;vn%Ic8e zSKY6*w=C0ds!q3diSBy5PPg39_Sknv#=h4g#@TN*mHz7c)D%7K+MB=q!dlxs8{6dT zW^qL=Vvk!~z@Rhz+RX*cte!7CCO>DI^U1XE-GX&0T?Z`9V+)1-bgVx_CN4Rr2Zp+a zy6PntmsRhtuFuMI^ilbMnTGQ1d^y}J8Y@x+xTPPdjP0t2)pJ~_WE zwFp=s<98r(n$jR-WN~S3Zemd-riqM9BFwn!381lHu%r=0ky2{}c%zz(UPwXAWnfs+ zco$f7VK*72$U-*|+54a@1p`YO8<=q!h&6-ZGZ>VuU|>n(Rh$On$z=G92W2f7SkhW9J&qY*$iQWF|azr9vs*+ z9lFWL2?UhoU|>mOBTkdCWI%kzg0dY9ENQ$#$XKLI7~svy2GYp|gq6T#r3U5!05)(~ A(EtDd literal 0 HcmV?d00001 diff --git a/.results/rhc-canonical-data-consolidated.zip.sha256 b/.results/rhc-canonical-data-consolidated.zip.sha256 new file mode 100644 index 0000000..0b74d6c --- /dev/null +++ b/.results/rhc-canonical-data-consolidated.zip.sha256 @@ -0,0 +1 @@ +f7c9c74bb0e84ac103298b2f5055a75a487d8a68e14e9c463443cf74db39780c rhc-canonical-data-consolidated.zip diff --git a/.results/rhc-canonical-package-index.json b/.results/rhc-canonical-package-index.json new file mode 100644 index 0000000..99c44da --- /dev/null +++ b/.results/rhc-canonical-package-index.json @@ -0,0 +1,18 @@ +{ + "bytes": 2485, + "files": [ + "DATA_COMPLETENESS.json", + "MANIFEST.json", + "REMEDIATION_QUEUE.csv", + "canonical_nft_transfers.csv", + "collector_errors.csv", + "global_events.csv", + "known_transaction_evidence.csv", + "shard_validations.json", + "wallet_coverage.csv", + "wallet_summary.csv" + ], + "package": "rhc-canonical-data-consolidated.zip", + "packager_run_id": 33221829681, + "sha256": "f7c9c74bb0e84ac103298b2f5055a75a487d8a68e14e9c463443cf74db39780c" +} \ No newline at end of file diff --git a/.results/rhc-canonical-package/DATA_COMPLETENESS.json b/.results/rhc-canonical-package/DATA_COMPLETENESS.json new file mode 100644 index 0000000..e945c86 --- /dev/null +++ b/.results/rhc-canonical-package/DATA_COMPLETENESS.json @@ -0,0 +1,43 @@ +{ + "canonical_nft_transfer_rows": 0, + "collector_error_rows": 0, + "data_collection_complete_for_wallet_verification": false, + "deepseek_handoff_allowed": false, + "expected_artifacts": 18, + "fixed_heads": { + "seadrop": null, + "seaport": null + }, + "global_event_rows": 0, + "global_event_validations": {}, + "global_history_complete": false, + "known_transaction_evidence_rows": 0, + "missing_artifacts": [ + "rhc-global-seadrop", + "rhc-global-seaport", + "rhc-canonical-wallet-0", + "rhc-canonical-wallet-1", + "rhc-canonical-wallet-2", + "rhc-canonical-wallet-3", + "rhc-canonical-wallet-4", + "rhc-canonical-wallet-5", + "rhc-canonical-wallet-6", + "rhc-canonical-wallet-7", + "rhc-canonical-wallet-8", + "rhc-canonical-wallet-9", + "rhc-canonical-wallet-10", + "rhc-canonical-wallet-11", + "rhc-canonical-wallet-12", + "rhc-canonical-wallet-13", + "rhc-canonical-wallet-14", + "rhc-canonical-wallet-15" + ], + "priority_counts": {}, + "production_approved_wallets": 0, + "remediation_rows": 0, + "status": "PARTIAL_REMEDIATION_REQUIRED", + "wallet_set_ok": false, + "wallet_status_counts": {}, + "wallets_expected": 232, + "wallets_observed": 0 +} \ No newline at end of file diff --git a/.results/rhc-canonical-package/MANIFEST.json b/.results/rhc-canonical-package/MANIFEST.json new file mode 100644 index 0000000..663652e --- /dev/null +++ b/.results/rhc-canonical-package/MANIFEST.json @@ -0,0 +1,47 @@ +[ + { + "bytes": 1199, + "path": "DATA_COMPLETENESS.json", + "sha256": "e06d1a147043f67b5349b4baf646f364371a15ebccea85922da057b6393e86ff" + }, + { + "bytes": 10, + "path": "REMEDIATION_QUEUE.csv", + "sha256": "cbf2ec508591df442cf34ebef01c3794bbe2c036cd92e251965c8de3033c9c22" + }, + { + "bytes": 10, + "path": "canonical_nft_transfers.csv", + "sha256": "cbf2ec508591df442cf34ebef01c3794bbe2c036cd92e251965c8de3033c9c22" + }, + { + "bytes": 10, + "path": "collector_errors.csv", + "sha256": "cbf2ec508591df442cf34ebef01c3794bbe2c036cd92e251965c8de3033c9c22" + }, + { + "bytes": 10, + "path": "global_events.csv", + "sha256": "cbf2ec508591df442cf34ebef01c3794bbe2c036cd92e251965c8de3033c9c22" + }, + { + "bytes": 10, + "path": "known_transaction_evidence.csv", + "sha256": "cbf2ec508591df442cf34ebef01c3794bbe2c036cd92e251965c8de3033c9c22" + }, + { + "bytes": 2, + "path": "shard_validations.json", + "sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945" + }, + { + "bytes": 10, + "path": "wallet_coverage.csv", + "sha256": "cbf2ec508591df442cf34ebef01c3794bbe2c036cd92e251965c8de3033c9c22" + }, + { + "bytes": 10, + "path": "wallet_summary.csv", + "sha256": "cbf2ec508591df442cf34ebef01c3794bbe2c036cd92e251965c8de3033c9c22" + } +] \ No newline at end of file diff --git a/.results/rhc-canonical-package/REMEDIATION_QUEUE.csv b/.results/rhc-canonical-package/REMEDIATION_QUEUE.csv new file mode 100644 index 0000000..6bfb621 --- /dev/null +++ b/.results/rhc-canonical-package/REMEDIATION_QUEUE.csv @@ -0,0 +1 @@ +empty diff --git a/.results/rhc-canonical-package/canonical_nft_transfers.csv b/.results/rhc-canonical-package/canonical_nft_transfers.csv new file mode 100644 index 0000000..6bfb621 --- /dev/null +++ b/.results/rhc-canonical-package/canonical_nft_transfers.csv @@ -0,0 +1 @@ +empty diff --git a/.results/rhc-canonical-package/collector_errors.csv b/.results/rhc-canonical-package/collector_errors.csv new file mode 100644 index 0000000..6bfb621 --- /dev/null +++ b/.results/rhc-canonical-package/collector_errors.csv @@ -0,0 +1 @@ +empty diff --git a/.results/rhc-canonical-package/global_events.csv b/.results/rhc-canonical-package/global_events.csv new file mode 100644 index 0000000..6bfb621 --- /dev/null +++ b/.results/rhc-canonical-package/global_events.csv @@ -0,0 +1 @@ +empty diff --git a/.results/rhc-canonical-package/known_transaction_evidence.csv b/.results/rhc-canonical-package/known_transaction_evidence.csv new file mode 100644 index 0000000..6bfb621 --- /dev/null +++ b/.results/rhc-canonical-package/known_transaction_evidence.csv @@ -0,0 +1 @@ +empty diff --git a/.results/rhc-canonical-package/shard_validations.json b/.results/rhc-canonical-package/shard_validations.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.results/rhc-canonical-package/shard_validations.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.results/rhc-canonical-package/wallet_coverage.csv b/.results/rhc-canonical-package/wallet_coverage.csv new file mode 100644 index 0000000..6bfb621 --- /dev/null +++ b/.results/rhc-canonical-package/wallet_coverage.csv @@ -0,0 +1 @@ +empty diff --git a/.results/rhc-canonical-package/wallet_summary.csv b/.results/rhc-canonical-package/wallet_summary.csv new file mode 100644 index 0000000..6bfb621 --- /dev/null +++ b/.results/rhc-canonical-package/wallet_summary.csv @@ -0,0 +1 @@ +empty From dee42d741c090c618419b197e6eb0f5a4f3c5416 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:54:23 +0000 Subject: [PATCH 37/42] chore: publish project opportunity universe [skip ci] --- .results/rhc-project-universe-v1-index.json | 7 +++++++ .results/rhc-project-universe-v1.zip | Bin 0 -> 22 bytes .results/rhc-project-universe-v1.zip.sha256 | 1 + 3 files changed, 8 insertions(+) create mode 100644 .results/rhc-project-universe-v1-index.json create mode 100644 .results/rhc-project-universe-v1.zip create mode 100644 .results/rhc-project-universe-v1.zip.sha256 diff --git a/.results/rhc-project-universe-v1-index.json b/.results/rhc-project-universe-v1-index.json new file mode 100644 index 0000000..2f9f669 --- /dev/null +++ b/.results/rhc-project-universe-v1-index.json @@ -0,0 +1,7 @@ +{ + "bytes": 22, + "files": [], + "package": "rhc-project-universe-v1.zip", + "publisher_run_id": 33221859357, + "sha256": "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85" +} \ No newline at end of file diff --git a/.results/rhc-project-universe-v1.zip b/.results/rhc-project-universe-v1.zip new file mode 100644 index 0000000000000000000000000000000000000000..15cb0ecb3e219d1701294bfdf0fe3f5cb5d208e7 GIT binary patch literal 22 NcmWIWW@Tf*000g10H*)| literal 0 HcmV?d00001 diff --git a/.results/rhc-project-universe-v1.zip.sha256 b/.results/rhc-project-universe-v1.zip.sha256 new file mode 100644 index 0000000..28f8e55 --- /dev/null +++ b/.results/rhc-project-universe-v1.zip.sha256 @@ -0,0 +1 @@ +8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85 rhc-project-universe-v1.zip From 809894706c3955b5eba661baaa252385ee45d412 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:04:47 +0900 Subject: [PATCH 38/42] data: add authenticated canonical artifact bundler --- canonical-completion/bundle_dependencies.py | 242 ++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 canonical-completion/bundle_dependencies.py diff --git a/canonical-completion/bundle_dependencies.py b/canonical-completion/bundle_dependencies.py new file mode 100644 index 0000000..bdafa45 --- /dev/null +++ b/canonical-completion/bundle_dependencies.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +import zipfile +from pathlib import Path +from typing import Any + +REPO = os.environ.get("GITHUB_REPOSITORY", "dcftradingcrypt/chart") +BRANCH = os.environ.get("SOURCE_BRANCH", "chatgpt/rhc-wallet-verification-p2-20260829") +TOKEN = os.environ["GITHUB_TOKEN"] +API = "https://api.github.com" +OUT = Path("out-canonical-bundle") +DOWNLOADS = OUT / "downloads" +EXTRACTED = OUT / "extracted" + +DEPENDENCIES = { + "RHC canonical global SeaDrop and Seaport histories": { + "script": "canonical-completion/global_event_collector.py", + "artifacts": {"rhc-canonical-seadrop", "rhc-canonical-seaport"}, + }, + "RHC canonical full-chain NFT transfers": { + "script": "canonical-completion/all_nft_transfer_collector.py", + "artifacts": { + "rhc-all-nft-transfers-erc721", + "rhc-all-nft-transfers-erc1155_single", + "rhc-all-nft-transfers-erc1155_batch", + }, + }, +} + + +def request_json(url: str, *, attempts: int = 8) -> Any: + last: Exception | None = None + for attempt in range(attempts): + req = urllib.request.Request( + url, + headers={ + "Authorization": f"Bearer {TOKEN}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "RHC-Canonical-Bundler/1.0", + }, + ) + try: + with urllib.request.urlopen(req, timeout=90) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + last = RuntimeError(f"HTTP {exc.code}: {exc.read(2000).decode('utf-8','replace')}") + if exc.code == 429 or exc.code >= 500: + time.sleep(min(60, 2 ** attempt + 1)) + continue + raise last + except Exception as exc: + last = exc + time.sleep(min(30, 2 ** attempt)) + raise RuntimeError(f"GET {url} failed: {last}") + + +def download(url: str, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + req = urllib.request.Request( + url, + headers={ + "Authorization": f"Bearer {TOKEN}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "RHC-Canonical-Bundler/1.0", + }, + ) + with urllib.request.urlopen(req, timeout=300) as response: + path.write_bytes(response.read()) + + +def file_sha(ref: str, path: str) -> str: + encoded = urllib.parse.quote(path, safe="/") + data = request_json(f"{API}/repos/{REPO}/contents/{encoded}?ref={urllib.parse.quote(ref, safe='')}") + return str(data["sha"]) + + +def latest_matching_run(workflow_name: str, script_path: str, current_script_sha: str) -> dict[str, Any] | None: + data = request_json( + f"{API}/repos/{REPO}/actions/runs?branch={urllib.parse.quote(BRANCH, safe='')}&status=success&per_page=100" + ) + for run in data.get("workflow_runs", []): + if run.get("name") != workflow_name or run.get("conclusion") != "success": + continue + run_sha = run.get("head_sha") + if not run_sha: + continue + try: + if file_sha(run_sha, script_path) == current_script_sha: + return run + except Exception: + continue + return None + + +def validate_extracted(name: str, root: Path) -> dict[str, Any]: + matches = list(root.rglob("VALIDATION.json")) + if len(matches) != 1: + raise RuntimeError(f"{name}: expected one VALIDATION.json, found {len(matches)}") + validation = json.loads(matches[0].read_text(encoding="utf-8")) + if validation.get("status") != "PASS": + raise RuntimeError(f"{name}: validation not PASS: {validation}") + if int(validation.get("unresolved_ranges", 0) or 0) != 0: + raise RuntimeError(f"{name}: unresolved ranges present: {validation}") + manifest = list(root.rglob("MANIFEST.json")) + if len(manifest) != 1: + raise RuntimeError(f"{name}: expected one MANIFEST.json") + entries = json.loads(manifest[0].read_text(encoding="utf-8")) + manifest_root = manifest[0].parent + for entry in entries: + path = manifest_root / entry["path"] + if not path.exists(): + raise RuntimeError(f"{name}: manifest file missing: {entry['path']}") + digest = hashlib.sha256(path.read_bytes()).hexdigest() + if digest != entry["sha256"]: + raise RuntimeError(f"{name}: hash mismatch: {entry['path']}") + return validation + + +def main() -> None: + if OUT.exists(): + shutil.rmtree(OUT) + DOWNLOADS.mkdir(parents=True) + EXTRACTED.mkdir(parents=True) + current_ref = os.environ.get("GITHUB_SHA") or BRANCH + current_shas = { + name: file_sha(current_ref, cfg["script"]) + for name, cfg in DEPENDENCIES.items() + } + deadline = time.time() + 5 * 3600 + selected_runs: dict[str, dict[str, Any]] = {} + while time.time() < deadline: + for name, cfg in DEPENDENCIES.items(): + if name not in selected_runs: + run = latest_matching_run(name, cfg["script"], current_shas[name]) + if run: + selected_runs[name] = run + if len(selected_runs) == len(DEPENDENCIES): + break + print({ + "waiting_for": sorted(set(DEPENDENCIES) - set(selected_runs)), + "selected": {name: run["id"] for name, run in selected_runs.items()}, + }, flush=True) + time.sleep(30) + if len(selected_runs) != len(DEPENDENCIES): + raise RuntimeError(f"Timed out waiting for matching successful runs: {selected_runs}") + + artifact_records = [] + expected_all = set().union(*(cfg["artifacts"] for cfg in DEPENDENCIES.values())) + found: dict[str, dict[str, Any]] = {} + for workflow_name, run in selected_runs.items(): + artifacts = request_json(f"{API}/repos/{REPO}/actions/runs/{run['id']}/artifacts?per_page=100") + for artifact in artifacts.get("artifacts", []): + if artifact.get("name") in expected_all and not artifact.get("expired"): + found[artifact["name"]] = {**artifact, "source_workflow": workflow_name, "source_run_id": run["id"], "source_head_sha": run["head_sha"]} + missing = expected_all - set(found) + if missing: + raise RuntimeError(f"Missing expected artifacts: {sorted(missing)}") + + validations = {} + for name in sorted(expected_all): + artifact = found[name] + zip_path = DOWNLOADS / f"{name}.zip" + download(artifact["archive_download_url"], zip_path) + with zipfile.ZipFile(zip_path) as archive: + bad = archive.testzip() + if bad: + raise RuntimeError(f"{name}: corrupt member {bad}") + destination = EXTRACTED / name + destination.mkdir(parents=True) + archive.extractall(destination) + validations[name] = validate_extracted(name, EXTRACTED / name) + artifact_records.append({ + "name": name, + "artifact_id": artifact["id"], + "artifact_digest": artifact.get("digest"), + "artifact_size": artifact.get("size_in_bytes"), + "source_workflow": artifact["source_workflow"], + "source_run_id": artifact["source_run_id"], + "source_head_sha": artifact["source_head_sha"], + "collector_script_sha": current_shas[artifact["source_workflow"]], + "download_sha256": hashlib.sha256(zip_path.read_bytes()).hexdigest(), + }) + + bundle_root = OUT / "rhc-canonical-raw-complete" + bundle_root.mkdir() + shutil.copytree(EXTRACTED, bundle_root / "artifacts") + (bundle_root / "SOURCE_ARTIFACTS.json").write_text( + json.dumps(artifact_records, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8" + ) + (bundle_root / "INPUT_VALIDATIONS.json").write_text( + json.dumps(validations, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8" + ) + summary = { + "status": "PASS", + "repository": REPO, + "source_branch": BRANCH, + "bundler_sha": current_ref, + "artifact_count": len(artifact_records), + "artifacts": sorted(found), + "all_input_validations_pass": True, + "all_unresolved_ranges_zero": True, + } + (bundle_root / "BUNDLE_VALIDATION.json").write_text( + json.dumps(summary, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8" + ) + manifest = [] + for path in sorted(bundle_root.rglob("*")): + if path.is_file() and path.name != "MANIFEST.json": + manifest.append({ + "path": str(path.relative_to(bundle_root)).replace("\\", "/"), + "bytes": path.stat().st_size, + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + }) + (bundle_root / "MANIFEST.json").write_text( + json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8" + ) + archive_path = OUT / "rhc-canonical-raw-complete.zip" + with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for path in sorted(bundle_root.rglob("*")): + if path.is_file(): + archive.write(path, path.relative_to(OUT)) + (OUT / "rhc-canonical-raw-complete.zip.sha256").write_text( + hashlib.sha256(archive_path.read_bytes()).hexdigest() + " " + archive_path.name + "\n", + encoding="utf-8", + ) + print(json.dumps(summary, sort_keys=True), flush=True) + + +if __name__ == "__main__": + main() From fac1de4eb0b6cc8d354b2310e7f0119f3450a930 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 00:05:00 +0000 Subject: [PATCH 39/42] chore: publish project opportunity universe [skip ci] --- .results/rhc-project-universe-v1-index.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.results/rhc-project-universe-v1-index.json b/.results/rhc-project-universe-v1-index.json index 2f9f669..409871d 100644 --- a/.results/rhc-project-universe-v1-index.json +++ b/.results/rhc-project-universe-v1-index.json @@ -2,6 +2,6 @@ "bytes": 22, "files": [], "package": "rhc-project-universe-v1.zip", - "publisher_run_id": 33221859357, + "publisher_run_id": 33222447358, "sha256": "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85" } \ No newline at end of file From f76c7c67300a292ca1f350ac47ac583693d2a1a1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 02:38:37 +0000 Subject: [PATCH 40/42] chore: publish canonical data collection result [skip ci] --- .results/rhc-canonical-data-completeness.json | 43 +++++++++++++++++++ .results/rhc-canonical-publisher.json | 5 +++ .results/rhc-canonical-remediation-queue.csv | 1 + 3 files changed, 49 insertions(+) create mode 100644 .results/rhc-canonical-data-completeness.json create mode 100644 .results/rhc-canonical-publisher.json create mode 100644 .results/rhc-canonical-remediation-queue.csv diff --git a/.results/rhc-canonical-data-completeness.json b/.results/rhc-canonical-data-completeness.json new file mode 100644 index 0000000..e945c86 --- /dev/null +++ b/.results/rhc-canonical-data-completeness.json @@ -0,0 +1,43 @@ +{ + "canonical_nft_transfer_rows": 0, + "collector_error_rows": 0, + "data_collection_complete_for_wallet_verification": false, + "deepseek_handoff_allowed": false, + "expected_artifacts": 18, + "fixed_heads": { + "seadrop": null, + "seaport": null + }, + "global_event_rows": 0, + "global_event_validations": {}, + "global_history_complete": false, + "known_transaction_evidence_rows": 0, + "missing_artifacts": [ + "rhc-global-seadrop", + "rhc-global-seaport", + "rhc-canonical-wallet-0", + "rhc-canonical-wallet-1", + "rhc-canonical-wallet-2", + "rhc-canonical-wallet-3", + "rhc-canonical-wallet-4", + "rhc-canonical-wallet-5", + "rhc-canonical-wallet-6", + "rhc-canonical-wallet-7", + "rhc-canonical-wallet-8", + "rhc-canonical-wallet-9", + "rhc-canonical-wallet-10", + "rhc-canonical-wallet-11", + "rhc-canonical-wallet-12", + "rhc-canonical-wallet-13", + "rhc-canonical-wallet-14", + "rhc-canonical-wallet-15" + ], + "priority_counts": {}, + "production_approved_wallets": 0, + "remediation_rows": 0, + "status": "PARTIAL_REMEDIATION_REQUIRED", + "wallet_set_ok": false, + "wallet_status_counts": {}, + "wallets_expected": 232, + "wallets_observed": 0 +} \ No newline at end of file diff --git a/.results/rhc-canonical-publisher.json b/.results/rhc-canonical-publisher.json new file mode 100644 index 0000000..864c8e5 --- /dev/null +++ b/.results/rhc-canonical-publisher.json @@ -0,0 +1,5 @@ +{ + "head_sha": "8d8983368125706fbaebe10a4a022823aee81103", + "publisher_run_attempt": 1, + "publisher_run_id": 33222447353 +} \ No newline at end of file diff --git a/.results/rhc-canonical-remediation-queue.csv b/.results/rhc-canonical-remediation-queue.csv new file mode 100644 index 0000000..6bfb621 --- /dev/null +++ b/.results/rhc-canonical-remediation-queue.csv @@ -0,0 +1 @@ +empty From f9bd91f7b743b6c917b1f40d5dab48f9e18dde15 Mon Sep 17 00:00:00 2001 From: dcftradingcrypt <136437469+dcftradingcrypt@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:06:54 +0900 Subject: [PATCH 41/42] ci: add checkpointed 8-wallet P2 batch collector --- .github/workflows/rhc-p2-batch8.yml | 122 ++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 .github/workflows/rhc-p2-batch8.yml diff --git a/.github/workflows/rhc-p2-batch8.yml b/.github/workflows/rhc-p2-batch8.yml new file mode 100644 index 0000000..6d41aa6 --- /dev/null +++ b/.github/workflows/rhc-p2-batch8.yml @@ -0,0 +1,122 @@ +name: RHC P2 batch8 checkpoint + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/rhc-p2-batch8.yml' + - 'wallet-verification/p2_addresses.txt' + - 'wallet-verification/collector.py.gz.b64' + +permissions: + contents: read + +concurrency: + group: rhc-p2-batch8-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + batch0: + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + BATCH_INDEX: '0' + BATCH_SIZE: '8' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Materialize fixed batch input + shell: bash + run: | + set -euo pipefail + mkdir -p runtime out-p2-batch8 + base64 -d wallet-verification/collector.py.gz.b64 | gzip -d > runtime/collector.py + python -m py_compile runtime/collector.py + python - <<'PY' + import json, os, re + from pathlib import Path + idx=int(os.environ['BATCH_INDEX']); size=int(os.environ['BATCH_SIZE']) + all_addr=[x.strip().lower() for x in Path('wallet-verification/p2_addresses.txt').read_text().splitlines() if x.strip()] + assert len(all_addr)==212 and len(set(all_addr))==212 + assert all(re.fullmatch(r'0x[a-f0-9]{40}', x) for x in all_addr) + selected=all_addr[idx*size:(idx+1)*size] + assert len(selected)==size + rows=[{ + 'wallet':a, + 'priority':'P2', + 'priority_points':5, + 'verification_reasons':['FIXED_P2_VERIFICATION_QUEUE','BATCH8_CHECKPOINTED_COLLECTION'], + 'known_mints':[], + 'known_sales':[] + } for a in selected] + Path('runtime/wallets.json').write_text(json.dumps(rows, indent=2), encoding='utf-8') + meta={'batch_index':idx,'batch_size':size,'start_offset':idx*size,'end_offset_exclusive':(idx+1)*size,'wallets':selected,'source_total':len(all_addr)} + Path('runtime/batch_metadata.json').write_text(json.dumps(meta, indent=2), encoding='utf-8') + Path('out-p2-batch8/BATCH_START.json').write_text(json.dumps(meta, indent=2), encoding='utf-8') + print(json.dumps(meta)) + PY + - name: Collect canonical evidence for exactly 8 wallets + shell: bash + run: python runtime/collector.py --wallets runtime/wallets.json --out out-p2-batch8 + - name: Build fail-closed checkpoint + if: always() + shell: bash + run: | + python - <<'PY' + import csv, json + from pathlib import Path + out=Path('out-p2-batch8') + meta=json.loads(Path('runtime/batch_metadata.json').read_text()) + expected=set(meta['wallets']) + summary=[]; errors=[]; validation={} + sp=out/'wallet_summary.csv'; ep=out/'errors.csv'; vp=out/'VALIDATION.json' + if sp.exists(): + with sp.open(newline='', encoding='utf-8-sig') as f: summary=list(csv.DictReader(f)) + if ep.exists(): + with ep.open(newline='', encoding='utf-8-sig') as f: errors=list(csv.DictReader(f)) + if vp.exists(): + try: validation=json.loads(vp.read_text(encoding='utf-8')) + except Exception as e: validation={'status':'UNREADABLE','error':repr(e)} + seen={str(r.get('wallet','')).lower() for r in summary if r.get('wallet')} + missing=sorted(expected-seen) + unexpected=sorted(seen-expected) + failures=[] + if validation.get('status')!='PASS': failures.append({'code':'UPSTREAM_VALIDATION_NOT_PASS','value':validation.get('status')}) + if len(summary)!=8: failures.append({'code':'SUMMARY_COUNT_MISMATCH','expected':8,'actual':len(summary)}) + if errors: failures.append({'code':'COLLECTOR_ERRORS_PRESENT','count':len(errors),'sample':errors[:8]}) + if missing: failures.append({'code':'MISSING_WALLET_SUMMARIES','wallets':missing}) + if unexpected: failures.append({'code':'UNEXPECTED_WALLET_SUMMARIES','wallets':unexpected}) + report={ + 'status':'PASS' if not failures else 'FAIL', + 'batch_index':meta['batch_index'], + 'batch_size':8, + 'wallets':meta['wallets'], + 'summary_rows':len(summary), + 'collector_error_rows':len(errors), + 'upstream_validation_status':validation.get('status'), + 'failures':failures, + 'next_batch_index':1 if not failures else 0, + 'next_offset':8 if not failures else 0, + 'production_approved_wallets':0 + } + (out/'BATCH_CHECKPOINT.json').write_text(json.dumps(report, indent=2), encoding='utf-8') + print(json.dumps(report)) + PY + - name: Preserve immutable batch inputs + if: always() + shell: bash + run: | + mkdir -p out-p2-batch8/runtime + cp runtime/collector.py runtime/wallets.json runtime/batch_metadata.json out-p2-batch8/runtime/ + cp wallet-verification/p2_addresses.txt out-p2-batch8/runtime/ + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rhc-p2-batch8-000 + path: out-p2-batch8/ + retention-days: 5 + if-no-files-found: error + compression-level: 9 From 7b76741ce290f55087fd85d051b328d820e3b384 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:07:17 +0000 Subject: [PATCH 42/42] chore: publish project opportunity universe [skip ci] --- .results/rhc-project-universe-v1-index.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.results/rhc-project-universe-v1-index.json b/.results/rhc-project-universe-v1-index.json index 409871d..403f7a8 100644 --- a/.results/rhc-project-universe-v1-index.json +++ b/.results/rhc-project-universe-v1-index.json @@ -2,6 +2,6 @@ "bytes": 22, "files": [], "package": "rhc-project-universe-v1.zip", - "publisher_run_id": 33222447358, + "publisher_run_id": 33262084117, "sha256": "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85" } \ No newline at end of file