Skip to content

Canonicalize wallet JSON unicode keys#561

Open
tinyopsstudio wants to merge 6 commits into
ramimbo:mainfrom
tinyopsstudio:tinyops-wallet-js-unicode-key-canonicalization
Open

Canonicalize wallet JSON unicode keys#561
tinyopsstudio wants to merge 6 commits into
ramimbo:mainfrom
tinyopsstudio:tinyops-wallet-js-unicode-key-canonicalization

Conversation

@tinyopsstudio
Copy link
Copy Markdown

@tinyopsstudio tinyopsstudio commented May 28, 2026

Bounty #406

Summary:

  • align browser wallet.js canonical JSON with server canonical_wallet_json() for Unicode strings and object keys
  • ASCII-escape non-ASCII string content before signing, matching Python json.dumps(..., ensure_ascii=True)
  • sort object keys by Unicode code point order so non-BMP keys do not diverge from Python sort_keys=True

Evidence:

  • current wallet.js uses native JSON.stringify plus default JavaScript .sort(), so non-ASCII values are not escaped like the server and keys such as "\\ue000" and "😀" are ordered by UTF-16 code units instead of Python Unicode code points
  • this can make browser-signed wallet transfer payloads fail server signature verification when memos or keys contain Unicode
  • this addresses the remaining canonicalization gap called out on PR Refs #406: Canonicalize wallet unicode memos #507 with a focused implementation and regression
  • no private keys, cookies, tokens, wallet JSON, browser storage, OAuth state, signatures, private data, price claims, liquidity claims, exchange claims, bridge promises, or off-ramp claims are added

Validation:

  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/bin/python -m pytest tests/test_wallet_api.py::test_wallet_js_canonicalizes_unicode_like_server -q -> 1 passed
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/bin/python -m pytest tests/test_wallet_api.py tests/test_wallets.py -q -> 44 passed
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/bin/python -m pytest -q -> 415 passed
  • ./.venv/bin/python -m ruff check tests/test_wallet_api.py -> passed
  • ./.venv/bin/python -m ruff format --check tests/test_wallet_api.py -> 1 file already formatted
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/bin/python -m mypy app -> success
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/bin/python scripts/docs_smoke.py -> docs smoke ok
  • git diff --check HEAD -> clean

Summary by CodeRabbit

  • Bug Fixes

    • Deterministic, ASCII-only payload canonicalization for signing.
    • Private key input is always cleared after a transfer attempt.
  • Tests

    • Added a cross-runtime canonicalization test to ensure JS and server produce the same canonical JSON for Unicode payloads.
    • Strengthened transfer tests to assert the clearing behavior occurs in the correct execution order.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 69b0864f-e928-4f1d-8506-163e28d2332a

📥 Commits

Reviewing files that changed from the base of the PR and between 7ae550a and 62fc56d.

📒 Files selected for processing (1)
  • tests/test_wallet_api.py

📝 Walkthrough

Walkthrough

wallet.js: stableJson now sorts object keys by Unicode code-point order and emits ASCII-escaped JSON; transfer submit adds a finally block that clears the private-key input. tests/test_wallet_api.py: adds Node parity test and tightens UI ordering assertions for transfer flow.

Changes

Wallet canonicalization and submit flow

