Skip to content

Harden demo scripts: dead eth-account import, nonce, success-check#141

Open
Valisthea wants to merge 1 commit into
asterdex:masterfrom
Valisthea:harden/api-demo-safety
Open

Harden demo scripts: dead eth-account import, nonce, success-check#141
Valisthea wants to merge 1 commit into
asterdex:masterfrom
Valisthea:harden/api-demo-safety

Conversation

@Valisthea
Copy link
Copy Markdown

Summary

The demo/ scripts are what integrators copy to build bots that hold API-wallet keys and move funds, so a small defect here propagates widely. This PR fixes three issues that bite on a fresh setup and pins runnable dependencies.

All changes are client-side only and do not alter any byte the server verifies. EIP-712 / HMAC signatures are byte-identical before and after — cross-checked against ethers.signTypedData (see Verification).

Fixes

1. Demos fail to import on a current eth-account

aster-code.py and sol_agent.py import encode_structured_data, which was removed in eth-account >= 0.13 (replaced by encode_typed_data). A fresh pip install eth-account makes the demos crash with ImportError before anything is sent. Both are migrated to encode_typed_data(full_message=...).

2. Nonce can collide and get rejected as -4225 Nonce Expired

get_nonce() used int(time.time()) * 1_000_000, i.e. 1-second resolution (the docs state microsecond precision). Two workers — or one process restarted — sharing a single API wallet emit the same nonce within the same second, and the server rejects the duplicate (-4225 Nonce Expired). aster-code.py also had no lock. Now uses microsecond time.time_ns() // 1000, mutex-guarded and strictly monotonic, so a nonce is never reused within a process. (Across multiple processes, still use one nonce source per key.)

3. consolidation.js reports success on failure

if (sendToMainAddressRes['status'] = 'SUCCESS') is an assignment, not a comparison, so the consolidation step always logs success — even when the transfer failed. The withdraw check spotWithdraw['hash'] != '' also dereferences an error return (''). Fixed to === and added guards so a failed transfer or withdraw is reported as a failure.

4. Minor

  • sol_agent.py: print the nonce that was actually sent, instead of calling get_nonce() a second time (which produced a different value than the one signed).
  • Added demo/requirements.txt and demo/package.json pinning runnable dependency versions.

Verification

  • python -m py_compile and node --check pass on all demo files.
  • Signature parity: signing the same payload with the migrated Python (encode_typed_data) and with ethers.signTypedData produces identical signatures for both the Message{msg} scheme and the dynamic action struct — so the migration stays wire-compatible with the live API.
  • The patched nonce is strictly monotonic and unique across a 50,000-call burst.

Intentionally not changed (needs server-side coordination)

These are genuine hardening opportunities, but changing them client-side would break verification against the current backend, so they are left for a coordinated follow-up rather than silently altered here:

  • Value-inferred EIP-712 types in sign_v3_eip712: the field type depends on the literal (canWithdraw=False vs 0 sign to different digests). A fixed per-action schema would be more robust.
  • Domain chainId hardcoded to 56 on the main-account path, with verifyingContract = 0x0 — the EIP-712 domain does not bind the operating chain.
  • personal_sign("You are signing into Astherus {nonce}") authorizes API-key creation and is byte-identical to the login message. EIP-712 typed data would let wallets show users what they are actually authorizing.

- aster-code.py, sol_agent.py: migrate encode_structured_data to
  encode_typed_data (removed in eth-account >= 0.13; the demos failed to
  import on a fresh install). Output is byte-identical, cross-checked
  against ethers signTypedData.
- nonce: use microsecond time.time_ns()//1000, locked and strictly
  monotonic, so concurrent workers or a restart never reuse a nonce. The
  previous int(time.time()) gave 1-second resolution and could collide
  (server returns -4225 Nonce Expired).
- sol_agent.py: print the nonce actually sent instead of calling
  get_nonce() a second time.
- consolidation.js: fix `if (x = 'SUCCESS')` (assignment, not comparison)
  and guard error returns so a failed transfer or withdraw no longer
  reports success.
- add requirements.txt and package.json to pin runnable dependencies.
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.

1 participant