follow-up(#16): bind integrator account= on /sign/solana #17
Labels
No labels
agent:implement
agent:ready
api
bot
bug
ci
enhancement
ready
security
terra-classic
testing
ux
web
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-ecosystem-legal#17
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Follow-up to #16 (EVM
account=remaining coverage) and the same integrator contract as Terra #11 / EVM #15.SDK
TermsGate/buildSignUrl({ account })(0.1.1+) putsaccount=on every network sign URL, includingsign_urls.solana./sign/terra-classicand/sign/evmbind that query and refuse a different connected wallet./sign/solanaignores it and signswindow.solana’s pubkey.Current codebase
web/src/pages/solana.tsis still a one-file shell:property; readsapp_name/redirect_urionly — does not callgetClaimedAccount().signShell, no terms/consent (#2 postponed).window.solanaor throwNo Solana wallet found;connect();accountId = publicKey.toString(); build canonical message with that id;signMessage(utf8); POSTnetwork: "SOLANA".getStatusalready-signed short-circuit (EVM/Terra have this after connect).btn.disabled = trueonly intry; catch re-enables. No sharedsignShellfinallybusy reset.web/src/query.tsgetClaimedAccount()already trimsaccountfor all networks. Solana page never reads it.packages/cl8y-clickwrap/src/urls.tsappendSignParams/buildSignUrlsetaccounton the Solana URL.TermsGateAccept usesNETWORK_SIGN_URL_KEYS.Solana→solana.api/src/account.rsSOLANA: bs58 decode must be 32 bytes; string is stored as submitted (do not lowercase — base58 is case-sensitive).api/src/verify/solana.rsed25519 over0xff || "solana offchain" || LE u64 len || msg. Portal signs raw UTF-8 (signMessage(..., "utf8")). Known P0 envelope mismatch (gaps/GAP_1786322222.md§ Incomplete / Security #2).web/e2e/sign-pages.spec.ts: property + button only (“terms disclosure postponed”). No Solana unit tests, no claimed-account tests, no wallet mock e2e.#4explicitly excluded Solana e2e.web/src/evm/account.ts/ Terraweb/src/terra/chain.ts+ mismatch e2e.Why this implementation is needed
Without a portal bind, an integrator (DEX/bridge/voting-class) that connected Phantom A and redirected with
account=<A>can still accept terms for Phantom B if the user switches wallets on the portal. The dApp then polls A and stays unsigned, while the portal shows success for B. Terra/EVM already fail closed with This page is for a different wallet. Solana is the remaining clickwrap network that drops that contract.SDK 0.1.1 already sends
account=on Accept fornetwork="Solana". The hole is only the portal page.Constraints / guardrails
toLowerCase(). Canonicalize by: trim → bs58 decode → exactly 32 bytes. Compare decoded bytes (or a single canonical bs58 encode of those bytes). Invalid claimed (0x…,terra1…,javascript:, short/high-bit garbage) → fail closed, same retail mismatch copy as EVM/Terra if possible.accountis not a URL. Never pass it tolocation,<a href>, or success redirect. Sign as … is a text node (el()/textContent). NoinnerHTML.account_id+ signature. Do not trust queryaccountonPOST /api/v1/signatures/wallet.signed_lateste2e will fail today because UTF-8signMessage≠ API envelope — see Out of scope / envelope note./sign/solana.#2terms disclosure /renderSignShellas a drive-by. Optional: show Sign as on the existing card. If you adoptsignShell, that is a documented expansion and must keep consent-gate invariants; default recommendation is a thin page change +web/src/solana/account.ts.#15/ Terra#9/#11class of work).window.solanaonly, matching today’s page.getStatus(property, "SOLANA", bound)issigned_latest, show success without forcing anothersignMessage(parity with EVM/Terra). Fetch status only after connect (no pre-connect probe).buildWalletMessage/Content-SHA256lockstep (#6). Do not invent a Solana-only message format.window.solanaonly. No real Phantom.Relevant files
web/src/pages/solana.ts(implementation)web/src/query.tsgetClaimedAccountweb/src/solana/account.ts+account.test.ts(mirror EVM helper)web/src/ui.tsel/renderSuccessweb/src/api.tsgetStatus/submitWalletweb/src/base58.ts(signature encode; pubkey decode should use a decode that matches APIbs58)api/src/account.rsSOLANAarmapi/src/verify/solana.rs(do not weaken)packages/cl8y-clickwrap/src/urls.ts/TermsGate.tsx(already passesaccount)web/e2e/sign-pages.spec.tsskills/portal-sign-disclosure/SKILL.md(out of scope unless signShell)gaps/GAP_1786322222.mdSolana envelope + already-signed rowsRecommended direction
canonicalizeSolanaAddress/assertSolanaAccountContinuitynext to EVM’s helper. Reuse mismatch copy: This page is for a different wallet. Reconnect that account and try again.renderSolana:claimedAccount = getClaimedAccount(); if set, prepend Sign as <claimed> (text). On connect,bound = assertSolanaAccountContinuity(claimed, publicKey.toString()). Build message / submit withboundonly.getStatus(property, "SOLANA", bound); ifsigned_latest,renderSuccessand return.0x/terra1/ empty / wrong length) and mismatch / no-claim pass-through / byte-equal encodings if you canonicalize encode.window.solanaconnect +signMessage. Mismatch e2e does not need API verify success. Matching e2e may assert portal did not show mismatch and calledsignMessagewith the bound id; do not requiresigned_latestuntil the envelope P0 is fixed (or mock the API). Document which bar you chose in the MR.catch— keep fail path enabling the button).Acceptance criteria
account=<valid bs58 32-byte>on/sign/solana?property=…, page shows Sign as <that pubkey> as text.submitWallet, no Accepted heading, claimed account remains unsigned.account→ fail closed; not used ashref/ navigation.accountquery → current behavior (sign whateverwindow.solanareturns, then API binds thataccount_id).signed_latestfor the bound account → success without a second signature.normalize_account/verify_solanaunchanged except if envelope work is explicitly in the MR (default: unchanged).cd web && npx vitest run src/solana src/query.test.ts(and page tests if added) green; Playwright workers=5 for any new spec.Test plan (functional paths)
accountIdis mock pubkey (existing submit path).account== mock pubkey → Sign as visible → sign uses that id.getSignatureStatus/ API already true → success,signMessagenot required.window.solana: existing error; CTA usable again.property: existing missing-property page (unchanged).app_name/redirect_uri: still work; success redirect still allowlisted (web/src/redirect.ts).accountmust not replaceredirect_uri.Test plan (attack / abuse)
account=javascript:…/data:/https://evil: not an href; bind fail-closed; Sign as text-only if rendered.account=0x+ 40 hex orterra1…: invalid Solana pubkey → mismatch, no submit.account=→ can sign another wallet and see portal success; integrator polling the original pubkey stays unsigned. Do not authenticate the query param.account=to a key they control → they may accept for that key; dApp still polls its store. Document; no server ACL on the query.Verification criteria
/sign/solana?property=<registered>&account=<phantom A>with wallet B connected must not Accepted.Out of scope
renderSignShell).solana offchainprefix) — still a P0 in the gap doc; do not bury it as a side effect unless the MR is explicitly dual-scoped.account=(numeric user ids, not Solana pubkeys).marked as related to #16
marked as related to #2
marked as related to #4
mentioned in commit
4f2904960cVerify — 2026-08-31 (
4f29049onmain)Implemented portal
account=bind on/sign/solana(GitLab #17). Envelope P0 (UTF-8signMessagevs APIsolana offchain) is unchanged — matching e2e does not requiresigned_latest.What landed
web/src/solana/account.ts— trim → bs58 decode → 32 bytes; compare decoded bytes; never lowercaseweb/src/pages/solana.ts— Sign as text node; bind before sign/submit;getStatusonly after connect; already-signed skipssignMessage; button re-enables on mismatch/missing walletweb/src/base58.tsbase58ToUint8lockstep with APIbs58web/e2e/solana-sign.spec.ts(workers=5, mockwindow.solana)skills/solana-account-bind/SKILL.mdAPI
normalize_account/verify_solananot modified.Verification checklist (issue body)
account=shows Sign as <pubkey> as a text nodeaccount_iduse that idsubmitWallet, no Accepted, claimed stays unsignedjavascript:/terra1/0x→ fail closed; not anhref; button re-enablesaccountquery → signs mockwindow.solanapubkeysigned_latest(mocked status) → success withoutsignMessagecd web && npx vitest run src/solana src/query.test.tsgreen (plussrc/base58.test.ts)cd web && npx playwright test --workers=5 e2e/solana-sign.spec.ts e2e/sign-pages.spec.tsgreenMatching bar chosen: assert
signMessage+ POSTaccount_id, do not require Accepted /signed_latest(envelope P0).All #17 criteria pass. Closing.