Skip to content

fix(deeplink): recognize address-less algorand:// asset opt-in QRs [PERA-4744] - #1107

Merged
filipetamota merged 2 commits into
mainfrom
fmota/pera-4744
Jul 31, 2026
Merged

fix(deeplink): recognize address-less algorand:// asset opt-in QRs [PERA-4744]#1107
filipetamota merged 2 commits into
mainfrom
fmota/pera-4744

Conversation

@filipetamota

Copy link
Copy Markdown
Collaborator

Pull Request Template

Description

Scanning an asset opt-in QR of the form algorand://?amount=0&asset=<id> did nothing on iOS + Android — the scanner didn't recognize it (functional on native Pera 6). This is the format Pera's opt-in QR generator emits (confirmed by decoding the reported QR: algorand://?amount=0&asset=1152109334).

An opt-in is a self opt-in, so the QR has no receiver address — the authority is empty. parseAlgorandUri ran its !address || !isValidAlgorandAddress(address) guard before the opt-in branch, so the ARC-90 path returned null for every address-less opt-in and parseDeeplink reported it as unrecognized. The perawallet:// old-parser already accepts the address-less form, and downstream useAssetOptInDeeplink already prompts the user to pick an account when the link carries none — only the algorand:// path was out of parity.

Moved the opt-in resolution ahead of the address guard: amount === '0' + an asset now returns ASSET_OPT_IN with the address only when one is present and valid, otherwise undefined (handler prompts for the account). The change is tightly scoped — every transfer path still requires a valid receiver, so algorand://?amount=100 and other address-less transfers stay null, and algorand://<addr>?amount=0&asset=<id> keeps its address.

Added parser tests: the real reported QR → ASSET_OPT_IN with no address; an opt-in that carries an address keeps it; and an address-less non-opt-in transfer still returns null.

Related Issues

Closes https://algorandfoundation.atlassian.net/browse/PERA-4744

Checklist

  • Have you tested your changes locally?
  • Have you reviewed the code for any potential issues?
  • Have you documented any necessary changes in the project's documentation?
  • Have you added any necessary tests for your changes?
  • Have you updated any relevant dependencies?

Additional Notes

  • Add any additional notes or comments that may be helpful for reviewers.

@filipetamota
filipetamota requested a review from a team as a code owner July 31, 2026 11:00
@yasin-ce

yasin-ce commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Should-fix

A present-but-corrupt address is now discarded instead of rejectedalgorand-parser.ts:59,64. hasValidAddress ? address : undefined collapses two different situations into one:

algorand://NOTANADDRESS?amount=0&asset=123   => ASSET_OPT_IN, no address
algorand://5CYN…JDJGHE?amount=0&asset=123    => ASSET_OPT_IN, no address   (last char dropped)

A truncated or misread QR used to return null and surface as "unrecognized". Now it opens the account picker as though the link never carried an address — the user opts a different account in and nothing says the link was damaged. Absent is a legitimate opt-in form; malformed isn't. Could we keep them apart? Adding the invalid case before the branch keeps the existing guard untouched:

if (address && !isValidAlgorandAddress(address)) {
    return null
}

if (amount === '0' && assetId) {
    return {
        type: DeeplinkType.ASSET_OPT_IN,
        sourceUrl: url,
        assetId,
        address: address || undefined,
    } as AssetOptInDeeplink
}

That preserves everything the PR is after (algorand://?amount=0&asset=… → prompt) and still rejects corruption. Worth a fourth test either way — it's the riskiest behavioural edge in the diff and the one that isn't covered.

The comment is doing more work than the code needsalgorand-parser.ts:51-57. Seven lines for a two-line rule; the non-obvious bit is only why the ordering matters, the rest restates the branch and re-explains what an opt-in is. Something like:

// Address-less by design: an opt-in has no receiver, so this must resolve
// before the address guard below.

Same for the 4-line comment on the first new test — the test name already says it.

Nits

  • hasValidAddress earns its own line only to be consumed once (:59). With the guard above it reduces to address: address || undefined.
  • as AssetOptInDeeplink is redundant now that address?: string — the literal already satisfies the interface. Pre-existing, and the old parser does the same; not this PR's problem.

Questions / awareness

  • Non-numeric asset ids ride straight through — not this PR, but the diff widens who reaches it. asset=abc and asset=-1 both yield ASSET_OPT_IN with assetId: 'abc' / '-1'. Downstream that's BigInt(assetId) in useAssetOptInDeeplink.ts step 3 — inside the try, so it's an error toast not a crash, but only after the account picker and the confirmation sheet. And the catch hands it to showError, whose comment promises typed AlreadyOptedInError / InsufficientBalanceForOptInError render readably — a raw SyntaxError from BigInt('abc') isn't one of those. Notable asymmetry: arc90-parser.ts:92,99 already enforce /^\d+$/ on the asset/<id> and app/<id> path forms ("ARC-90: assetid = 1*DIGIT"), but nothing validates the asset= query param. Ticket it?
  • type=appl also lands herealgorand://?type=appl&amount=0&asset=123 resolves to ASSET_OPT_IN, because arc90-parser.ts:109-110 maps appl to noop and noop shares the branch with payment. Pre-existing shape, newly reachable without an address. Probably harmless; flagging for awareness.
  • Only the exact string '0' countsamount=0.00 returns null. Matches old-parser.ts:105 and ARC-26 amounts are integers, so I think this is right. Do we know native Pera 6 is equally strict, given parity is the stated goal?
  • The guard at algorand-parser.ts:111 is the keyreg branch and should stay as-is — a keyreg genuinely needs a sender. Worth noting so it doesn't get "consistency-fixed" alongside this one.
  • old-parser.ts:105 is now the same rule written twice, once per scheme. Not worth unifying for this fix; worth remembering next time either side changes.

@filipetamota

Copy link
Copy Markdown
Collaborator Author

@yasin-ce

Addressed the corrupt-vs-address-less case as suggested: a present-but-invalid address now returns null (unrecognized) while a genuinely address-less link still opts in, with a test covering both a truncated and a non-address string. Trimmed the comment/removed hasValidAddress too, and filed PERA-4753 for the non-numeric asset= validation.

@filipetamota
filipetamota merged commit 269c041 into main Jul 31, 2026
15 checks passed
@filipetamota
filipetamota deleted the fmota/pera-4744 branch July 31, 2026 12:10
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