Layer / File(s) Summary
Canonical JSON helpers and stableJson rewrite
app/static/wallet.js
Adds asciiJson and compareCodePoints; rewrites stableJson to sort object keys by Unicode code points and output ASCII-escaped JSON used for signing.
Transfer submit private-key field cleanup
app/static/wallet.js
Adds a finally clause in the transfer submit handler to call clearPrivateKeyField(form) after the API attempt completes.
Tests: imports, UI ordering checks, Node parity
tests/test_wallet_api.py
Adds json, shutil, subprocess imports; strengthens test_transfer_action_clears_private_key_after_submit_attempt() to assert call ordering; adds test_wallet_js_canonicalizes_unicode_like_server() which runs app/static/wallet.js in Node to assert stableJson(payload) equals Python canonical_wallet_json(payload) (skips if node missing).
  • Possibly related PRs:
    • ramimbo/mergework#508: Similar modifications to wallet.js to add/verify a finally block clearing the private-key input with corresponding test coverage.
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed Title names the changed surface and summarizes the main change: aligning wallet JSON canonicalization for Unicode keys and characters.
Description check ✅ Passed Description includes all required template sections: summary with bullet points, evidence of the problem and implementation approach, validation with passing test results, and bounty reference.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Mergework Public Artifact Hygiene ✅ Passed No investment, price, cash-out, off-ramp, fabricated payout, or private security claims found in PR code, description, or comments; only technical canonicalization changes and regression tests.
Bounty Pr Focus ✅ Passed PR diff restricted to two stated files. Implements Unicode canonicalization with new functions and comprehensive tests. No scope creep.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@xingxi0614-cpu
Copy link
Copy Markdown

Reviewed PR #561 at head 55f35ee9eaf4634879ee221919e1dd5faae6c819 for the wallet JSON Unicode canonicalization slice.

No blocker found in this reviewed scope.

Evidence checked:

  • inspected app/static/wallet.js and tests/test_wallet_api.py;
  • confirmed stableJson() now sorts object keys by Unicode code point order instead of JavaScript UTF-16 code-unit order;
  • confirmed string values and object keys are ASCII-escaped through asciiJson(), aligning browser output with Python json.dumps(..., ensure_ascii=True) for non-ASCII payload content;
  • confirmed the regression test executes wallet.js through Node and compares browser-side stableJson() output with server-side canonical_wallet_json() on a payload containing accented text, an emoji, and keys that expose UTF-16 vs Unicode-code-point ordering differences;
  • checked CodeRabbit reported no actionable comments on this head.

Validation run locally:

  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/Scripts/python.exe -m pytest tests/test_wallet_api.py::test_wallet_js_canonicalizes_unicode_like_server -q -> 1 passed
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/Scripts/python.exe -m pytest tests/test_wallet_api.py tests/test_wallets.py -q -> 44 passed
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/Scripts/python.exe -m pytest -q -> 415 passed
  • ./.venv/Scripts/python.exe -m ruff check tests/test_wallet_api.py -> passed
  • ./.venv/Scripts/python.exe -m ruff format --check tests/test_wallet_api.py -> 1 file already formatted
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/Scripts/python.exe -m mypy app -> success
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 ./.venv/Scripts/python.exe scripts/docs_smoke.py -> docs smoke ok
  • git diff --check origin/main...HEAD -> clean

This looks mergeable from my reviewed slice.

@eliasx45
Copy link
Copy Markdown

Current-head review for PR #561 at 55f35ee9eaf4634879ee221919e1dd5faae6c819.

I do not see a blocker in the wallet JSON canonicalization slice. The implementation changes the browser signing path from native JSON.stringify() key order to a deterministic serializer that sorts object keys by Unicode code point and ASCII-escapes string content before signing.

Additional evidence I checked beyond the existing focused regression:

node --check app\static\wallet.js
# passed

.\.venv\Scripts\python.exe -m pytest tests\test_wallet_api.py::test_wallet_js_canonicalizes_unicode_like_server tests\test_wallet_api.py::test_wallet_transfer_api_rejects_memo_control_characters -q
# 2 passed

.\.venv\Scripts\python.exe -m ruff check tests\test_wallet_api.py
# All checks passed!

.\.venv\Scripts\python.exe -m ruff format --check tests\test_wallet_api.py
# 1 file already formatted

git diff --check origin/main...HEAD
# clean

I also ran an ad hoc Node/Python canonicalization probe with escaped non-BMP keys and values (U+10000, U+1F600, and U+E000). Browser stableJson() matched Python canonical_wallet_json() exactly:

equal True
js_ascii {"memo":"caf\\u00e9 \\ud83d\\ude00","type":"probe","\\ue000":1,"\\ud800\\udc00":3,"\\ud83d\\ude00":2}
py_ascii {"memo":"caf\\u00e9 \\ud83d\\ude00","type":"probe","\\ue000":1,"\\ud800\\udc00":3,"\\ud83d\\ude00":2}

