Implement functionality to release funds after deployment (#7)

Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

- Implement functionality to release funds after first successful deployment

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#7
This commit is contained in:
nabarun 2024-10-21 14:25:49 +00:00
parent d486f44cfe
commit ef26f9b39e
10 changed files with 98 additions and 27 deletions

View File

@ -3,7 +3,9 @@ import {
DeepPartial, DeepPartial,
FindManyOptions, FindManyOptions,
FindOneOptions, FindOneOptions,
FindOptionsWhere FindOptionsWhere,
IsNull,
Not
} from 'typeorm'; } from 'typeorm';
import path from 'path'; import path from 'path';
import debug from 'debug'; import debug from 'debug';
@ -151,14 +153,19 @@ export class Database {
} }
async allProjectsWithoutDeployments(): Promise<Project[]> { async allProjectsWithoutDeployments(): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project); const allProjects = await this.getProjects({
where: {
auctionId: Not(IsNull()),
},
relations: ['deployments'],
withDeleted: true,
});
const projects = await projectRepository const projects = allProjects.filter(project => {
.createQueryBuilder('project') if (project.deletedAt !== null) return false;
.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 return project.deployments.length === 0;
.andWhere('project.auctionId IS NOT NULL') // Ensure auctionId is not null });
.getMany();
return projects; return projects;
} }

View File

@ -52,6 +52,9 @@ export class Project {
@Column({ type: 'simple-array', nullable: true }) @Column({ type: 'simple-array', nullable: true })
deployerLrns!: string[] | null; deployerLrns!: string[] | null;
@Column('boolean', { default: false, nullable: true })
fundsReleased!: boolean;
// TODO: Compute template & framework in import repository // TODO: Compute template & framework in import repository
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
template!: string | null; template!: string | null;

View File

@ -319,6 +319,21 @@ export class Registry {
return deployerLrns; return deployerLrns;
} }
async releaseDeployerFunds(
auctionId: string
): Promise<any> {
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 * Fetch ApplicationDeploymentRecords for deployments
*/ */
@ -417,6 +432,10 @@ export class Registry {
} }
async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> { async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
if (auctionIds.length === 0) {
return [];
}
const auctions = await this.registry.getAuctionsByIds(auctionIds); const auctions = await this.registry.getAuctionsByIds(auctionIds);
const completedAuctions = auctions const completedAuctions = auctions

View File

@ -74,6 +74,7 @@ type Project {
description: String description: String
deployerLrns: [String] deployerLrns: [String]
auctionId: String auctionId: String
fundsReleased: Boolean
template: String template: String
framework: String framework: String
webhooks: [String!] webhooks: [String!]

View File

@ -173,6 +173,9 @@ export class Service {
where: records.map((record) => ({ where: records.map((record) => ({
applicationDeploymentRequestId: record.attributes.request, applicationDeploymentRequestId: record.attributes.request,
})), })),
relations: {
project: true,
},
order: { order: {
createdAt: 'DESC', createdAt: 'DESC',
}, },
@ -189,8 +192,6 @@ export class Service {
// Update deployment data for ApplicationDeploymentRecords // Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records.map(async (record) => { const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.request]; const deployment = recordToDeploymentsMap[record.attributes.request];
const project = await this.getProjectById(deployment.projectId)
assert(project)
const parts = record.attributes.url.replace('https://', '').split('.'); const parts = record.attributes.url.replace('https://', '').split('.');
const baseDomain = parts.slice(1).join('.'); const baseDomain = parts.slice(1).join('.');
@ -204,15 +205,21 @@ export class Service {
await this.db.updateDeploymentById(deployment.id, deployment); await this.db.updateDeploymentById(deployment.id, deployment);
const baseDomains = project.baseDomains || []; const baseDomains = deployment.project.baseDomains || [];
if (!baseDomains.includes(baseDomain)) { if (!baseDomains.includes(baseDomain)) {
baseDomains.push(baseDomain); baseDomains.push(baseDomain);
} }
await this.db.updateProjectById(project.id, { // Release deployer funds on successful deployment
baseDomains if (!deployment.project.fundsReleased) {
}) const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId);
await this.db.updateProjectById(deployment.projectId, {
baseDomains,
fundsReleased,
});
}
log( log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`, `Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
@ -292,7 +299,7 @@ export class Service {
async checkAuctionStatus(): Promise<void> { async checkAuctionStatus(): Promise<void> {
const projects = await this.db.allProjectsWithoutDeployments(); 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)); .filter((id): id is string => Boolean(id));
const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(validAuctionIds); const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(validAuctionIds);
@ -301,7 +308,7 @@ export class Service {
); );
for (const project of projectsToBedeployed) { for (const project of projectsToBedeployed) {
const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project!.auctionId!); const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project.auctionId!);
if (!deployerLrns) { if (!deployerLrns) {
log(`No winning deployer for auction ${project!.auctionId}`); log(`No winning deployer for auction ${project!.auctionId}`);
@ -1261,4 +1268,27 @@ export class Service {
const auctions = await this.laconicRegistry.getAuctionData(auctionId); const auctions = await this.laconicRegistry.getAuctionData(auctionId);
return auctions[0]; return auctions[0];
} }
async releaseDeployerFundsByProjectId(projectId: string): Promise<boolean> {
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;
}
} }

View File

@ -17,6 +17,7 @@ const WAIT_DURATION = 5000;
export const AuctionCard = ({ project }: { project: Project }) => { export const AuctionCard = ({ project }: { project: Project }) => {
const [auctionStatus, setAuctionStatus] = useState<string>(''); const [auctionStatus, setAuctionStatus] = useState<string>('');
const [deployerLrns, setDeployerLrns] = useState<string[]>([]); const [deployerLrns, setDeployerLrns] = useState<string[]>([]);
const [fundsStatus, setFundsStatus] = useState<boolean>(false);
const [auctionDetails, setAuctionDetails] = useState<Auction | null>(null); const [auctionDetails, setAuctionDetails] = useState<Auction | null>(null);
const [openDialog, setOpenDialog] = useState<boolean>(false); const [openDialog, setOpenDialog] = useState<boolean>(false);
const client = useGQLClient(); const client = useGQLClient();
@ -29,18 +30,16 @@ export const AuctionCard = ({ project }: { project: Project }) => {
setAuctionStatus(result.status); setAuctionStatus(result.status);
setAuctionDetails(result); setAuctionDetails(result);
setDeployerLrns(project.deployerLrns); setDeployerLrns(project.deployerLrns);
}, [client, project.auctionId, project.deployerLrns]); setFundsStatus(project.fundsReleased);
}, []);
useEffect(() => { useEffect(() => {
if (auctionStatus !== 'completed') { if (auctionStatus !== 'completed') {
checkAuctionStatus(); checkAuctionStatus();
const intervalId = setInterval(checkAuctionStatus, WAIT_DURATION); const intervalId = setInterval(checkAuctionStatus, WAIT_DURATION);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
} }
}, [auctionStatus, checkAuctionStatus]);
useEffect(() => {
if (auctionStatus === 'completed') { if (auctionStatus === 'completed') {
const fetchUpdatedProject = async () => { const fetchUpdatedProject = async () => {
// Wait for 5 secs since the project is not immediately updated with deployer LRNs // 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); const updatedProject = await client.getProject(project.id);
setDeployerLrns(updatedProject.project?.deployerLrns || []); setDeployerLrns(updatedProject.project?.deployerLrns || []);
}; };
fetchUpdatedProject(); fetchUpdatedProject();
} }
}, [auctionStatus, client, project.id]); }, [auctionStatus, client]);
const renderAuctionStatus = useCallback( const renderAuctionStatus = useCallback(
() => ( () => (
@ -101,6 +99,18 @@ export const AuctionCard = ({ project }: { project: Project }) => {
))} ))}
</div> </div>
)} )}
<div className="flex justify-between items-center mt-1">
<span className="text-elements-high-em text-sm font-medium tracking-tight">Deployer Funds Status</span>
<div className="ml-2">
<Tag
size="xs"
type={fundsStatus ? 'positive' : 'emphasized'}
>
{fundsStatus ? 'RELEASED' : 'LOCKED'}
</Tag>
</div>
</div>
</div> </div>
<Dialog open={openDialog} onClose={handleCloseDialog} fullWidth maxWidth="md"> <Dialog open={openDialog} onClose={handleCloseDialog} fullWidth maxWidth="md">

