Skip to content

fix(wallets): normalize Gmail dots in signer locator to match backend#1946

Merged
albertoelias-crossmint merged 3 commits into
mainfrom
devin/1782324095-fix-gmail-dot-normalization
Jun 24, 2026
Merged

fix(wallets): normalize Gmail dots in signer locator to match backend#1946
albertoelias-crossmint merged 3 commits into
mainfrom
devin/1782324095-fix-gmail-dot-normalization

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

The backend's normalizeEmail() strips dots from Gmail local parts at wallet creation time (e.g., test.test@gmail.comtesttest@gmail.com), but the SDK's getSignerLocator() was constructing locators with the raw dotted email. This caused string-equality mismatches in three places during useSigner():

  1. isRecoverySigner()matchesRecovery()getSignerLocator(config) === getSignerLocator(recovery)false
  2. signerIsRegistered() → API lookup by locator — not found
  3. Second isRecoverySigner() fallback — false again
  4. → throws Signer "email:test.test@gmail.com" is not registered in this wallet.

Impact: All outbound wallet operations (send, swap, transfer) were blocked for any Gmail user whose email contains dots. Inbound (receiving) was unaffected since it does not require signer auth.

Fix: Added normalizeEmailForLocator() in signer-locator.ts that mirrors the backend's normalization — strips dots from @gmail.com/@googlemail.com local parts and normalizes googlemail.comgmail.com. Applied to the email branch of getSignerLocator().

References

Test plan

  • 14 new unit tests in signer-locator.test.ts covering:
    • Gmail dot stripping (first.last@gmail.comfirstlast@gmail.com)
    • Multiple dots (f.i.r.s.t@gmail.comfirst@gmail.com)
    • googlemail.comgmail.com domain normalization
    • Combined dots + googlemail normalization
    • Case-insensitive handling (First.Last@Gmail.com)
    • Non-Gmail domains are NOT affected (dots preserved)
    • Already-normalized addresses pass through unchanged
    • All other signer types (external-wallet, phone, passkey, api-key, device) unchanged
  • All 116 existing wallet.test.ts tests pass (including existing email signer "not registered" tests)

Package updates

  • @crossmint/wallets-sdk: patch — changeset added via .changeset/fix-gmail-dot-normalization.md

Category: improvements
Product Area: wallets

Link to Devin session: https://crossmint.devinenterprise.com/sessions/fd1d8361dff94f7a946fc9ec9414f19c
Requested by: @jcurbelo

jcurbelo added 2 commits June 24, 2026 18:04
The backend strips dots from Gmail local parts at wallet creation
(noniep.reggie@gmail.com -> noniepreggie@gmail.com), but the SDK
constructed signer locators with the raw dotted email. This caused
locator mismatches in isRecoverySigner() and signerIsRegistered(),
blocking all outbound operations (send/swap/transfer) for Gmail
users with dots in their email.
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author
Original prompt from Robin

@playbook:playbook-c8f0625505dd4cbfb299ae95b18570e1 troubleshoot:

User can receive but all send/swap/transfer fail at Initiated → root cause unknown

Reporter: Reginald White (@noniep)
Reported via: Jeremy (screenshots + video)

Context: A user can receive funds normally but cannot swap, or transfer — every outbound attempt fails, with different errors across attempts. Screenshots show a generic "Transfer Failed" screen failing at the Initiated step, before Processing or Confirming on network.
The user has funds in both their save and send accounts, so this is not an empty-balance issue. We don't yet know the cause – this one will take some investigation.

Observations + Open Questions:

Inbound works; but outbound paths (swap/transfer) fail. Whatever's broken is specific to origination, not to the wallet or auth wholesale.
User has funds in both save and send accounts – not a balance problem.
Failures occur at Initiated, before Processing/Confirming — the action is dying early, before on-chain confirmation.
Errors differ across attempts — could be multiple failure modes, an intermittent fault, or one root cause surfacing as several generic messages.
Tx history is retrievable by username, so we could see when the last successful send was.
What does "[this email] is not registered in this wallet" mean? Is this a known Crossmint error code?
I confirmed that this user's email as displated in the error message is reflected in his profile.
The double apostrophe after gmail.com is, obviously, not part of the user's email.

wallet object:

{
  "_id": {
    "$oid": "6901b9ee931048695fc9927b"
  },
  "idempotencyKey": "stellar-smart-wallet-create-with-user-687926e2aea7d3f60751da85-6901b9ed931048695fc99250",
  "projectId": "687926e2aea7d3f60751da85",
  "__v": 0,
  "address": "CAOONJZPRLSZ4BKCQZTZL4TAIIIEOMFRKXMNBYCBR4Y45MRWDZAH4CJO",
  "config": {
    "adminSigner": {
      "type": "email",
      "email": {
        "_isEncodedPII": true,
        "_id": "0ad9d3247a97f4b... (1118 chars truncated...)

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@changeset-bot

changeset-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 986a3e6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@crossmint/wallets-sdk Patch
@crossmint/wallets-quickstart-devkit Patch
@crossmint/wallets-playground-react Patch
@crossmint/client-sdk-react-base Patch
@crossmint/client-sdk-react-native-ui Patch
@crossmint/client-sdk-react-ui Patch
@crossmint/wallets-playground-expo Patch
@crossmint/auth-ssr-nextjs-demo Patch
@crossmint/client-sdk-nextjs-starter Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/wallets/src/utils/signer-locator.ts:11-26
**Duplicated normalization logic already exists in the same package**

`packages/wallets/src/utils/signer-validation.ts` already contains a private `normalizeEmail()` function that performs the exact same Gmail dot-stripping and `googlemail.com``gmail.com` substitution. This PR adds a second, functionally equivalent implementation (`normalizeEmailForLocator`) rather than extracting the shared logic. If the backend's normalization rules change (e.g., Gmail adds plus-addressing treatment or a new alias domain), only one copy will get updated, causing the locator comparison and the config-mismatch validator to diverge silently. The fix is to export `normalizeEmail` from `signer-validation.ts` and import it here.

### Issue 2 of 2
packages/wallets/src/utils/signer-locator.test.ts:42-45
**Lowercasing of non-Gmail domains is not tested**

`normalizeEmailForLocator` always calls `toLowerCase()` and returns `lower` for non-Gmail addresses — a silent behavior change from the original code that returned `signer.email` verbatim. For example, `"User@Company.Com"` now becomes `"email:user@company.com"` rather than `"email:User@Company.Com"`. The existing non-Gmail test only passes a pre-lowercased input, so this case is untested. A case like `{ email: "First.Last@Company.Com" }``"email:first.last@company.com"` should be added to confirm the behavior is intentional and matches what the backend stores.

Reviews (1): Last reviewed commit: "chore: add changeset for gmail dot norma..." | Re-trigger Greptile

Comment thread packages/wallets/src/utils/signer-locator.ts Outdated
Comment thread packages/wallets/src/utils/signer-locator.test.ts

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

🐛 1 issue in files not directly in the diff

🐛 Email signer locator in buildInternalConfig not normalized, causing approval matching failures for Gmail addresses with dots (packages/wallets/src/signers/descriptors/email.ts:28)

The PR adds Gmail dot normalization to getSignerLocator (packages/wallets/src/utils/signer-locator.ts:22) but does not update the locator construction in emailSignerDescriptor.buildInternalConfig (packages/wallets/src/signers/descriptors/email.ts:28), which still uses the raw email: email:${emailConfig.email}. This creates a mismatch: getSignerLocator produces email:firstlast@gmail.com but the assembled signer's locator() method (via NonCustodialSigner at packages/wallets/src/signers/non-custodial/ncs-signer.ts:34-36) returns email:first.last@gmail.com. When approving transactions/signatures, wallet.ts:1165 and wallet.ts:1216 compare s.locator() against pendingApproval.signer.locator (which uses the backend-normalized form). For Gmail addresses with dots, this comparison fails and throws InvalidSignerError, making Gmail users with dots in their email unable to approve any transactions or signatures.

Open in Devin Review

Comment thread packages/wallets/src/utils/signer-locator.ts Outdated
…InternalConfig

- Export normalizeEmail from signer-validation.ts and import in
  signer-locator.ts instead of duplicating the logic.
- Normalize the email locator in emailSignerDescriptor.buildInternalConfig
  via getSignerLocator so the NCS signer locator matches the backend form.
- Add test for non-Gmail lowercasing behavior.
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Re: Devin Review findings — both addressed in 986a3e6:

  1. buildInternalConfig locator bug — Now uses getSignerLocator(config) instead of inline email:${emailConfig.email}, so the NCS signer's locator() returns the normalized form and approval matching works correctly.

  2. Lowercasing flag — Intentional and matches the backend's normalizeEmail(). Added a test confirming this for non-Gmail domains.

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "fix(wallets): reuse shared normalizeEmai..." | Re-trigger Greptile

@albertoelias-crossmint albertoelias-crossmint merged commit 890d49a into main Jun 24, 2026
4 of 5 checks passed
@albertoelias-crossmint albertoelias-crossmint deleted the devin/1782324095-fix-gmail-dot-normalization branch June 24, 2026 19:43
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.

2 participants