Skip to content

Create new production release - #1390

Open
ebma wants to merge 86 commits into
mainfrom
staging
Open

ebma wants to merge 86 commits into
mainfrom
staging

Conversation

@ebma

@ebma ebma commented Sep 29, 2026

Copy link
Copy Markdown
Member

No description provided.

ebma and others added 30 commits September 19, 2026 16:53
The opportunistic in-range path quoted, sized, and balance-checked a fresh
USDC->BRLA->USDC run before the state machine got a chance to resume a run
that died mid-flow. With the in-flight USDC no longer in the wallet, that
balance check failed on every cron run and the stuck state was never
resumed. The out-of-range path had its own resume check; hoist a single
one to the top of checkForRebalancing for both Base flows.

Also stop selecting the profitable USDC->BRLA amount when the Base USDC
balance cannot fund it: fall back to the standard amount instead of
crashing after two quote round-trips.
Dry-run by default: prints the persisted USDC->BRLA->USDC state from
Supabase Storage. With --confirm it resets the phase to idle while keeping
history, for runs whose funds were reconciled manually.
…ming

Resuming a persisted run in dry-run mode would write state and move funds
during an invocation the spec defines as read-only, and resuming before the
coverage read let funds move when the indexer read would have failed.
Both were latent on the out-of-range paths before the resume was hoisted.
The reset script and a live run write the same Supabase object without
locking, and the script's closing message contradicted the README's
reconcile-first order.
Continuing into the fresh evaluation quoted and sized a run that assumes
the in-flight USDC is still in the wallet, and logged "no rebalancing
needed" while a run was paused.
The affordability gate read the wallet balance even when the policy mode
is off, which the spec defines as returning before any balance read.

The resume-before-fresh-evaluation ordering, the dry-run pause, the
--restart bypass and the profitable-amount gate had no automated
coverage because index.ts runs the cycle at import. Move that
orchestration into rebalanceCycle.ts behind injected dependencies so a
later refactor cannot silently reorder it.
…-flight

Resume in-flight rebalancer runs before sizing a fresh one
A non-JSON error response from Squidrouter (a Cloudflare or load-balancer
error page) was collapsed to `{}` and the HTTP status was not logged, so
production logs showed "Error fetching route from Squidrouter API: {}"
with no way to tell what the upstream actually returned.
The nightly e2e smoke test failed because production's POST /v1/quotes
answered 500: Squidrouter's /route endpoint returned a non-JSON 5xx from
the gateway in front of it. Production logs show this ~1-2 times per hour,
each one a user-facing 500, while the same request succeeds a second later.

Extend the existing retry-once path (previously only for rate limits) to a
5xx whose body is not Squid's JSON error shape. Squid's own deterministic
errors, such as low liquidity, keep failing fast without a retry.
…e read

`response.text()` rejects when the gateway truncates the error stream.
That escaped as a bare TypeError, losing the HTTP status and skipping the
gateway retry, whereas the previous `.json().catch()` still produced an
HttpError. Treat an unreadable body as empty so the status is logged and
a 5xx is still retried.
…y spec

The Squid integration spec said only 429s are retried and every other
error fails fast. It also described the 429 handling as exponential
backoff, whereas `getRoute` retries once after the advertised retryAfter.
…retry

Retry Squidrouter route requests that fail at the gateway
The hook is a leftover from the standalone rebalancer repo. Run from
apps/rebalancer, husky 9 stops at ".git can't be found", so it never
installed anything; the root prepare already installs the hooks for the
whole repo. Because the rebalancer does not depend on husky, bun can also
run the hook before the root's binary is linked, which fails a cold
`bun install --frozen-lockfile` with exit 127, as seen in CI and in
`bun bootstrap:worktree`.
…pare

Remove the rebalancer's redundant husky prepare hook
The API rejects with "Invalid pixKey or receiverTaxId." (trailing period), so the exact-match
comparison never fired and server-side rejections surfaced as a generic VortexSdkError.
GET /v1/ramp/{id}?showUnsignedTxs=true returned the raw unsignedTxs, so a
client could fetch the user's source-of-funds transactions for a SELL ramp
before every ephemeral presign was received and validated, bypassing the
gate that register and update enforce.
…apping

Map the API's invalid pixKey rejection to InvalidPixKeyError in the SDK
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>
ebma and others added 30 commits September 29, 2026 15:55
A failed Squid fetch installed the static-only fallback and set isLoaded,
which turned every later initializeEvmTokens() call into a no-op. Squid-only
tokens such as PAXG then stayed unlisted and unquotable until the process
restarted, and the fetch had no timeout, so a hung request stalled boot.