View File

@ -23,12 +23,8 @@ const Id = () => {
const handleSetupDomain = async () => { const handleSetupDomain = async () => {
if (id) { if (id) {
// console.log('id', id);
// console.log('getting project for id', id);
const project = await client.getProject(id); const project = await client.getProject(id);
// console.log('project found:', project);
if (project && project.project) { if (project && project.project) {
// console.log('project:', project.project);
setProject(project.project); setProject(project.project);
} }
} else { } else {
@ -38,7 +34,7 @@ const Id = () => {
useEffect(() => { useEffect(() => {
handleSetupDomain(); handleSetupDomain();
}); }, []);
return ( return (
<> <>

View File

@ -124,5 +124,6 @@ export const project: Project = {
deployerLrns: ['lrn://deployer.apps.snowballtools.com '], deployerLrns: ['lrn://deployer.apps.snowballtools.com '],
webhooks: ['beepboop'], webhooks: ['beepboop'],
icon: 'Icon', icon: 'Icon',
fundsReleased: true,
baseDomains: ['baseDomain'], baseDomains: ['baseDomain'],
}; };

View File

@ -25,6 +25,7 @@ query ($projectId: String!) {
prodBranch prodBranch
auctionId auctionId
deployerLrns deployerLrns
fundsReleased
framework framework
repository repository
webhooks webhooks
@ -76,6 +77,7 @@ query ($organizationSlug: String!) {
framework framework
auctionId auctionId
deployerLrns deployerLrns
fundsReleased
prodBranch prodBranch
webhooks webhooks
repository repository
@ -193,6 +195,7 @@ query ($searchText: String!) {
framework framework
auctionId auctionId
deployerLrns deployerLrns
fundsReleased
prodBranch prodBranch
webhooks webhooks
updatedAt updatedAt

View File

@ -171,6 +171,7 @@ export type Project = {
framework: string; framework: string;
deployerLrns: string[]; deployerLrns: string[];
auctionId: string; auctionId: string;
fundsReleased: boolean;
webhooks: string[]; webhooks: string[];
members: ProjectMember[]; members: ProjectMember[];
environmentVariables: EnvironmentVariable[]; environmentVariables: EnvironmentVariable[];