Skip to content

refactor: replace custom hex utils with @noble/hashes#916

Merged
jbojcic1 merged 1 commit intomasterfrom
claude/issue-626-20260304-1121
Mar 6, 2026
Merged

refactor: replace custom hex utils with @noble/hashes#916
jbojcic1 merged 1 commit intomasterfrom
claude/issue-626-20260304-1121

Conversation

@jbojcic1
Copy link
Collaborator

@jbojcic1 jbojcic1 commented Mar 4, 2026

Remove custom uint8ArrayToHex and hexToUint8Array from lib/utils and use bytesToHex / hexToBytes from @noble/hashes/utils instead.

Closes #626

Generated with Claude Code

@vercel
Copy link

vercel bot commented Mar 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agicash Ready Ready Preview, Comment Mar 6, 2026 0:06am

Request Review

@supabase
Copy link

supabase bot commented Mar 4, 2026

This pull request has been ignored for the connected project hrebgkfhjpkbxpztqqke because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link
Contributor

@gudnuf gudnuf left a comment

Choose a reason for hiding this comment

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

I think we can also remove our sha256 lib function or at least turn it into a wrapper around sha256 from @noble.

iirc the function signature was a bit different and required a bit more refactoring

@jbojcic1
Copy link
Collaborator Author

jbojcic1 commented Mar 6, 2026

I think we can also remove our sha256 lib function or at least turn it into a wrapper around sha256 from @noble.

iirc the function signature was a bit different and required a bit more refactoring

@claude can you look into this and see if possible. If yes suggest a plan how to tackle it

@claude
Copy link
Contributor

claude bot commented Mar 6, 2026

Claude finished @jbojcic1's task in 1m 17s —— View job


Investigating sha256 replacement with @noble/hashes

  • Find current sha256 implementation
  • Find all usages across the codebase
  • Compare with @noble/hashes sha256 API
  • Propose a plan

Analysis

Current implementation (app/lib/sha256.ts):

export async function computeSHA256(message: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(message);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
  return hashHex;
}

@noble/hashes sha256 (@noble/hashes/sha2):

sha256(msg: Input): Uint8Array  // Input = string | Uint8Array

Key differences:

  • Sync vs async: noble's sha256 is synchronous (no await needed), while current uses crypto.subtle.digest which is async
  • Return type: noble returns Uint8Array, not a hex string — need bytesToHex() from @noble/hashes/utils to convert

Proposed Plan

Yes, this is definitely doable. Here's how:

  1. Create a thin wrapper (or inline everywhere):

    import { sha256 } from '@noble/hashes/sha2';
    import { bytesToHex } from '@noble/hashes/utils';
    
    // Option A: wrapper (keeps changes minimal)
    export function computeSHA256(message: string): string {
      return bytesToHex(sha256(message));
    }
    
    // Option B: inline at each call site
    bytesToHex(sha256(quoteId))
  2. Update 6 files that import computeSHA256:

    • app/features/shared/spark.tsqueryKey (currently puts a Promise in the key — fixing this to sync is actually an improvement)
    • app/features/shared/cashu.tscomputeTokenId()
    • app/features/user/auth.ts — password reset hash
    • app/features/send/cashu-send-quote-repository.ts — quote ID hash
    • app/features/receive/cashu-receive-quote-repository.ts — quote ID hash
    • app/features/receive/cashu-receive-quote-repository.server.ts — quote ID hash
  3. Remove await from all call sites since noble's sha256 is synchronous. Some callers use Promise.all([encrypt(...), computeSHA256(...)]) — these would simplify since the hash is no longer a promise.

  4. Delete app/lib/sha256.ts

Recommendation

Option A (wrapper) is lower risk — it keeps the same function name/import, just changes the signature from Promise<string> to string. The main refactoring is removing await at call sites.

Option B (inline) is cleaner long-term but touches more import lines.

Either way, it's a straightforward change — maybe ~30 minutes of work. Want me to implement it?


@jbojcic1
Copy link
Collaborator Author

jbojcic1 commented Mar 6, 2026

Claude finished @jbojcic1's task in 1m 17s —— View job

Investigating sha256 replacement with @noble/hashes

  • Find current sha256 implementation
  • Find all usages across the codebase
  • Compare with @noble/hashes sha256 API
  • Propose a plan

Analysis

Current implementation (app/lib/sha256.ts):

export async function computeSHA256(message: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(message);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
  return hashHex;
}

@noble/hashes sha256 (@noble/hashes/sha2):

sha256(msg: Input): Uint8Array  // Input = string | Uint8Array

Key differences:

  • Sync vs async: noble's sha256 is synchronous (no await needed), while current uses crypto.subtle.digest which is async
  • Return type: noble returns Uint8Array, not a hex string — need bytesToHex() from @noble/hashes/utils to convert

Proposed Plan

Yes, this is definitely doable. Here's how:

  1. Create a thin wrapper (or inline everywhere):

    import { sha256 } from '@noble/hashes/sha2';
    import { bytesToHex } from '@noble/hashes/utils';
    
    // Option A: wrapper (keeps changes minimal)
    export function computeSHA256(message: string): string {
      return bytesToHex(sha256(message));
    }
    
    // Option B: inline at each call site
    bytesToHex(sha256(quoteId))
  2. Update 6 files that import computeSHA256:

    • app/features/shared/spark.tsqueryKey (currently puts a Promise in the key — fixing this to sync is actually an improvement)
    • app/features/shared/cashu.tscomputeTokenId()
    • app/features/user/auth.ts — password reset hash
    • app/features/send/cashu-send-quote-repository.ts — quote ID hash
    • app/features/receive/cashu-receive-quote-repository.ts — quote ID hash
    • app/features/receive/cashu-receive-quote-repository.server.ts — quote ID hash
  3. Remove await from all call sites since noble's sha256 is synchronous. Some callers use Promise.all([encrypt(...), computeSHA256(...)]) — these would simplify since the hash is no longer a promise.

  4. Delete app/lib/sha256.ts

Recommendation

Option A (wrapper) is lower risk — it keeps the same function name/import, just changes the signature from Promise<string> to string. The main refactoring is removing await at call sites.

Option B (inline) is cleaner long-term but touches more import lines.

Either way, it's a straightforward change — maybe ~30 minutes of work. Want me to implement it?

@claude do you think it's a good idea to do this given that it would turn async action to sync? is it better to have it async (disregard query key case in app/features/shared/spark.ts which uses promise. that's obviously a bug that needs to be fixed if we stay with async approach)?

@claude
Copy link
Contributor

claude bot commented Mar 6, 2026

Claude finished @jbojcic1's task in 27s —— View job


Sync vs Async SHA-256: Analysis

  • Analyze tradeoffs of sync vs async sha256

Short answer: sync is the right call here. Switching to @noble/hashes is a good idea.

Here's why:

1. The inputs are tiny. Every usage in this codebase hashes short strings — quote IDs, token IDs, passwords. We're talking about hashing maybe 32–128 bytes. SHA-256 on inputs this small takes microseconds in pure JS. The overhead of creating a Promise, scheduling a microtask, and awaiting it is likely more expensive than the hash computation itself.

2. crypto.subtle async is designed for different use cases. The Web Crypto API is async because it's meant to handle expensive operations (encrypting large payloads, key generation) and can offload to native/hardware crypto. For sub-microsecond hashes of tiny strings, that async machinery adds complexity with no performance benefit.

3. Sync simplifies call sites. Removing await from 6 call sites reduces cognitive overhead. Some callers currently do Promise.all([encrypt(...), computeSHA256(...)]) — the hash resolves instantly anyway, so the Promise.all is just ceremony. Making it sync makes the code honest about what's actually expensive (the encryption) vs. what's trivial (the hash).

4. @noble/hashes is battle-tested. It's already a dependency (used by @cashu/cashu-ts and @noble/curves), audited, and widely used in the Bitcoin/crypto ecosystem. No new dependency risk.

When would async matter? If you were hashing megabytes of data where blocking the main thread is a real concern. That's not happening here — and if it ever did, you'd want a streaming hash anyway, not crypto.subtle.digest which also buffers everything in memory.

TL;DR: For small inputs like IDs and passwords, sync sha256() from @noble/hashes is simpler, equally fast (likely faster due to no Promise overhead), and the right tool for the job. The async version was technically over-engineered for this use case.


View job

@jbojcic1
Copy link
Collaborator Author

jbojcic1 commented Mar 6, 2026

@claude good, lets implement it then according to your plan. Just make that change as a new commit in claude/issue-626-20260304-1121 branch.

@claude
Copy link
Contributor

claude bot commented Mar 6, 2026

Claude finished @jbojcic1's task in 2m 7s —— View job


Replacing computeSHA256 with @noble/hashes

  • Read current sha256.ts and all consumer files
  • Update sha256.ts to use @noble/hashes (sync wrapper)
  • Update all 9 call sites to remove await / simplify Promise.all
  • Run bun run fix:all — clean
  • Commit and push

Summary

Replaced the async crypto.subtle.digest implementation with synchronous sha256 from @noble/hashes/sha2 + bytesToHex from @noble/hashes/utils.

Changes across 9 files:

File Change
app/lib/sha256.ts Rewrote to use @noble/hashes — now sync, 4 lines
app/features/shared/cashu.ts getTokenHash() return type Promise<string>string
app/features/shared/spark.ts computeSHA256(mnemonic) in queryKey now correctly returns a string instead of a Promise
app/features/user/auth.ts Removed await from computeSHA256 call
app/features/send/cashu-send-quote-repository.ts Replaced Promise.all with sync hash + awaited encrypt
app/features/send/cashu-send-swap-service.ts Removed await from two getTokenHash calls
app/features/receive/cashu-receive-quote-repository.ts Replaced Promise.all with sync hash + awaited encrypt
app/features/receive/cashu-receive-quote-repository.server.ts Replaced Promise.all with sync hash + awaited encrypt
app/features/receive/cashu-receive-swap-repository.ts Removed await from getTokenHash call

Bonus fix: sparkWalletQueryOptions in spark.ts was putting a Promise<string> into the queryKey array (since computeSHA256 was async but wasn't awaited). Now it correctly puts a string.

View job

@jbojcic1
Copy link
Collaborator Author

jbojcic1 commented Mar 6, 2026

@claude can you also rebase it on top of latest master so I can merge?

@claude
Copy link
Contributor

claude bot commented Mar 6, 2026

Claude finished @jbojcic1's task in 14s —— View job


Rebase request

I'm unable to perform git rebase operations — this is a limitation of my current capabilities (I can only create/push commits and delete files, not run git operations like rebase, merge, or reset).

You can rebase locally with:

git fetch origin master
git rebase origin/master
git push --force-with-lease

Or if you prefer, GitHub's "Update branch" button on the PR page can merge master into the branch (though that creates a merge commit rather than a rebase).

You can find more information and potential workarounds in the FAQ.

View job

@jbojcic1
Copy link
Collaborator Author

jbojcic1 commented Mar 6, 2026

Claude finished @jbojcic1's task in 14s —— View job

Rebase request

I'm unable to perform git rebase operations — this is a limitation of my current capabilities (I can only create/push commits and delete files, not run git operations like rebase, merge, or reset).

You can rebase locally with:

git fetch origin master
git rebase origin/master
git push --force-with-lease

Or if you prefer, GitHub's "Update branch" button on the PR page can merge master into the branch (though that creates a merge commit rather than a rebase).

You can find more information and potential workarounds in the FAQ.

View job

@claude what exactly do I need to do to give you rebase permissions?

Regarding github button, there is also a button to rebase but for some reason it doesn't create verified commits and then if I use that I can't merge because our repo requires verified commits. Not sure why github commits made that way are not verified but once done by you are done in github action and are verified.

@claude
Copy link
Contributor

claude bot commented Mar 6, 2026

Claude finished @jbojcic1's task in 23s —— View job


Rebase permissions & verified commits

  • Research rebase capability limitations
  • Explain verified commit behavior

On rebase permissions: Unfortunately, this isn't a permissions issue — it's a fundamental limitation of the Claude Code Action. I operate through the GitHub API (creating commits via the API), and the API doesn't expose git rebase as an operation. Rebase requires running actual git commands (git rebase, git push --force-with-lease), which I cannot do in this environment. There's no setting you can change to enable this. You can track this limitation in the FAQ.

Your best option for this PR: Run locally:

git fetch origin master
git checkout claude/issue-626-20260304-1121
git rebase origin/master
git push --force-with-lease

On verified commits: The reason my commits are verified but GitHub's rebase button produces unverified ones is:

  • My commits are created through the GitHub API using a GitHub App token. GitHub automatically signs API-created commits with its own GPG key, so they show as "Verified".
  • GitHub's "Update with rebase" button rewrites commits (new SHAs), and during this rewrite, the original author's GPG signature is stripped. The rewritten commits are attributed to the original author but aren't re-signed by GitHub. Since your repo requires verified commits, these unsigned rebased commits get rejected.

This is a known GitHub limitation. Your options:

  1. Rebase locally with your own GPG/SSH signing key configured, then force-push
  2. Use merge instead of rebase via the "Update branch" button (merge commits created by GitHub are signed/verified)
  3. If you have branch protection set to require linear history, option 2 won't work and local rebase is the way to go

View job

Remove custom hex conversion functions from lib/utils and use bytesToHex/hexToBytes
from @noble/hashes/utils instead — more robust and already a dependency.

Closes #626

Co-authored-by: Josip Bojčić <jbojcic1@users.noreply.github.com>
@jbojcic1 jbojcic1 force-pushed the claude/issue-626-20260304-1121 branch from 6063b16 to 0313b7a Compare March 6, 2026 12:05
@jbojcic1 jbojcic1 merged commit 7e25302 into master Mar 6, 2026
5 checks passed
@gudnuf gudnuf deleted the claude/issue-626-20260304-1121 branch March 6, 2026 17:36
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.

remove uint8ArrayToHex and hexToUint8Array from our lib

2 participants