Unblock gold purchases and harden the gold app - #1388
Merged
Merged
Conversation
The gold unit tests cover quote validation, recovery checkpoints and gas preflight, but no root script ran them. Wiring them into .github/workflows/ci.yml is left to a maintainer (see the PR description). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The standalone gold.satoshipay.io release sent nosniff, referrer and permissions headers; the Netlify-served /pt-br/gold/ sends none and can be framed by any site while it signs transactions and collects PIX keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hero PNG was 1.96 MB and the wordmark 273 KB on the first screen; the WebP is 209 KB and the wordmark keeps 3x its largest display size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chart only fetched 42 days, so 1A and Tudo showed six weeks, and the change badge always showed the 42-day change whatever the period. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One failed status request ended polling and sent the buyer to the support escalation screen although the ramp kept running server-side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The countdown decremented once per timer tick, and browsers pause timers while the buyer is in the banking app, so it showed time that had passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fee disclosure and legal notes used 11px #837d74 on #fffdf9 (4.0:1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gold asked /v1/ramp-info whether the user passed KYC, but that route only accepts API credentials and reports on their owner, never on the OTP session. Production sets no public key, so every buy stopped after the e-mail code with "A public or secret API credential is required" and every sell looped back to the code. /v1/brl/getUser resolves the session user's approved Avenia account; skip KYC only on CONFIRMED, as the widget does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /v1/ramp/:id returns unsignedTxs only with showUnsignedTxs=true and the SDK status call never sends it, so "Continuar esta venda" always failed with "Não foi possível recuperar as confirmações" after a declined wallet prompt, a reload or a receipt timeout, although the copy invites the user to retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both buttons resume the stored operation, and it was only cleared on a terminal status. The API never expires an unstarted ramp, and a failed sell was never cleared, so one abandoned PIX or failed sell locked the wallet out of buying and selling on that device for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Status responses have no expiresAt, so a resumed PIX screen invented a fresh ten-minute countdown; start is refused 15 minutes after creation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gold sent no locale, so the Vortex OTP e-mail arrived in English and the API reset the user's stored locale to en-US; the template renders Portuguese for pt-BR, as the widget requests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Any refresh error cleared the stored session, so a network blip or a 503 from /v1/auth/refresh logged the user out mid-operation. The security spec (Supabase OTP rule 9) allows teardown only when the refresh returns 401. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API passes country and countryOfTaxId through to Avenia unchanged. The widget's KYC (packages/kyc) sends "BRA", gold sent "BR". No unit test: the payload is built inline in KycStep, which the node test runner cannot load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every buy and sell asked for a new e-mail code, even with a valid Vortex session in the same tab. Like the widget, the session now keeps the e-mail it was verified for and is reused only for that address; a rejected session falls back to the code step. "Voltar" no longer leads into a code step the user may have skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
✅ Deploy Preview for vrtx-dashboard canceled.
|
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vortexfi ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
With a reusable session the amount step now requests the quote itself, so an amount typed meanwhile was shown on the review as the purchase value while the PIX is for the quoted amount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The amount step now shows the flow error, so going back from a wrong e-mail code or a refreshed quote left that message under the amount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Failed and abandoned operations are now cleared so they stop blocking new ones, which also removed the only place on the device that showed their operation code; support needs it when a PIX was debited or a sell stopped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resuming a sell now signs transactions fetched from the ramp status, and neither the SDK nor the API checks what the user's wallet signs. Only the approve of at most the quoted PAXG to Squid's router and the router swap may reach the wallet; the unused typed-data handler is dropped, so any other transaction kind fails closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API refuses to record or start a ramp 15 minutes after registration, so a swap sent later, for example when resuming an old sell, moves the gold to the ephemeral account of a ramp that can never run. Stop four minutes before the deadline and release the dead ramp instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 29, 2026
This was referenced Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A review of
apps/gold(ouro. by Vortex, live at/pt-br/gold/) found that no real purchase can complete in production, plus recovery, security and UX defects. This PR fixes what can be fixed safely in the gold app and its Netlify config. API-side findings are listed at the end for follow-up. Every commit is self-contained and passes the gold suite.Blocking: KYC readiness used a credential-only endpoint
getBrazilBuyReadinesscalled/v1/ramp-infothrough the SDK. That route only accepts API credentials (validatePublicKey,apiKeyAuth) and reports on the credential owner, never on the OTP session.The production build sets no
VITE_VORTEX_PUBLIC_KEY: the inlined env object in the live bundle only containsVITE_SIGNING_SERVICE_PATH, andGET /v1/ramp-infoanswers401 CREDENTIAL_REQUIRED. So every buy stopped after the e-mail code with "A public or secret API credential is required", and every sell looped back to the code.Gold now reads the session user's own Avenia account through
/v1/brl/getUser, which uses the same resolution as ramp registration. It skips KYC only onidentityStatus === "CONFIRMED", asvalidateKyc.actor.tsdoes.Recovery
GET /v1/ramp/:idreturnsunsignedTxsonly withshowUnsignedTxs=true, and the SDK never sends that. So "Continuar esta venda" always failed after a declined wallet prompt, a reload or a receipt timeout.initialone hour aftercreatedAtcounts as failed. The unhandled-payment worker starts paid PIX ramps well before that (it runs every 15 minutes).createdAt + 15 minwhen a status response has noexpiresAt.401.Correctness and UX
locale: "pt-BR", as the widget does.countryandcountryOfTaxIdchange from"BR"to"BRA", matchingpackages/kyc. The API passes both unchanged to Avenia.#837d74(4.0:1) to 12px#6b665f(5.6:1).Security, CI and performance
Headers. Netlify now sends these headers for
/pt-br/gold/*:frame-ancestors 'none'andX-Frame-Options: DENY, because the page signs transactions and collects PIX keys.nosniff, a referrer policy and a permissions policy.The standalone gold.satoshipay.io release had the latter set; the Netlify move dropped them. The headers are scoped to gold, so the widget stays frameable.
Tests in CI. The root
testscript and thetestjob in.github/workflows/ci.ymlnow run gold'snode --testsuite (bun run test:gold), which never ran before.Images. The landing hero goes from a 1.96 MB PNG to a 209 KB WebP, and the wordmark from 273 KB to 34 KB.
Added in review
0xce16…D666) and the router swap; anything else fails before the wallet prompt. The unused typed-data handler is removed.updateRampandstartRamprefuse 15 minutes after registration, so a swap sent later (e.g. resuming an old sell) strands the gold. Gold refuses to broadcast within 4 minutes of that deadline and releases the dead ramp.🧪 Gold testsin thetestjob).Test plan
bun run test:gold: 33 pass. There were 17 on staging, and each new test fails there.bunx vitest run src/tests/netlify-headers.test.ts src/tests/netlify-redirects.test.tsinapps/frontend. The headers test fails without thenetlify.tomlchange.bun run build:goldwith the production env.testjob.curl -I <preview>/pt-br/gold/(and/index.html) shows the new headers;/pt-BR/gold/301s to it,/pt-br/widgethas none.Follow-ups
API
Gold
🤖 Generated with Claude Code