That covers the key UTF-16-vs-code-point ordering edge this PR is meant to fix. No private keys, wallet material, cookies, tokens, signatures, private data, price/liquidity/exchange/off-ramp claims, or fabricated payout claims were used.

Copy link
Copy Markdown

@eliasx45 eliasx45 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocker found on this head.

I checked the browser/server canonicalization boundary because this is the risky part of the change. The new stableJson() path now sorts object keys by Unicode code point rather than UTF-16 unit order, and asciiJson() emits non-ASCII values/keys in the same escaped form the Python server signs through canonical_wallet_json(). That covers the important astral-key case where default JS sorting would otherwise disagree with Python.

Validated locally:

  • git diff --check origin/main...HEAD
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .\.venv\Scripts\python.exe -m pytest tests\test_wallet_api.py::test_wallet_js_canonicalizes_unicode_like_server tests\test_wallet_api.py::test_wallet_api_register_lookup_and_transfer tests\test_wallet_api.py::test_github_session_can_link_and_claim_wallet -q -> 3 passed
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .\.venv\Scripts\python.exe -m pytest tests\test_wallet_api.py -q -> 33 passed
  • node --check app\static\wallet.js
  • .\.venv\Scripts\python.exe -m ruff check tests\test_wallet_api.py
  • .\.venv\Scripts\python.exe -m ruff format --check tests\test_wallet_api.py
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .\.venv\Scripts\python.exe -m mypy app\wallets.py app\ledger\service.py
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .\.venv\Scripts\python.exe -m pytest tests\test_wallet_api.py tests\test_ledger.py -q -> 55 passed
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .\.venv\Scripts\python.exe -m pytest -q -> 415 passed
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .\.venv\Scripts\python.exe scripts\docs_smoke.py -> docs smoke ok

Copy link
Copy Markdown

@aunysillyme aunysillyme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 561 Review: Approved

I have inspected the diff, verified the canonicalization helpers, and reviewed the added Node.js vm integration test:

  1. Deterministic Canonicalization: The addition of asciiJson is correct and perfectly aligns JS string serialization with Python's standard json.dumps(..., ensure_ascii=True). By replacing non-ASCII characters with \uXXXX escapes, it eliminates potential signature mismatches caused by varying unicode encoders.
  2. Key Ordering Fix: Implementing compareCodePoints ensures object keys are sorted strictly by Unicode code points (using codePointAt(0)) rather than default UTF-16 code unit values. This is an elegant fix that resolves discrepancies when sorting surrogate-pair characters (e.g. emojis like "😀") or private use characters (e.g. "\ue000").
  3. Robust Integration Testing: The added pytest integration test_wallet_js_canonicalizes_unicode_like_server is outstanding. It correctly provisions a sandboxed Node.js VM context to evaluate wallet.js's stableJson output on Unicode-heavy payloads, confirming it matches the Python backend canonical_wallet_json exactly.
  4. Security Audit: No credentials, private key material, or unsafe variables are introduced.

LGTM. Differentiated and highly correct implementation to secure cross-platform signature canonicalization.

Copy link
Copy Markdown

@eliasx45 eliasx45 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked current head 55f35ee9eaf4634879ee221919e1dd5faae6c819 against refreshed origin/main after GitHub marked the PR dirty.

Verdict update: request changes for merge readiness. My prior code-level approval of the wallet canonicalization logic still stands, but the branch is no longer mergeable as-is.

Current merge check:

git fetch origin main --prune
git fetch origin pull/561/head:refs/remotes/origin/pr/561
git diff --check origin/main...refs/remotes/origin/pr/561
# clean

git merge-tree --write-tree origin/main refs/remotes/origin/pr/561
# Auto-merging app/static/wallet.js
# Auto-merging tests/test_wallet_api.py
# CONFLICT (content): Merge conflict in tests/test_wallet_api.py

Please rebase/merge current main and resolve tests/test_wallet_api.py. Once the conflict is gone, the Unicode canonicalization behavior I previously reviewed still looks good.

