Skip to content

External API contract tests: Alfredpay, Avenia, price feeds (milestones 2-4)#1254

Open
ebma wants to merge 4 commits into
stagingfrom
test/external-api-contracts-milestones-2-4
Open

External API contract tests: Alfredpay, Avenia, price feeds (milestones 2-4)#1254
ebma wants to merge 4 commits into
stagingfrom
test/external-api-contracts-milestones-2-4

Conversation

@ebma

@ebma ebma commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

Implements milestones 2–4 of docs/features/contract-tests.md, completing the external-API-contract layer for all planned services (SquidRouter landed as milestone 1). Per service, consumed-contract zod schemas live next to the client's types.ts and the same assertions run in two halves:

  • Hermetic (PR-blocking): each fake's output is schema.parse()d as part of the ordinary api test run, so a fake edited out of sync with the contract fails locally before push.
  • Live (nightly, non-blocking): the real client against the partner API. Errors from the call itself are inconclusive (warn + skip); only a successful response violating a schema fails. CONTRACT_EXPECT_LIVE=1 fails a suite where zero live calls completed.

The consumed field sets were derived from a full call-site trace of each client method — fields nothing reads are not asserted, and methods with no production consumers (Alfredpay getQuote; Avenia createOnchainSwapQuote/createOnchainSwapTicket/getMainAccountBalance/getAveniaSwapTicket) are deliberately uncovered.

Real drift found during live verification

  • Alfredpay 409 limit-error quantities are JSON numbers, not the strings our types assumed (observed live: "maxQuantity":86996891.21). The client now stringifies at the boundary so AlfredpayTradeLimitError.quantity stays a truthful string; the schema models the numeric wire.
  • FakeBrla.subaccountInfo had drifted: it served accountInfo: {} while production reads accountInfo.identityStatus. The fake now serves a scriptable identityStatus — exactly the verified-fake failure mode this layer exists to catch.
  • Pre-existing type lies fixed (called out in the PRD): BaseTicket.createdAt/updatedAt/expiresAt and PixInputTicketOutput.expiration are now string (wire truth); nothing consumed them as Date (confirmed by trace + clean monorepo typecheck).
  • Open question: Alfredpay getAllConfigs returns 400 {"errorCode":111301} against the production host. Handled as inconclusive here, but alfredpay-limits.service calls this in production — worth checking independently.

Safety properties of the live halves

  • Quote/config tests need only credentials; all transaction-creating and account-reading tests are additionally gated behind explicit sandbox fixture env vars (documented in apps/api/.env.example) and skip cleanly when unset.
  • At most one transaction per direction per run; nothing progresses past the point where a real payment would be required (Alfredpay onramp stays CREATED; the Avenia PIX pay-in ticket expires unpaid; payout tickets are hermetic-only since creating one live would move funds).

Operator action required

The nightly contracts.yml now reads sandbox credentials/fixtures from repository secrets: CONTRACT_ALFREDPAY_{BASE_URL,API_KEY,API_SECRET,CUSTOMER_ID,FIAT_ACCOUNT_ID,KYC_SUBMISSION_ID}, CONTRACT_BRLA_{BASE_URL,API_KEY,PRIVATE_KEY}, CONTRACT_AVENIA_SUBACCOUNT_ID, COINGECKO_API_KEY. Until provisioned, the nightly fails loudly by design (credential rot must not look green).

Verification

  • Full api suite: 424 pass / 0 fail; shared: 42 pass; bun typecheck and bun verify clean.
  • Live halves run locally where safe (quotes/configs/CoinGecko — read-equivalent calls production makes constantly): Alfredpay quotes both directions + limit-error contract, Avenia all three production quote shapes, CoinGecko simple/price all pass against the real APIs.

Milestone 5 (warn-only safeParse in the production clients) intentionally ships separately per endpoint once its schema survives a quiet week of nightlies, per the PRD.

ebma added 4 commits July 7, 2026 18:32
Milestone 2 of docs/features/contract-tests.md: consumed-contract zod
schemas for configs, quotes (both directions), onramp/offramp order
creation and polling, fiat accounts, KYC status, and the 409 trade-limit
error body — parsed against the fake (hermetic, PR-blocking) and the
partner API (live, nightly). Order-creating live tests are gated behind
pre-provisioned sandbox fixtures and stop before any payment.

Live verification surfaced a wire-type lie: the limit-breach
errorMetadata quantities arrive as JSON numbers, not strings — the
client now stringifies them at the boundary so
AlfredpayTradeLimitError.quantity stays a truthful string.

getQuote has no production consumers and is deliberately uncovered.
Milestone 3 of docs/features/contract-tests.md: consumed-contract zod
schemas for quotes, pix-key validation, PIX ticket creation/listing,
payout ticket status, limits, balances, and subaccount info. The
subaccount-scoped live tests are gated behind a pre-provisioned sandbox
subaccount; the only transaction created per run is a PIX pay-in ticket
that expires unpaid. Payout tickets are covered hermetically only —
creating one live would move funds.

Resolves the wire-type lie the PRD calls out: BaseTicket timestamps and
PixInputTicketOutput.expiration are ISO strings on the wire, not Date
(nothing ever consumed them as Date). The contract also caught FakeBrla
drift: subaccountInfo served an empty accountInfo while production reads
accountInfo.identityStatus — now scriptable on the fake.

