You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Vendors can register but can never be approved, so no vendor can ever receive a loan.
vendor-registry-contract gained a full approval lifecycle in Contracts issue #7: register_vendor() now sets status = VendorStatus::Pending instead of activating immediately, and approve_vendor() and suspend_vendor() were added as admin-only functions. is_active() returns true only for Approved, and creditline-contract's validate_vendor() calls is_active().
Nothing in this API calls any of it. grep -rn "approve_vendor\|approveVendor\|VendorStatus\|suspend_vendor" src/ returns zero matches across the entire repo. VendorRegistryContractClient exposes only isVendorActive() and getVendor(), both read-only. There is no write path to the vendor registry contract at all.
The off-chain side has the same hole. The vendors table created in supabase/migrations/20260504000001_create_vendors_table.sql has no status, approved, or verified column, so there is nowhere to record approval state even locally.
The user-facing outcome: a vendor completes registration through POST /vendors, appears in GET /vendors, shows up in the web app, and is permanently stuck in Pending on-chain. Any learner who tries to take a loan against that vendor is rejected with VendorNotActive. The protocol has no path to onboard a single working vendor.
Ground Rules
This issue must be solved according to StepFi's established engineering standards. Before writing any code:
Read context/architecture-context.md in full
Read context/code-standards.md in full
Read context/progress-tracker.md to understand what has already been built and why, so you do not duplicate or contradict recent work
Read src/modules/vendors/vendors.controller.ts and vendors.service.ts in full to follow the existing controller and service split
Read src/modules/liquidity/liquidity.service.ts for the established pattern of building unsigned XDR and returning it to the client for signing. Clients never call the chain directly
Read src/common/interceptors/audit.interceptor.ts and src/common/decorators/audit-action.decorator.ts, since approval and suspension are privileged actions that must be audit-logged
Read contracts/vendor-registry-contract/src/lib.rs in StepFi-Contracts for the exact approve_vendor and suspend_vendor signatures
This issue depends on the companion issue that introduces AdminGuard and the ADMIN_WALLETS allowlist. There is currently no admin concept in this API. Do not invent a second one here. If that issue has not landed, coordinate rather than duplicating.
Your PR will be rejected regardless of whether CI passes if it conflicts with anything in these files, introduces patterns inconsistent with what already exists, or duplicates functionality that was solved differently elsewhere in the codebase.
What To Build
Add a migration in supabase/migrations/ adding a status column to vendors, constrained to pending, approved, suspended, rejected, defaulting to pending. Timestamp-prefix the filename. Never edit an applied migration. Backfill existing rows to pending so local state matches the on-chain default.
Add write methods to VendorRegistryContractClient: buildApproveVendorXdr(admin, vendor) and buildSuspendVendorXdr(admin, vendor). These build unsigned XDR. They must not submit. Follow the XDR-building pattern in liquidity.service.ts.
Add POST /vendors/:id/approve and POST /vendors/:id/suspend to vendors.controller.ts, both guarded with @UseGuards(JwtAuthGuard, AdminGuard).
Both endpoints return unsigned XDR for the admin wallet to sign, consistent with every other state-changing operation in this API. Do not hold an admin key server-side.
Update the local status column only after the signed transaction is confirmed. Hook this into the existing transaction-status-checker job rather than optimistically writing on XDR build, so a signed-but-never-submitted transaction cannot leave the database claiming a vendor is approved when the chain disagrees.
Reject the transition if the vendor is not currently Pending, matching the contract's VendorNotPending error, and return a 409 with a clear message rather than letting the contract call fail opaquely.
Decorate both endpoints with @AuditAction so approvals and suspensions land in the audit log.
Add status to the vendor DTOs in src/modules/vendors/dto/vendor.dto.ts so clients can distinguish pending from approved vendors.
Full Swagger decorators on both endpoints: @ApiOperation, @ApiResponse for 200, 401, 403, 409, and @ApiTags.
The admin UI for driving these endpoints is out of scope here. That belongs to StepFi-Web core: build full Stellar event indexer service #24 (protocol governance panel). Do not build a UI in this repo.
POST /vendors/:id/approve returns unsigned XDR for an allowlisted admin wallet
Both new endpoints return 403 for an authenticated non-admin and 401 when unauthenticated
Approving a vendor that is not Pending returns 409, not an opaque contract failure
The local status column is written only after on-chain confirmation, never on XDR build
A vendor that has been approved on-chain returns true from isVendorActive()
Approvals and suspensions appear in the audit log with the acting admin wallet
status is present on vendor DTOs and distinguishes pending from approved
The migration is a new timestamped file and no applied migration was edited
All monetary and state-changing operations validated and safe against invalid input
Mandatory Checks Before Opening PR
All context/ files read and understood
Code follows context/code-standards.md exactly
context/progress-tracker.md updated with an accurate entry describing the change and why
Build passes with zero errors (npm run build)
All existing tests still pass, test count has not decreased
New tests written covering the new functionality
No new any types
Full Swagger decorators on every new endpoint
PR template filled out completely, including the "problem this solves" and "how it was tested" sections
PR references this issue number exactly
PRs that fail any check above will be closed without review, regardless of how much work went into them. No exceptions. This is a Grantfox-funded, quality-first project. Code that does not meet these standards will not be merged and will not be paid.
Problem
Vendors can register but can never be approved, so no vendor can ever receive a loan.
vendor-registry-contractgained a full approval lifecycle in Contracts issue #7:register_vendor()now setsstatus = VendorStatus::Pendinginstead of activating immediately, andapprove_vendor()andsuspend_vendor()were added as admin-only functions.is_active()returnstrueonly forApproved, andcreditline-contract'svalidate_vendor()callsis_active().Nothing in this API calls any of it.
grep -rn "approve_vendor\|approveVendor\|VendorStatus\|suspend_vendor" src/returns zero matches across the entire repo.VendorRegistryContractClientexposes onlyisVendorActive()andgetVendor(), both read-only. There is no write path to the vendor registry contract at all.The off-chain side has the same hole. The
vendorstable created insupabase/migrations/20260504000001_create_vendors_table.sqlhas no status, approved, or verified column, so there is nowhere to record approval state even locally.The user-facing outcome: a vendor completes registration through
POST /vendors, appears inGET /vendors, shows up in the web app, and is permanently stuck inPendingon-chain. Any learner who tries to take a loan against that vendor is rejected withVendorNotActive. The protocol has no path to onboard a single working vendor.Ground Rules
This issue must be solved according to StepFi's established engineering standards. Before writing any code:
src/modules/vendors/vendors.controller.tsandvendors.service.tsin full to follow the existing controller and service splitsrc/modules/liquidity/liquidity.service.tsfor the established pattern of building unsigned XDR and returning it to the client for signing. Clients never call the chain directlysrc/common/interceptors/audit.interceptor.tsandsrc/common/decorators/audit-action.decorator.ts, since approval and suspension are privileged actions that must be audit-loggedcontracts/vendor-registry-contract/src/lib.rsin StepFi-Contracts for the exactapprove_vendorandsuspend_vendorsignaturesThis issue depends on the companion issue that introduces
AdminGuardand theADMIN_WALLETSallowlist. There is currently no admin concept in this API. Do not invent a second one here. If that issue has not landed, coordinate rather than duplicating.Your PR will be rejected regardless of whether CI passes if it conflicts with anything in these files, introduces patterns inconsistent with what already exists, or duplicates functionality that was solved differently elsewhere in the codebase.
What To Build
supabase/migrations/adding astatuscolumn tovendors, constrained topending,approved,suspended,rejected, defaulting topending. Timestamp-prefix the filename. Never edit an applied migration. Backfill existing rows topendingso local state matches the on-chain default.VendorRegistryContractClient:buildApproveVendorXdr(admin, vendor)andbuildSuspendVendorXdr(admin, vendor). These build unsigned XDR. They must not submit. Follow the XDR-building pattern inliquidity.service.ts.POST /vendors/:id/approveandPOST /vendors/:id/suspendtovendors.controller.ts, both guarded with@UseGuards(JwtAuthGuard, AdminGuard).statuscolumn only after the signed transaction is confirmed. Hook this into the existingtransaction-status-checkerjob rather than optimistically writing on XDR build, so a signed-but-never-submitted transaction cannot leave the database claiming a vendor is approved when the chain disagrees.Pending, matching the contract'sVendorNotPendingerror, and return a 409 with a clear message rather than letting the contract call fail opaquely.@AuditActionso approvals and suspensions land in the audit log.statusto the vendor DTOs insrc/modules/vendors/dto/vendor.dto.tsso clients can distinguish pending from approved vendors.@ApiOperation,@ApiResponsefor 200, 401, 403, 409, and@ApiTags.Files To Touch
supabase/migrations/<timestamp>_add_vendor_status.sql(new)src/stellar/contracts/clients/vendor-registry.client.tssrc/stellar/contracts/interfaces/vendor-registry.interface.tssrc/stellar/contracts/mocks/vendor-registry.mock.tssrc/modules/vendors/vendors.controller.tssrc/modules/vendors/vendors.service.tssrc/modules/vendors/dto/vendor.dto.tssrc/jobs/transaction-status-checker/transaction-status-checker.service.tssrc/modules/vendors/vendors.service.spec.tsAcceptance Criteria
POST /vendors/:id/approvereturns unsigned XDR for an allowlisted admin walletPendingreturns 409, not an opaque contract failurestatuscolumn is written only after on-chain confirmation, never on XDR buildtruefromisVendorActive()statusis present on vendor DTOs and distinguishes pending from approvedMandatory Checks Before Opening PR
npm run build)anytypesPRs that fail any check above will be closed without review, regardless of how much work went into them. No exceptions. This is a Grantfox-funded, quality-first project. Code that does not meet these standards will not be merged and will not be paid.