Copy link
Copy Markdown

@barnacleagent-svg barnacleagent-svg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVED

Scope: Canonicalizes wallet JSON unicode keys in wallet.js. Adds asciiJson() and compareCodePoints() helper functions, then uses them in stableJson() to ensure deterministic unicode key ordering.

Checklist:

  • Diff: +30/-3 in wallet.js, +test updates
  • Proper unicode handling with codePointAt
  • Tests verify new behavior
  • No regression risk

Conclusion: Correct unicode handling. Ready to merge.

Copy link
Copy Markdown

@wangedmund77-cmyk wangedmund77-cmyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 55f35ee9eaf4634879ee221919e1dd5faae6c819 for bounty #578.

Evidence checked:

  • Inspected the diff scope: app/static/wallet.js and tests/test_wallet_api.py.
  • git diff --check origin/main...origin/pr/561 returned clean.
  • Local merge probe git merge-tree --write-tree origin/main origin/pr/561 fails with a content conflict in tests/test_wallet_api.py. GitHub mergeability also reports CONFLICTING.

Requesting changes until the branch is rebased/resolved. After rebase, the useful follow-up check is to rerun the wallet API/browser-facing JSON-key coverage because the conflict is in the same test file that verifies this behavior.

Copy link
Copy Markdown

@eliasx45 eliasx45 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked current head 7ae550ad73c150c611e46f2074d423ade95f53f0. Verdict: request changes remains for merge readiness.

The branch still conflicts with current origin/main in the same wallet API test file that carries the canonicalization regression:

git diff --check origin/main...HEAD
# clean

git merge-tree --write-tree origin/main HEAD
# Auto-merging app/static/wallet.js
# Auto-merging tests/test_wallet_api.py
# CONFLICT (content): Merge conflict in tests/test_wallet_api.py

node --check app/static/wallet.js passed on the current head, so I am not flagging the browser helper syntax. The blocker is that this cannot merge cleanly yet; please rebase/merge current main and resolve tests/test_wallet_api.py, then rerun the wallet JSON canonicalization regression in the resolved context.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 107b49ff-5725-439c-a15d-e00818341f6d

📥 Commits

Reviewing files that changed from the base of the PR and between 55f35ee and 7ae550a.

📒 Files selected for processing (2)
  • app/static/wallet.js
  • tests/test_wallet_api.py

Comment thread tests/test_wallet_api.py
Copy link
Copy Markdown

@eliasx45 eliasx45 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed current head 62fc56d32422e2a26ba10ac3a43b75faf2aaa0cf. Verdict: approve.

The branch is merge-clean now, and the current test update addresses the CodeRabbit note by asserting the transfer cleanup call is inside the finally path after the result/error branches. The Unicode canonicalization behavior also remains covered by the Node/Python parity test.

Validation on this head:

git diff --check origin/main...HEAD
# clean

git merge-tree --write-tree origin/main HEAD
# b37bffc65ac72abab0094a4b39fed9c94be0ec8f

node --check app/static/wallet.js
# passed

..\mergework\.venv\Scripts\python.exe -m pytest tests/test_wallet_api.py::test_wallet_js_canonicalizes_unicode_like_server tests/test_wallet_api.py::test_wallet_transfer_api_rejects_memo_control_characters tests/test_wallet_api.py::test_transfer_action_clears_private_key_after_submit_attempt -q
# 3 passed

..\mergework\.venv\Scripts\python.exe -m pytest tests/test_wallet_api.py tests/test_wallets.py -q
# 45 passed

..\mergework\.venv\Scripts\python.exe -m ruff check tests/test_wallet_api.py
# All checks passed!

..\mergework\.venv\Scripts\python.exe -m ruff format --check tests/test_wallet_api.py
# 1 file already formatted

..\mergework\.venv\Scripts\python.exe scripts/docs_smoke.py
# docs smoke ok

No private keys, wallet material, signatures, cookies, tokens, production data, price/liquidity/off-ramp claims, or fabricated payout claims were used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants