From 81e2d360849688f15d1f9b437a119b244bef1ad9 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Wed, 27 May 2026 18:40:30 -0500 Subject: [PATCH 1/4] fix(provider): resolve bids from sibling wallets at a shared hostUri MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multiple Provider rows can share the same hostUri on-chain (different owner wallets registering the same daemon). The API dedupes by hostUri and only returns one canonical row per hostUri, so bids submitted from non-canonical wallets at the same hostUri previously failed the `providers.find(p => p.owner === bid.provider)` lookup in the UI — leaving empty rows in the bid list, breaking the search/audited filters, and throwing "Cannot find bid provider" on manifest send. API: - Strengthen the dedup tiebreaker to prefer the wallet with active leases (via new `LeaseRepository.getActiveLeaseCountByProviders`). Falls back to `createdHeight DESC` only when leases are tied. - Expose the sibling wallets that share each canonical row's hostUri as `aliasOwners: string[]` on the provider list response. deploy-web: - Add `findProviderForBidProvider(providers, bidProvider)` helper that resolves a bid by `owner` OR by `aliasOwners`. - Use the helper in BidGroup display + CreateLease manifest send, search filter, and audited filter. - Hide bid rows whose resolved provider is offline or on an invalid version so users no longer see unselectable empty rows on mainnet. --- .../repositories/lease/lease.repository.ts | 20 +++++ .../provider/http-schemas/provider.schema.ts | 1 + .../provider/provider.service.spec.ts | 86 ++++++++++++++++++- .../services/provider/provider.service.ts | 35 ++++++-- apps/api/src/types/provider.ts | 1 + apps/api/src/utils/map/provider.ts | 4 +- .../components/new-deployment/BidGroup.tsx | 5 +- .../CreateLease/CreateLease.tsx | 10 ++- apps/deploy-web/src/types/provider.ts | 1 + .../src/utils/providerUtils.spec.ts | 38 ++++++++ apps/deploy-web/src/utils/providerUtils.ts | 7 +- apps/deploy-web/tests/seeders/provider.ts | 1 + 12 files changed, 191 insertions(+), 18 deletions(-) create mode 100644 apps/deploy-web/src/utils/providerUtils.spec.ts diff --git a/apps/api/src/deployment/repositories/lease/lease.repository.ts b/apps/api/src/deployment/repositories/lease/lease.repository.ts index 112aff9eed..61e0976139 100644 --- a/apps/api/src/deployment/repositories/lease/lease.repository.ts +++ b/apps/api/src/deployment/repositories/lease/lease.repository.ts @@ -96,6 +96,26 @@ export class LeaseRepository implements DrainingDeploymentLeaseSource { * @param params - Query parameters for filtering and pagination * @returns Object with total count and array of lease rows */ + async getActiveLeaseCountByProviders(providerAddresses: string[]): Promise> { + if (!providerAddresses.length) return new Map(); + + const rows = (await Lease.findAll({ + attributes: ["providerAddress", [fn("COUNT", col("id")), "leaseCount"]], + where: { + providerAddress: { [Op.in]: providerAddresses }, + closedHeight: null + }, + group: ["providerAddress"], + raw: true + })) as unknown as Array<{ providerAddress: string; leaseCount: string | number }>; + + const result = new Map(); + for (const row of rows) { + result.set(row.providerAddress, Number(row.leaseCount)); + } + return result; + } + async findLeasesWithPagination(params: DatabaseLeaseListParams): Promise<{ count: number; rows: Lease[] }> { const { skip = 0, limit = 100, owner, dseq, gseq, oseq, provider, state, reverse = false } = params; diff --git a/apps/api/src/provider/http-schemas/provider.schema.ts b/apps/api/src/provider/http-schemas/provider.schema.ts index 0bc5d1dca8..8066833cb1 100644 --- a/apps/api/src/provider/http-schemas/provider.schema.ts +++ b/apps/api/src/provider/http-schemas/provider.schema.ts @@ -45,6 +45,7 @@ export const ProviderListResponseSchema = z.array( isOnline: z.boolean(), lastOnlineDate: z.string().nullable(), isAudited: z.boolean(), + aliasOwners: z.array(z.string()), gpuModels: z.array( z.object({ vendor: z.string(), diff --git a/apps/api/src/provider/services/provider/provider.service.spec.ts b/apps/api/src/provider/services/provider/provider.service.spec.ts index 5c73ef6fc1..f092e000fa 100644 --- a/apps/api/src/provider/services/provider/provider.service.spec.ts +++ b/apps/api/src/provider/services/provider/provider.service.spec.ts @@ -9,6 +9,7 @@ import { mock } from "vitest-mock-extended"; import { cacheEngine } from "@src/caching/helpers"; import { AUDITOR } from "@src/deployment/config/provider.config"; +import type { LeaseRepository } from "@src/deployment/repositories/lease/lease.repository"; import { createLeaseStatus } from "../../../../test/seeders/lease-status.seeder"; import { createProviderSeed, createProviderWithAttributeSignatures } from "../../../../test/seeders/provider.seeder"; import { createUserWallet } from "../../../../test/seeders/user-wallet.seeder"; @@ -397,8 +398,37 @@ describe(ProviderService.name, () => { expect(result[0].owner).toBe(onlineProvider.owner); }); - it("should prefer newest provider when multiple online providers share the same hostUri", async () => { - const { service, providerRepository, auditorsService, providerAttributesSchemaService } = setup(); + it("prefers provider with active leases over newer provider when multiple online providers share the same hostUri", async () => { + const { service, providerRepository, leaseRepository, auditorsService, providerAttributesSchemaService } = setup(); + + const sharedHostUri = "https://provider.example.com:8443"; + const olderOnlineWithLeases = { + ...createProviderWithAttributeSignatures(AUDITOR), + hostUri: sharedHostUri, + isOnline: true, + createdHeight: 100 + } as unknown as Provider; + const newerOnlineNoLeases = { + ...createProviderWithAttributeSignatures(AUDITOR), + hostUri: sharedHostUri, + isOnline: true, + createdHeight: 200 + } as unknown as Provider; + + providerRepository.getWithAttributesAndAuditors.mockResolvedValue([olderOnlineWithLeases, newerOnlineNoLeases]); + providerRepository.getProviderWithNodes.mockResolvedValue([]); + leaseRepository.getActiveLeaseCountByProviders.mockResolvedValue(new Map([[olderOnlineWithLeases.owner, 5]])); + auditorsService.getAuditors.mockResolvedValue([]); + providerAttributesSchemaService.getProviderAttributesSchema.mockResolvedValue(providerAttributeSchemaStub); + + const result = await service.getProviderList(); + + expect(result).toHaveLength(1); + expect(result[0].owner).toBe(olderOnlineWithLeases.owner); + }); + + it("falls back to newest provider when lease counts are tied", async () => { + const { service, providerRepository, leaseRepository, auditorsService, providerAttributesSchemaService } = setup(); const sharedHostUri = "https://provider.example.com:8443"; const olderOnline = { @@ -416,6 +446,7 @@ describe(ProviderService.name, () => { providerRepository.getWithAttributesAndAuditors.mockResolvedValue([olderOnline, newerOnline]); providerRepository.getProviderWithNodes.mockResolvedValue([]); + leaseRepository.getActiveLeaseCountByProviders.mockResolvedValue(new Map()); auditorsService.getAuditors.mockResolvedValue([]); providerAttributesSchemaService.getProviderAttributesSchema.mockResolvedValue(providerAttributeSchemaStub); @@ -425,6 +456,43 @@ describe(ProviderService.name, () => { expect(result[0].owner).toBe(newerOnline.owner); }); + it("populates aliasOwners with sibling wallets that share the canonical row's hostUri", async () => { + const { service, providerRepository, leaseRepository, auditorsService, providerAttributesSchemaService } = setup(); + + const sharedHostUri = "https://provider.example.com:8443"; + const canonical = { + ...createProviderWithAttributeSignatures(AUDITOR), + hostUri: sharedHostUri, + isOnline: true, + createdHeight: 100 + } as unknown as Provider; + const sibling = { + ...createProviderWithAttributeSignatures(AUDITOR), + hostUri: sharedHostUri, + isOnline: true, + createdHeight: 200 + } as unknown as Provider; + const standalone = { + ...createProviderWithAttributeSignatures(AUDITOR), + isOnline: true, + createdHeight: 50 + } as unknown as Provider; + + providerRepository.getWithAttributesAndAuditors.mockResolvedValue([canonical, sibling, standalone]); + providerRepository.getProviderWithNodes.mockResolvedValue([]); + leaseRepository.getActiveLeaseCountByProviders.mockResolvedValue(new Map([[canonical.owner, 1]])); + auditorsService.getAuditors.mockResolvedValue([]); + providerAttributesSchemaService.getProviderAttributesSchema.mockResolvedValue(providerAttributeSchemaStub); + + const result = await service.getProviderList(); + + const sharedRow = result.find(p => p.hostUri === sharedHostUri); + const standaloneRow = result.find(p => p.owner === standalone.owner); + expect(sharedRow?.owner).toBe(canonical.owner); + expect(sharedRow?.aliasOwners).toEqual([sibling.owner]); + expect(standaloneRow?.aliasOwners).toEqual([]); + }); + it("should deduplicate providers with the same hostUri", async () => { const { service, providerRepository, auditorsService, providerAttributesSchemaService } = setup(); @@ -516,8 +584,17 @@ describe(ProviderService.name, () => { const jwtTokenService = mock({ generateJwtToken: jest.fn().mockResolvedValue(Ok("mock-jwt-token")) }); + const leaseRepository = mock(); + leaseRepository.getActiveLeaseCountByProviders.mockResolvedValue(new Map()); - const service = new ProviderService(providerProxyService, providerRepository, providerAttributesSchemaService, auditorsService, jwtTokenService); + const service = new ProviderService( + providerProxyService, + providerRepository, + providerAttributesSchemaService, + auditorsService, + jwtTokenService, + leaseRepository + ); return { service, @@ -525,7 +602,8 @@ describe(ProviderService.name, () => { providerAttributesSchemaService, auditorsService, jwtTokenService, - providerProxyService + providerProxyService, + leaseRepository }; } }); diff --git a/apps/api/src/provider/services/provider/provider.service.ts b/apps/api/src/provider/services/provider/provider.service.ts index fa5a57f933..a12a66663a 100644 --- a/apps/api/src/provider/services/provider/provider.service.ts +++ b/apps/api/src/provider/services/provider/provider.service.ts @@ -9,6 +9,7 @@ import { singleton } from "tsyringe"; import { Memoize } from "@src/caching/helpers"; import { LeaseStatusResponse } from "@src/deployment/http-schemas/lease.schema"; +import { LeaseRepository } from "@src/deployment/repositories/lease/lease.repository"; import type { Auditor } from "@src/provider/http-schemas/auditor.schema"; import { ProviderRepository } from "@src/provider/repositories/provider/provider.repository"; import { ProviderAuth, ProviderIdentity, ProviderProxyService } from "@src/provider/services/provider/provider-proxy.service"; @@ -32,7 +33,8 @@ export class ProviderService { private readonly providerRepository: ProviderRepository, private readonly providerAttributesSchemaService: ProviderAttributesSchemaService, private readonly auditorsService: AuditorService, - private readonly jwtTokenService: ProviderJwtTokenService + private readonly jwtTokenService: ProviderJwtTokenService, + private readonly leaseRepository: LeaseRepository ) {} async sendManifest(options: { provider: string; dseq: string; manifest: string; auth: ProviderAuth }) { @@ -148,16 +150,20 @@ export class ProviderService { offset += BATCH_SIZE; } while (batch.length === BATCH_SIZE); + const activeLeaseCountByOwner = await this.leaseRepository.getActiveLeaseCountByProviders( + providersWithAttributesAndAuditors.map(provider => provider.owner) + ); + const providerByHostUri = new Map(); + const ownersByHostUri = new Map(); await forEachInChunks(providersWithAttributesAndAuditors, provider => { const existing = providerByHostUri.get(provider.hostUri); - if ( - !existing || - (!existing.isOnline && provider.isOnline) || - (existing.isOnline === provider.isOnline && provider.createdHeight > existing.createdHeight) - ) { + if (!existing || this.isBetterProviderRepresentative(provider, existing, activeLeaseCountByOwner)) { providerByHostUri.set(provider.hostUri, provider); } + const owners = ownersByHostUri.get(provider.hostUri) ?? []; + owners.push(provider.owner); + ownersByHostUri.set(provider.hostUri, owners); }); const distinctProviders = Array.from(providerByHostUri.values()); @@ -174,7 +180,8 @@ export class ProviderService { await forEachInChunks(distinctProviders, provider => { const lastSuccessfulSnapshot = providerByOwner.get(provider.owner)?.lastSuccessfulSnapshot; - finalProviders.push(mapProviderToList(provider, providerAttributeSchema, auditors, lastSuccessfulSnapshot)); + const aliasOwners = (ownersByHostUri.get(provider.hostUri) ?? []).filter(owner => owner !== provider.owner); + finalProviders.push(mapProviderToList(provider, providerAttributeSchema, auditors, lastSuccessfulSnapshot, aliasOwners)); }); return finalProviders; @@ -191,6 +198,20 @@ export class ProviderService { return this.mapProviderResults(providersWithAttributesAndAuditors, providerWithNodes, auditors, providerAttributeSchema); } + private isBetterProviderRepresentative(candidate: Provider, current: Provider, activeLeaseCountByOwner: Map): boolean { + if (candidate.isOnline !== current.isOnline) { + return !!candidate.isOnline; + } + + const candidateLeases = activeLeaseCountByOwner.get(candidate.owner) ?? 0; + const currentLeases = activeLeaseCountByOwner.get(current.owner) ?? 0; + if (candidateLeases !== currentLeases) { + return candidateLeases > currentLeases; + } + + return candidate.createdHeight > current.createdHeight; + } + private mapProviderResults( providersWithAttributesAndAuditors: Provider[], providerWithNodes: Provider[], diff --git a/apps/api/src/types/provider.ts b/apps/api/src/types/provider.ts index 2cc20efc4f..18b7e912f1 100644 --- a/apps/api/src/types/provider.ts +++ b/apps/api/src/types/provider.ts @@ -23,6 +23,7 @@ export interface ProviderList { isOnline: boolean; lastOnlineDate: Date | null; isAudited: boolean; + aliasOwners: string[]; gpuModels: Array<{ vendor: string; model: string; diff --git a/apps/api/src/utils/map/provider.ts b/apps/api/src/utils/map/provider.ts index bac5d61e7e..435a40b7ca 100644 --- a/apps/api/src/utils/map/provider.ts +++ b/apps/api/src/utils/map/provider.ts @@ -9,7 +9,8 @@ export const mapProviderToList = ( provider: Provider, providerAttributeSchema: ProviderAttributesSchema, auditors: Array, - lastSuccessfulSnapshot?: ProviderSnapshot + lastSuccessfulSnapshot?: ProviderSnapshot, + aliasOwners: string[] = [] ): ProviderList => { const isValidSdkVersion = provider.cosmosSdkVersion ? semver.gte(provider.cosmosSdkVersion, "v0.45.9") : false; const name = provider.isOnline ? new URL(provider.hostUri).hostname : null; @@ -73,6 +74,7 @@ export const mapProviderToList = ( isOnline: !!provider.isOnline, lastOnlineDate: lastSuccessfulSnapshot?.checkDate || null, isAudited: provider.providerAttributeSignatures.some(a => auditorSet.has(a.auditor)), + aliasOwners, attributes: provider.providerAttributes.map(attr => ({ key: attr.key, value: attr.value, diff --git a/apps/deploy-web/src/components/new-deployment/BidGroup.tsx b/apps/deploy-web/src/components/new-deployment/BidGroup.tsx index 550d16d9e2..4d86b73f30 100644 --- a/apps/deploy-web/src/components/new-deployment/BidGroup.tsx +++ b/apps/deploy-web/src/components/new-deployment/BidGroup.tsx @@ -8,6 +8,7 @@ import networkStore from "@src/store/networkStore"; import type { BidDto, DeploymentDto } from "@src/types/deployment"; import type { ApiProviderList } from "@src/types/provider"; import { deploymentGroupResourceSum, getStorageAmount } from "@src/utils/deploymentDetailUtils"; +import { findProviderForBidProvider } from "@src/utils/providerUtils"; import { FormPaper } from "../sdl/FormPaper"; import { LabelValueOld } from "../shared/LabelValueOld"; import { SpecDetail } from "../shared/SpecDetail"; @@ -121,8 +122,8 @@ export const BidGroup: React.FunctionComponent = ({ {fBids.map(bid => { - const provider = providers && providers.find(x => x.owner === bid.provider); - const showBid = provider?.isValidVersion && (!isSendingManifest || selectedBid?.id === bid.id); + const provider = findProviderForBidProvider(providers, bid.provider); + const showBid = provider?.isOnline && provider.isValidVersion && (!isSendingManifest || selectedBid?.id === bid.id); return (showBid && provider) || selectedNetworkId !== MAINNET_ID ? ( = ({ dseq, dependencies for (let i = 0; i < bidKeys.length; i++) { const currentBid = selectedBids[bidKeys[i]]; - const provider = providers?.find(x => x.owner === currentBid.provider); + const provider = findProviderForBidProvider(providers, currentBid.provider); if (!provider) { throw new Error("Cannot find bid provider"); @@ -254,7 +255,7 @@ export const CreateLease: React.FunctionComponent = ({ dseq, dependencies if (search) { filteredBids = filteredBids.filter(bid => { - const provider = providers?.find(p => p.owner === bid.provider); + const provider = findProviderForBidProvider(providers, bid.provider); return provider?.attributes.some(att => att.value?.toLowerCase().includes(search.toLowerCase())) || provider?.hostUri.includes(search); }); } @@ -264,7 +265,10 @@ export const CreateLease: React.FunctionComponent = ({ dseq, dependencies } if (isFilteringAudited) { - filteredBids = filteredBids.filter(bid => !!providers.filter(x => x.isAudited).find(p => p.owner === bid.provider)); + filteredBids = filteredBids.filter(bid => { + const provider = findProviderForBidProvider(providers, bid.provider); + return !!provider?.isAudited; + }); } setFilteredBids(filteredBids.map(bid => bid.id)); diff --git a/apps/deploy-web/src/types/provider.ts b/apps/deploy-web/src/types/provider.ts index b509a54d73..05ec613247 100644 --- a/apps/deploy-web/src/types/provider.ts +++ b/apps/deploy-web/src/types/provider.ts @@ -189,6 +189,7 @@ export interface ApiProviderList { isOnline: boolean; lastOnlineDate: string; isAudited: boolean; + aliasOwners: string[]; gpuModels: { vendor: string; model: string; ram: string; interface: string }[]; stats: { cpu: StatsItem; diff --git a/apps/deploy-web/src/utils/providerUtils.spec.ts b/apps/deploy-web/src/utils/providerUtils.spec.ts new file mode 100644 index 0000000000..f58b8a21c0 --- /dev/null +++ b/apps/deploy-web/src/utils/providerUtils.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { findProviderForBidProvider } from "./providerUtils"; + +import { buildProvider } from "@tests/seeders/provider"; + +describe(findProviderForBidProvider.name, () => { + it("finds the provider when the bid provider matches its owner", () => { + const provider = buildProvider({ aliasOwners: [] }); + + const result = findProviderForBidProvider([provider], provider.owner); + + expect(result).toBe(provider); + }); + + it("finds the canonical provider when the bid provider matches one of its aliasOwners", () => { + const aliasOwner = "akash1alias000000000000000000000000000000000"; + const provider = buildProvider({ aliasOwners: [aliasOwner] }); + + const result = findProviderForBidProvider([provider], aliasOwner); + + expect(result).toBe(provider); + }); + + it("returns undefined when no provider matches owner or aliasOwners", () => { + const provider = buildProvider({ aliasOwners: [] }); + + const result = findProviderForBidProvider([provider], "akash1notlisted"); + + expect(result).toBeUndefined(); + }); + + it("returns undefined when providers is undefined", () => { + const result = findProviderForBidProvider(undefined, "akash1anything"); + + expect(result).toBeUndefined(); + }); +}); diff --git a/apps/deploy-web/src/utils/providerUtils.ts b/apps/deploy-web/src/utils/providerUtils.ts index 38faa2624a..e0661e50b1 100644 --- a/apps/deploy-web/src/utils/providerUtils.ts +++ b/apps/deploy-web/src/utils/providerUtils.ts @@ -1,7 +1,7 @@ import networkStore from "@src/store/networkStore"; import type { ISnapshotMetadata } from "@src/types"; import { ProviderSnapshots } from "@src/types"; -import type { ProviderStatus, ProviderStatusDto, ProviderVersion } from "@src/types/provider"; +import type { ApiProviderList, ProviderStatus, ProviderStatusDto, ProviderVersion } from "@src/types/provider"; import { bytesToShrink } from "./unitUtils"; export type LocalProviderData = { @@ -76,3 +76,8 @@ export const getProviderNameFromUri = (uri: string) => { const name = new URL(uri).hostname; return name; }; + +export const findProviderForBidProvider = (providers: ApiProviderList[] | undefined, bidProvider: string): ApiProviderList | undefined => { + if (!providers) return undefined; + return providers.find(provider => provider.owner === bidProvider || provider.aliasOwners?.includes(bidProvider)); +}; diff --git a/apps/deploy-web/tests/seeders/provider.ts b/apps/deploy-web/tests/seeders/provider.ts index d95fb607b9..97a4a607aa 100644 --- a/apps/deploy-web/tests/seeders/provider.ts +++ b/apps/deploy-web/tests/seeders/provider.ts @@ -66,6 +66,7 @@ export function buildProvider(overrides?: Partial): ApiProvid isOnline: faker.datatype.boolean(), lastOnlineDate: faker.date.recent().toISOString(), isAudited: faker.datatype.boolean(), + aliasOwners: [], attributes: [ { key: "region", value: "us-east", auditedBy: [faker.string.alphanumeric(42)] }, { key: "host", value: faker.internet.domainWord(), auditedBy: [faker.string.alphanumeric(42)] }, From 33ccb401c9539fe6471fc61563ba2fe1b3fcd2af Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Wed, 27 May 2026 18:50:07 -0500 Subject: [PATCH 2/4] test(provider): update OpenAPI doc snapshot for aliasOwners Adds the new `aliasOwners` field (and its required-list entry) to the provider-list response in the docs snapshot. --- apps/api/test/functional/__snapshots__/docs.spec.ts.snap | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/api/test/functional/__snapshots__/docs.spec.ts.snap b/apps/api/test/functional/__snapshots__/docs.spec.ts.snap index 7c8ea9c89d..f4c22c5c5a 100644 --- a/apps/api/test/functional/__snapshots__/docs.spec.ts.snap +++ b/apps/api/test/functional/__snapshots__/docs.spec.ts.snap @@ -11750,6 +11750,12 @@ exports[`API Docs > GET /v1/doc > returns docs with all routes expected 1`] = ` "akashVersion": { "type": "string", }, + "aliasOwners": { + "items": { + "type": "string", + }, + "type": "array", + }, "attributes": { "items": { "properties": { @@ -12013,6 +12019,7 @@ exports[`API Docs > GET /v1/doc > returns docs with all routes expected 1`] = ` "isOnline", "lastOnlineDate", "isAudited", + "aliasOwners", "gpuModels", "attributes", "host", From 0a4d3cf78cf31725243d16a0e0b207cf203fc54e Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Wed, 27 May 2026 23:55:43 -0500 Subject: [PATCH 3/4] fix(provider): dedupe aliasOwners per hostUri Use Set for owners-per-hostUri accumulator so any duplicate provider rows don't produce repeated entries in aliasOwners. --- .../src/provider/services/provider/provider.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api/src/provider/services/provider/provider.service.ts b/apps/api/src/provider/services/provider/provider.service.ts index a12a66663a..855a9b2958 100644 --- a/apps/api/src/provider/services/provider/provider.service.ts +++ b/apps/api/src/provider/services/provider/provider.service.ts @@ -155,14 +155,14 @@ export class ProviderService { ); const providerByHostUri = new Map(); - const ownersByHostUri = new Map(); + const ownersByHostUri = new Map>(); await forEachInChunks(providersWithAttributesAndAuditors, provider => { const existing = providerByHostUri.get(provider.hostUri); if (!existing || this.isBetterProviderRepresentative(provider, existing, activeLeaseCountByOwner)) { providerByHostUri.set(provider.hostUri, provider); } - const owners = ownersByHostUri.get(provider.hostUri) ?? []; - owners.push(provider.owner); + const owners = ownersByHostUri.get(provider.hostUri) ?? new Set(); + owners.add(provider.owner); ownersByHostUri.set(provider.hostUri, owners); }); const distinctProviders = Array.from(providerByHostUri.values()); @@ -180,7 +180,7 @@ export class ProviderService { await forEachInChunks(distinctProviders, provider => { const lastSuccessfulSnapshot = providerByOwner.get(provider.owner)?.lastSuccessfulSnapshot; - const aliasOwners = (ownersByHostUri.get(provider.hostUri) ?? []).filter(owner => owner !== provider.owner); + const aliasOwners = Array.from(ownersByHostUri.get(provider.hostUri) ?? []).filter(owner => owner !== provider.owner); finalProviders.push(mapProviderToList(provider, providerAttributeSchema, auditors, lastSuccessfulSnapshot, aliasOwners)); }); From 4baa168442d7d18444713079db78c73466de9c76 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Thu, 28 May 2026 14:59:01 -0500 Subject: [PATCH 4/4] test(provider): cover alias-aware bid filtering in CreateLease Add unit tests for the search and audited filters in CreateLease that exercise findProviderForBidProvider, including resolving a bid submitted from a sibling wallet via aliasOwners. Raises deploy-web patch coverage of the changed lines above the 50% codecov target (was 40%). --- .../CreateLease/CreateLease.spec.tsx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx index 743edc97c7..cb615c246d 100644 --- a/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx +++ b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx @@ -318,6 +318,43 @@ describe(CreateLease.name, () => { } }); + describe("bid filtering", () => { + it("keeps only audited providers' bids when the Audited filter is enabled, resolving sibling wallets via aliasOwners", async () => { + const auditedProvider = buildProvider({ owner: "akash1audited", aliasOwners: ["akash1auditedsibling"], isAudited: true }); + const unauditedProvider = buildProvider({ owner: "akash1plain", aliasOwners: [], isAudited: false }); + // The bid is submitted from a sibling wallet that shares the canonical row's hostUri (the bug this PR fixes). + const auditedBid = buildRpcBid({ bid: { id: { gseq: 1, provider: "akash1auditedsibling" }, state: "open" } }); + const unauditedBid = buildRpcBid({ bid: { id: { gseq: 1, provider: "akash1plain" }, state: "open" } }); + const BidGroup = vi.fn(ComponentMock); + + setup({ BidGroup, bids: [auditedBid, unauditedBid], providers: [auditedProvider, unauditedProvider] }); + + await vi.waitFor(() => expect(BidGroup).toHaveBeenCalled()); + await userEvent.click(screen.getByRole("checkbox", { name: /Audited/i })); + + await vi.waitFor(() => { + expect(BidGroup).toHaveBeenLastCalledWith(expect.objectContaining({ filteredBids: [mapToBidDto(auditedBid).id] }), {}); + }); + }); + + it("filters bids by search term against the resolved provider's hostUri", async () => { + const matchingProvider = buildProvider({ owner: "akash1match", hostUri: "https://needlehost.example.com:8443", attributes: [] }); + const otherProvider = buildProvider({ owner: "akash1other", hostUri: "https://otherhost.example.com:8443", attributes: [] }); + const matchingBid = buildRpcBid({ bid: { id: { gseq: 1, provider: "akash1match" }, state: "open" } }); + const otherBid = buildRpcBid({ bid: { id: { gseq: 1, provider: "akash1other" }, state: "open" } }); + const BidGroup = vi.fn(ComponentMock); + + setup({ BidGroup, bids: [matchingBid, otherBid], providers: [matchingProvider, otherProvider] }); + + await vi.waitFor(() => expect(BidGroup).toHaveBeenCalled()); + await userEvent.type(screen.getByLabelText("Search provider"), "needlehost"); + + await vi.waitFor(() => { + expect(BidGroup).toHaveBeenLastCalledWith(expect.objectContaining({ filteredBids: [mapToBidDto(matchingBid).id] }), {}); + }); + }); + }); + function setup(input?: { dseq?: string; BidGroup?: (typeof CREATE_LEASE_DEPENDENCIES)["BidGroup"];