diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 35c3aa65..5f840da5 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -3,7 +3,9 @@ import { DeepPartial, FindManyOptions, FindOneOptions, - FindOptionsWhere + FindOptionsWhere, + IsNull, + Not } from 'typeorm'; import path from 'path'; import debug from 'debug'; @@ -151,14 +153,19 @@ export class Database { } async allProjectsWithoutDeployments(): Promise { - const projectRepository = this.dataSource.getRepository(Project); + const allProjects = await this.getProjects({ + where: { + auctionId: Not(IsNull()), + }, + relations: ['deployments'], + withDeleted: true, + }); - const projects = await projectRepository - .createQueryBuilder('project') - .leftJoinAndSelect('project.deployments', 'deployment', 'deployment.deletedAt IS NULL') // Join only non-soft-deleted deployments - .where('deployment.id IS NULL') // Get projects where no deployments are present - .andWhere('project.auctionId IS NOT NULL') // Ensure auctionId is not null - .getMany(); + const projects = allProjects.filter(project => { + if (project.deletedAt !== null) return false; + + return project.deployments.length === 0; + }); return projects; } diff --git a/packages/backend/src/entity/Project.ts b/packages/backend/src/entity/Project.ts index ea44483f..d9696767 100644 --- a/packages/backend/src/entity/Project.ts +++ b/packages/backend/src/entity/Project.ts @@ -52,6 +52,9 @@ export class Project { @Column({ type: 'simple-array', nullable: true }) deployerLrns!: string[] | null; + @Column('boolean', { default: false, nullable: true }) + fundsReleased!: boolean; + // TODO: Compute template & framework in import repository @Column('varchar', { nullable: true }) template!: string | null; diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 89f53ebc..5368d36f 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -319,6 +319,21 @@ export class Registry { return deployerLrns; } + async releaseDeployerFunds( + auctionId: string + ): Promise { + const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); + const auction = await this.registry.releaseFunds( + { + auctionId + }, + this.registryConfig.privateKey, + fee + ); + + return auction; + } + /** * Fetch ApplicationDeploymentRecords for deployments */ @@ -417,6 +432,10 @@ export class Registry { } async getCompletedAuctionIds(auctionIds: string[]): Promise { + if (auctionIds.length === 0) { + return []; + } + const auctions = await this.registry.getAuctionsByIds(auctionIds); const completedAuctions = auctions diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index b4f74d72..8c222fb2 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -74,6 +74,7 @@ type Project { description: String deployerLrns: [String] auctionId: String + fundsReleased: Boolean template: String framework: String webhooks: [String!] diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 57ad9ef5..3f7e7f98 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -173,6 +173,9 @@ export class Service { where: records.map((record) => ({ applicationDeploymentRequestId: record.attributes.request, })), + relations: { + project: true, + }, order: { createdAt: 'DESC', }, @@ -189,8 +192,6 @@ export class Service { // Update deployment data for ApplicationDeploymentRecords const deploymentUpdatePromises = records.map(async (record) => { const deployment = recordToDeploymentsMap[record.attributes.request]; - const project = await this.getProjectById(deployment.projectId) - assert(project) const parts = record.attributes.url.replace('https://', '').split('.'); const baseDomain = parts.slice(1).join('.'); @@ -204,15 +205,21 @@ export class Service { await this.db.updateDeploymentById(deployment.id, deployment); - const baseDomains = project.baseDomains || []; + const baseDomains = deployment.project.baseDomains || []; if (!baseDomains.includes(baseDomain)) { baseDomains.push(baseDomain); } - await this.db.updateProjectById(project.id, { - baseDomains - }) + // Release deployer funds on successful deployment + if (!deployment.project.fundsReleased) { + const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId); + + await this.db.updateProjectById(deployment.projectId, { + baseDomains, + fundsReleased, + }); + } log( `Updated deployment ${deployment.id} with URL ${record.attributes.url}`, @@ -292,7 +299,7 @@ export class Service { async checkAuctionStatus(): Promise { const projects = await this.db.allProjectsWithoutDeployments(); - const validAuctionIds = projects.map((project) => project.auctionId!) + const validAuctionIds = projects.map((project) => project.auctionId) .filter((id): id is string => Boolean(id)); const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(validAuctionIds); @@ -301,7 +308,7 @@ export class Service { ); for (const project of projectsToBedeployed) { - const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project!.auctionId!); + const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project.auctionId!); if (!deployerLrns) { log(`No winning deployer for auction ${project!.auctionId}`); @@ -1261,4 +1268,27 @@ export class Service { const auctions = await this.laconicRegistry.getAuctionData(auctionId); return auctions[0]; } + + async releaseDeployerFundsByProjectId(projectId: string): Promise { + const project = await this.db.getProjectById(projectId); + + if (!project || !project.auctionId) { + log(`Project ${projectId} ${!project ? 'not found' : 'does not have an auction'}`); + + return false; + } + + const auction = await this.laconicRegistry.releaseDeployerFunds(project.auctionId); + + if (auction.auction.fundsReleased) { + log(`Funds released for auction ${project.auctionId}`); + await this.db.updateProjectById(projectId, { fundsReleased: true }); + + return true; + } + + log(`Error releasing funds for auction ${project.auctionId}`); + + return false; + } } diff --git a/packages/frontend/src/components/projects/project/overview/Activity/AuctionCard.tsx b/packages/frontend/src/components/projects/project/overview/Activity/AuctionCard.tsx index e79e944b..a5949099 100644 --- a/packages/frontend/src/components/projects/project/overview/Activity/AuctionCard.tsx +++ b/packages/frontend/src/components/projects/project/overview/Activity/AuctionCard.tsx @@ -17,6 +17,7 @@ const WAIT_DURATION = 5000; export const AuctionCard = ({ project }: { project: Project }) => { const [auctionStatus, setAuctionStatus] = useState(''); const [deployerLrns, setDeployerLrns] = useState([]); + const [fundsStatus, setFundsStatus] = useState(false); const [auctionDetails, setAuctionDetails] = useState(null); const [openDialog, setOpenDialog] = useState(false); const client = useGQLClient(); @@ -29,18 +30,16 @@ export const AuctionCard = ({ project }: { project: Project }) => { setAuctionStatus(result.status); setAuctionDetails(result); setDeployerLrns(project.deployerLrns); - }, [client, project.auctionId, project.deployerLrns]); + setFundsStatus(project.fundsReleased); + }, []); useEffect(() => { if (auctionStatus !== 'completed') { checkAuctionStatus(); const intervalId = setInterval(checkAuctionStatus, WAIT_DURATION); - return () => clearInterval(intervalId); } - }, [auctionStatus, checkAuctionStatus]); - useEffect(() => { if (auctionStatus === 'completed') { const fetchUpdatedProject = async () => { // Wait for 5 secs since the project is not immediately updated with deployer LRNs @@ -48,10 +47,9 @@ export const AuctionCard = ({ project }: { project: Project }) => { const updatedProject = await client.getProject(project.id); setDeployerLrns(updatedProject.project?.deployerLrns || []); }; - fetchUpdatedProject(); } - }, [auctionStatus, client, project.id]); + }, [auctionStatus, client]); const renderAuctionStatus = useCallback( () => ( @@ -101,6 +99,18 @@ export const AuctionCard = ({ project }: { project: Project }) => { ))} )} + +
+ Deployer Funds Status +
+ + {fundsStatus ? 'RELEASED' : 'LOCKED'} + +
+
diff --git a/packages/frontend/src/pages/org-slug/projects/create/success/Id.tsx b/packages/frontend/src/pages/org-slug/projects/create/success/Id.tsx index 66dba7b9..bd1d680c 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/success/Id.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/success/Id.tsx @@ -23,12 +23,8 @@ const Id = () => { const handleSetupDomain = async () => { if (id) { - // console.log('id', id); - // console.log('getting project for id', id); const project = await client.getProject(id); - // console.log('project found:', project); if (project && project.project) { - // console.log('project:', project.project); setProject(project.project); } } else { @@ -38,7 +34,7 @@ const Id = () => { useEffect(() => { handleSetupDomain(); - }); + }, []); return ( <> diff --git a/packages/frontend/src/stories/MockStoriesData.ts b/packages/frontend/src/stories/MockStoriesData.ts index 92f71721..5559cbed 100644 --- a/packages/frontend/src/stories/MockStoriesData.ts +++ b/packages/frontend/src/stories/MockStoriesData.ts @@ -124,5 +124,6 @@ export const project: Project = { deployerLrns: ['lrn://deployer.apps.snowballtools.com '], webhooks: ['beepboop'], icon: 'Icon', + fundsReleased: true, baseDomains: ['baseDomain'], }; diff --git a/packages/gql-client/src/queries.ts b/packages/gql-client/src/queries.ts index 6f115a82..a87a4cb8 100644 --- a/packages/gql-client/src/queries.ts +++ b/packages/gql-client/src/queries.ts @@ -25,6 +25,7 @@ query ($projectId: String!) { prodBranch auctionId deployerLrns + fundsReleased framework repository webhooks @@ -76,6 +77,7 @@ query ($organizationSlug: String!) { framework auctionId deployerLrns + fundsReleased prodBranch webhooks repository @@ -193,6 +195,7 @@ query ($searchText: String!) { framework auctionId deployerLrns + fundsReleased prodBranch webhooks updatedAt diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index b1220898..15b9e073 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -171,6 +171,7 @@ export type Project = { framework: string; deployerLrns: string[]; auctionId: string; + fundsReleased: boolean; webhooks: string[]; members: ProjectMember[]; environmentVariables: EnvironmentVariable[];