Track whether the Squid list itself loaded, guard on that instead, resolve
to whether it did, and bound the fetch at 10 s. The static fallback on
failure is unchanged.
Boot awaited a single initializeEvmTokens() call, so one failed Squid
request left the API on static tokens until the next restart. Wait for the
first attempt only, then retry every 60 s until the list loads.
The business EUR onramp account and its deposit events share the EUR provider activation that production is still waiting on, so they carry the same sandbox note as EUR buys.
One failed status request ended the check, an expired attempt polled for
five minutes, and a rejection showed Avenia's English reason code and sent
the user back to the old attempt. Tolerate transient and reconciliation
errors, stop on every final state, explain it in Portuguese and return to
the form for a new attempt; stop polling when the modal closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form accepted any 11 digits, so a typo created a subaccount bound to
the wrong CPF, and the API is about to reject such CPFs outright.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keys were stored for every ramp and never removed. A completed ramp's
ephemeral accounts are swept with presigned cleanup transactions, so only
failed ramps' keys can still recover funds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page stayed a spinner until the Privy chunk (about 750 kB gzipped)
loaded and Privy reported ready. App now renders outside PrivyProvider,
which hands its auth state up, so the landing paints from the entry chunk
and only the sign-in buttons wait; returning users keep the loader so the
landing does not flash before their dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found the own-account path sent EUR users to production keys, skipped EUR wallet linking and IBAN provisioning, omitted the country for the payout-account lookup, did not say direct clients sign the ephemeral transactions, and assumed verification creates the payout account.
The API derives the EUR permit owner from the provider binding and ignores walletAddress, which only the SDK uses; accounts holding both legal types must also send customerType.
The sell example reused the buy quote, so registerRamp dispatched it to the onramp handler, and it indexed an empty account list for sellers who had not added a pay-out account.
listDomesticFiatAccounts takes the exported DomesticCountry string enum, so the "MX" literal fails TypeScript type-checking.
…own-account

Document the dashboard route to API keys and an own-account ramp path
The API refuses to record or start a ramp 15 minutes after registration.
A user-wallet transfer or Squid swap broadcast later moves the funds to
the ephemeral of a ramp that can never start, leaving them for manual
recovery. Refuse each broadcast once less than four minutes remain;
typed-data permits move nothing until Vortex executes them, so they stay
unguarded.
…guard

Stop widget sell broadcasts after the ramp start window closes
Unblock gold purchases and harden the gold app
The modal re-ran its focus effect whenever onClose changed identity, and
every caller passes a new function on each render. The balance poll
re-renders the app every 30 seconds, so focus jumped from the field being
typed in to the close button, where the next Space or Enter closed the
flow and discarded the form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing the modal aborted the wait between polls but not a status request
already in flight, so a late approval still called onApproved and, in the
sell flow, fetched a new quote for a flow that was gone. The abort path had
no test at all; cover both pollers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 404 or 409 from the KYC status check that never cleared reached the
screen as the API's English text after five polls, and a new attempt the
API refused (Avenia did not mark the old one retryable) showed an English
409 behind a button that only resubmitted it. Both now say in Portuguese
what happened and point to support with the request code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Avenia's tax_id reason means the CPF was not found, not that it differs
from the document, and the new messages drifted from the app's wording
("de novo", the mobile-only "toque em", advice Avenia does not give).
Assert every reason's message and that no reason code leaks through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rejection returns to the long form with its explanation below the
fields, out of view on a phone and not announced; it now scrolls into
view as an alert. After approval the step no longer stays on the checking
spinner: when the sell quote still finds the account unapproved while
Avenia syncs, the user can continue instead of waiting forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared CPF check rejects digit runs, and 01234567890 is the one run
besides repeated digits whose check digits are valid, so gold accepted it.
The field error now names its input for screen readers and sits under the
field instead of centred with the OTP step's margin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Google sign-in returns to the page with privy_oauth_code before Privy
has stored its session, so the landing flashed by again; the session
check now counts that return and lives in a tested helper. A Privy that
failed to initialize left a disabled button with no explanation, and a
returning user on an endless loader; the error Privy reports now ends the
wait and says to reload. The sign-in buttons show that Privy is loading,
and the Privy subtree is kept in state because useMemo may be discarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Storing and deleting ephemeral keys skipped db.close() when the
transaction rejected, and the caller swallows delete errors, so a failure
left a connection open that blocks a later upgrade of the store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-retry

Retry the Squid token list when it fails to load at boot
Gold: faster landing, sturdier KYC polling, CPF check and key cleanup
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