createOnchainSwapQuote/Ticket, getMainAccountBalance and
getAveniaSwapTicket have no production consumers and are deliberately
uncovered.
Milestone 4 of docs/features/contract-tests.md: the simple/price shape
getCryptoPrice consumes, as a schema next to priceFeed.service.ts (its
future Milestone-5 warn-only parse site). FakePrices patches above the
HTTP seam, so the hermetic half is fixture-based; the live half mirrors
the service's request construction and asserts presence of the requested
ids. Falls back to the keyless public API when no COINGECKO_API_KEY is
set. Nabla/DIA rates are chain state — out of scope per the
no-chain-fidelity non-goal.

Adds zod to apps/api (catalog version, already a transitive dependency
via shared).
The nightly contracts workflow now passes sandbox credentials and
pre-provisioned fixtures from repository secrets (CONTRACT_ALFREDPAY_*,
CONTRACT_BRLA_*, CONTRACT_AVENIA_SUBACCOUNT_ID, COINGECKO_API_KEY).
Until those are provisioned, missing secrets resolve to empty and
CONTRACT_EXPECT_LIVE fails the run loudly — the intended rot alarm, per
the PRD.

.env.example documents the BRLA credentials and the sandbox-only
contract fixtures; testing-strategy.md and the PRD status line reflect
milestones 1-4 being implemented, with milestone 5 (warn-only production
parsing) shipping separately per endpoint after a quiet week of
nightlies.
@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for vortex-sandbox ready!

Name Link
🔨 Latest commit 1a6b335
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/6a4d2a719e9a3500080bedce
😎 Deploy Preview https://deploy-preview-1254--vortex-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for vortexfi ready!

Name Link
🔨 Latest commit 1a6b335
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/6a4d2a719e9a3500080bedcc
😎 Deploy Preview https://deploy-preview-1254--vortexfi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment on lines +2 to +19
import {
AccountLimitsResponse,
AveniaAccountBalanceResponse,
AveniaAccountInfoResponse,
AveniaFeeType,
AveniaOperationFee,
AveniaPayinTicket,
AveniaPayoutTicket,
AveniaQuoteResponse,
AveniaSubaccountAccountInfo,
AveniaSubaccountWallet,
AveniaTicketStatus,
Limit,
PixInputTicketOutput,
PixKeyData,
PixOutputTicketOutput,
UsedLimitDetails
} from "./types";

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR completes milestones 2–4 of the “external API contracts” layer by introducing consumed-field Zod schemas and contract suites for Alfredpay, Avenia/BRLA, and CoinGecko price feeds, with hermetic (PR-blocking) and live (nightly) halves to detect drift.

Changes:

  • Add consumed-contract Zod schemas + hermetic schema tests for Alfredpay and Avenia/BRLA in @vortexfi/shared.
  • Add new live + fake-backed contract suites in apps/api/src/tests/contracts/ for Alfredpay, Avenia/BRLA, and CoinGecko simple/price.
  • Align BRLA ticket timestamp types with wire reality (ISO strings) and update fakes/workflow/env docs to support nightly live runs.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/shared/src/services/brla/types.ts Change ticket timestamp fields from Date to wire-accurate ISO string.
packages/shared/src/services/brla/schemas.ts Add Avenia/BRLA consumed-field Zod schemas for contract verification.
packages/shared/src/services/brla/schemas.test.ts Add hermetic tests to pin accept/reject behavior of Avenia/BRLA schemas.
packages/shared/src/services/brla/index.ts Re-export BRLA schemas from the service barrel.
packages/shared/src/services/alfredpay/schemas.ts Add Alfredpay consumed-field Zod schemas (configs/quotes/tx/KYC/limits).
packages/shared/src/services/alfredpay/schemas.test.ts Add hermetic tests for Alfredpay schema accept/reject behavior.
packages/shared/src/services/alfredpay/index.ts Re-export Alfredpay schemas from the service barrel.
packages/shared/src/services/alfredpay/alfredpayApiService.ts Stringify numeric limit-error quantities at the boundary to keep error type truthful.
docs/testing-strategy.md Document expanded external contract coverage (Alfredpay/Avenia/CoinGecko).
docs/features/contract-tests.md Update PRD status and explicitly describe covered/uncovered endpoints.
bun.lock Add zod to the workspace lockfile.
apps/api/src/tests/contracts/pricefeeds.contract.test.ts Add CoinGecko contract suite (fixtures + optional live).
apps/api/src/tests/contracts/avenia.contract.test.ts Add Avenia/BRLA contract suite (fake + live with fixture gating).
apps/api/src/tests/contracts/alfredpay.contract.test.ts Add Alfredpay contract suite (fake + live with fixture gating).
apps/api/src/test-utils/fake-world/fake-anchors.ts Update BRLA fake outputs to match consumed contract (identityStatus, ISO expiration).
apps/api/src/api/services/priceFeed.schemas.ts Introduce Zod schema for consumed CoinGecko simple/price response shape.
apps/api/package.json Add zod dependency for new price feed schema usage.
apps/api/.env.example Document new fixture env vars for live contract tests.
.github/workflows/contracts.yml Wire nightly live contract runs to repository secrets for sandbox credentials/fixtures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +19
import { z } from "zod";
import {
AccountLimitsResponse,
AveniaAccountBalanceResponse,
AveniaAccountInfoResponse,
AveniaFeeType,
AveniaOperationFee,
AveniaPayinTicket,
AveniaPayoutTicket,
AveniaQuoteResponse,
AveniaSubaccountAccountInfo,
AveniaSubaccountWallet,
AveniaTicketStatus,
Limit,
PixInputTicketOutput,
PixKeyData,
PixOutputTicketOutput,
UsedLimitDetails
} from "./types";
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