diff --git a/database/migrations/schema-changes-migration.js b/database/migrations/schema-changes-migration.js new file mode 100644 index 0000000..9edd693 --- /dev/null +++ b/database/migrations/schema-changes-migration.js @@ -0,0 +1,70 @@ +async function up(knex) { + // 1. Update Solver Bonding Pool: rename bonding_pool to name + // First, get all solver bonding pools with their bonding_pool relation + const solverBondingPools = await knex('solver_bonding_pools') + .select('id', 'bonding_pool_id') + .whereNotNull('bonding_pool_id'); + + // For each solver bonding pool, get the name from bonding_pool and update the solver bonding pool + for (const sbp of solverBondingPools) { + if (sbp.bonding_pool_id) { + // Get the name from bonding_pool + const bondingPool = await knex('bonding_pools') + .select('name') + .where('id', sbp.bonding_pool_id) + .first(); + + if (bondingPool && bondingPool.name) { + // Update the solver bonding pool with the name + await knex('solver_bonding_pools') + .where('id', sbp.id) + .update({ + name: bondingPool.name + }); + } + } + } + + // 2. Calculate activeNetworks for all solvers + const solvers = await knex('solvers').select('id'); + + for (const solver of solvers) { + // Get all active networks for this solver + const activeNetworks = await knex('solver_networks') + .join('networks', 'solver_networks.network_id', 'networks.id') + .where({ + 'solver_networks.solver_id': solver.id, + 'solver_networks.active': true + }) + .select('networks.name'); + + // Extract network names + const networkNames = activeNetworks.map(n => n.name); + + // Update the solver with activeNetworks and hasActiveNetworks + await knex('solvers') + .where('id', solver.id) + .update({ + activeNetworks: JSON.stringify(networkNames), + hasActiveNetworks: networkNames.length > 0 + }); + } + + // 3. Ensure all required fields are set for Solver Network + // Make a list of solver networks that have isVouched=true but no vouchedBy + const vouchedNetworks = await knex('solver_networks') + .where('isVouched', true) + .whereNull('vouchedBy'); + + console.log(`Found ${vouchedNetworks.length} solver networks with isVouched=true but no vouchedBy`); + + return; +} + +async function down(knex) { + // This migration cannot be reversed automatically + console.log('This migration cannot be reversed automatically'); + return; +} + +module.exports = { up, down }; diff --git a/src/api/solver-bonding-pool/content-types/solver-bonding-pool/lifecycles.ts b/src/api/solver-bonding-pool/content-types/solver-bonding-pool/lifecycles.ts new file mode 100644 index 0000000..35852de --- /dev/null +++ b/src/api/solver-bonding-pool/content-types/solver-bonding-pool/lifecycles.ts @@ -0,0 +1,106 @@ +import { updateServiceFeeEnabledForSolver } from '../../../solver/content-types/solver/lifecycles'; + +export default { + async afterCreate(event) { + await updateRelatedSolvers(event); + }, + + async afterUpdate(event) { + await updateRelatedSolvers(event); + }, + + async beforeDelete(event) { + await storeRelatedSolversForUpdate(event); + }, + + async afterDelete(event) { + await updateStoredSolvers(event); + }, +}; + +// Store for temporarily keeping solver IDs between beforeDelete and afterDelete +const solverIdsToUpdate = new Map(); + +async function updateRelatedSolvers(event) { + try { + const { result } = event; + + // If this is a delete operation, result might not be available + if (!result || !result.solvers) { + return; + } + + // Get the solver IDs + const solverIds = Array.isArray(result.solvers) + ? result.solvers.map(solver => solver.id || solver) + : []; + + // If no solvers are directly available in the result, fetch them + if (solverIds.length === 0 && result.id) { + const bondingPool = await strapi.entityService.findOne( + 'api::solver-bonding-pool.solver-bonding-pool', + result.id, + { populate: ['solvers'] } + ); + + if (bondingPool && bondingPool.solvers) { + bondingPool.solvers.forEach(solver => { + solverIds.push(solver.id); + }); + } + } + + // Update each solver's service fee enabled status + for (const solverId of solverIds) { + if (solverId) { + await updateServiceFeeEnabledForSolver(solverId); + } + } + } catch (error) { + console.error('Error updating related solvers:', error); + } +} + +async function storeRelatedSolversForUpdate(event) { + try { + const { where } = event.params; + const { id } = where; + + // Get the bonding pool with its solvers + const bondingPool = await strapi.entityService.findOne( + 'api::solver-bonding-pool.solver-bonding-pool', + id, + { populate: ['solvers'] } + ); + + if (bondingPool && bondingPool.solvers) { + // Store the solver IDs for later use in afterDelete + const solverIds = bondingPool.solvers.map(solver => solver.id); + solverIdsToUpdate.set(id, solverIds); + } + } catch (error) { + console.error('Error storing related solvers for update:', error); + } +} + +async function updateStoredSolvers(event) { + try { + const { where } = event.params; + const { id } = where; + + // Get the stored solver IDs + const solverIds = solverIdsToUpdate.get(id) || []; + + // Update each solver's service fee enabled status + for (const solverId of solverIds) { + if (solverId) { + await updateServiceFeeEnabledForSolver(solverId); + } + } + + // Clean up the stored solver IDs + solverIdsToUpdate.delete(id); + } catch (error) { + console.error('Error updating stored solvers:', error); + } +} diff --git a/src/api/solver-bonding-pool/content-types/solver-bonding-pool/schema.json b/src/api/solver-bonding-pool/content-types/solver-bonding-pool/schema.json index b116b8d..e8705d9 100644 --- a/src/api/solver-bonding-pool/content-types/solver-bonding-pool/schema.json +++ b/src/api/solver-bonding-pool/content-types/solver-bonding-pool/schema.json @@ -21,11 +21,9 @@ "type": "datetime", "required": true }, - "bonding_pool": { - "type": "relation", - "relation": "manyToOne", - "target": "api::bonding-pool.bonding-pool", - "inversedBy": "solver_bonding_pools" + "name": { + "type": "string", + "required": true }, "solvers": { "type": "relation", @@ -37,6 +35,12 @@ "type": "boolean", "required": true, "default": false + }, + "solver_networks": { + "type": "relation", + "relation": "oneToMany", + "target": "api::solver-network.solver-network", + "mappedBy": "vouchedBy" } } } diff --git a/src/api/solver-network/content-types/solver-network/lifecycles.ts b/src/api/solver-network/content-types/solver-network/lifecycles.ts new file mode 100644 index 0000000..2060a61 --- /dev/null +++ b/src/api/solver-network/content-types/solver-network/lifecycles.ts @@ -0,0 +1,53 @@ +import { calculateActiveNetworksForSolver } from '../../../solver/content-types/solver/lifecycles'; + +export default { + async beforeCreate(event) { + await validateVouchedBy(event); + }, + + async beforeUpdate(event) { + await validateVouchedBy(event); + }, + + async afterCreate(event) { + await updateParentSolver(event); + }, + + async afterUpdate(event) { + await updateParentSolver(event); + }, + + async afterDelete(event) { + await updateParentSolver(event); + }, +}; + +async function validateVouchedBy(event) { + const { data } = event.params; + + // Check if isVouched is true and vouchedBy is not set + if (data.isVouched === true && !data.vouchedBy) { + throw new Error('vouchedBy relationship must be set when isVouched is true'); + } +} + +async function updateParentSolver(event) { + try { + const { result } = event; + + // If this is a delete operation, result might not be available + if (!result || !result.solver) { + return; + } + + // Get the solver ID + const solverId = result.solver.id || result.solver; + + if (solverId) { + // Update the solver's activeNetworks field + await calculateActiveNetworksForSolver(solverId); + } + } catch (error) { + console.error('Error updating parent solver:', error); + } +} diff --git a/src/api/solver-network/content-types/solver-network/schema.json b/src/api/solver-network/content-types/solver-network/schema.json index fe21831..0fd3d68 100644 --- a/src/api/solver-network/content-types/solver-network/schema.json +++ b/src/api/solver-network/content-types/solver-network/schema.json @@ -22,11 +22,13 @@ "type": "relation", "relation": "manyToOne", "target": "api::network.network", - "inversedBy": "solver_networks" + "inversedBy": "solver_networks", + "required": true }, "address": { "type": "string", - "regex": "^(0x)?[0-9a-fA-F]{40}$" + "regex": "^(0x)?[0-9a-fA-F]{40}$", + "required": true }, "payoutAddress": { "type": "string", @@ -40,7 +42,8 @@ "environment": { "type": "relation", "relation": "oneToOne", - "target": "api::environment.environment" + "target": "api::environment.environment", + "required": true }, "isWhiteListed": { "type": "boolean", @@ -54,6 +57,11 @@ "type": "boolean", "default": false, "required": true + }, + "vouchedBy": { + "type": "relation", + "relation": "manyToOne", + "target": "api::solver-bonding-pool.solver-bonding-pool" } } } diff --git a/src/api/solver/content-types/solver/lifecycles.ts b/src/api/solver/content-types/solver/lifecycles.ts new file mode 100644 index 0000000..c599c0b --- /dev/null +++ b/src/api/solver/content-types/solver/lifecycles.ts @@ -0,0 +1,196 @@ +export default { + async beforeCreate(event) { + await updateActiveNetworks(event); + await updateServiceFeeEnabled(event); + }, + + async beforeUpdate(event) { + await updateActiveNetworks(event); + await updateServiceFeeEnabled(event); + }, +}; + +async function updateActiveNetworks(event) { + const { data, where } = event.params; + const solverData: SolverData = data; + + // If this is an update operation and we're not updating solver_networks relation + if (where && !data.solver_networks) { + // Get the current solver data to check if we need to update activeNetworks + const solver = await strapi.entityService.findOne( + 'api::solver.solver', + where.id, + { populate: ['solver_networks.network'] } + ); + + if (solver) { + // Calculate active networks + await calculateActiveNetworks(solver, solverData); + } + } else if (data.solver_networks) { + // For create or when solver_networks is being updated + // We'll need to fetch the networks after the operation in afterCreate/afterUpdate + // as the relations might not be fully established yet + } +} + +// Define interface for the data object +interface SolverData { + activeNetworks?: string[]; + hasActiveNetworks?: boolean; + isServiceFeeEnabled?: boolean; +} + +// This function will be called after create/update to ensure relations are established +export async function calculateActiveNetworksForSolver(solverId) { + try { + // Get the solver with its networks + const solver = await strapi.entityService.findOne( + 'api::solver.solver', + solverId, + { populate: ['solver_networks.network'] } + ); + + if (solver) { + // Calculate active networks + const data: SolverData = {}; + await calculateActiveNetworks(solver, data); + + // Update the solver with the calculated values + if (data.activeNetworks || data.hasActiveNetworks !== undefined) { + await strapi.entityService.update( + 'api::solver.solver', + solverId, + { data } + ); + } + } + } catch (error) { + console.error(`Error calculating active networks for solver ${solverId}:`, error); + } +} + +// This function will be called to update the service fee enabled status for a solver +export async function updateServiceFeeEnabledForSolver(solverId) { + try { + // Get the solver with its bonding pools + const solver = await strapi.entityService.findOne( + 'api::solver.solver', + solverId, + { + populate: { + solver_bonding_pools: { + fields: ['name', 'joinedOn'] + } + } + } + ); + + if (solver) { + // Calculate service fee enabled status + const data: SolverData = {}; + await calculateServiceFeeEnabled(solver, data); + + // Update the solver with the calculated values + if (data.isServiceFeeEnabled !== undefined) { + await strapi.entityService.update( + 'api::solver.solver', + solverId, + { data } + ); + } + } + } catch (error) { + console.error(`Error updating service fee enabled for solver ${solverId}:`, error); + } +} + +async function calculateActiveNetworks(solver, data: SolverData) { + if (!solver.solver_networks) { + data.activeNetworks = []; + data.hasActiveNetworks = false; + return; + } + + // Filter active networks and extract their names + const activeNetworkNames = solver.solver_networks + .filter(network => network.active) + .map(network => network.network?.name) + .filter(Boolean); // Remove any undefined values + + data.activeNetworks = activeNetworkNames; + data.hasActiveNetworks = activeNetworkNames.length > 0; +} + +async function updateServiceFeeEnabled(event) { + const { data, where } = event.params; + const solverData: SolverData = data; + + // For create operation or update operation + if (where) { + // Get the current solver data with bonding pools + const solver = await strapi.entityService.findOne( + 'api::solver.solver', + where.id, + { + populate: { + solver_bonding_pools: { + fields: ['name', 'joinedOn'] + } + } + } + ); + + if (solver) { + // Calculate service fee enabled status + await calculateServiceFeeEnabled(solver, solverData); + } + } + // For create operation, we'll handle it in afterCreate since we need the ID +} + +async function calculateServiceFeeEnabled(solver, data: SolverData) { + // Default to false + data.isServiceFeeEnabled = false; + + if (!solver.solver_bonding_pools || solver.solver_bonding_pools.length === 0) { + return; + } + + // Get current date for comparison + const currentDate = new Date(); + + // Check each bonding pool + for (const bondingPool of solver.solver_bonding_pools) { + // Skip if joinedOn is not set + if (!bondingPool.joinedOn) { + continue; + } + + const joinedDate = new Date(bondingPool.joinedOn); + const monthsDifference = getMonthsDifference(joinedDate, currentDate); + + // CoW bonding pool (name is "CoW" and not colocated) + if (bondingPool.name === "CoW" && solver.isColocated === "No") { + if (monthsDifference >= 6) { + data.isServiceFeeEnabled = true; + return; // Exit early once we find a qualifying pool + } + } + // Colocated bonding pool + else if (solver.isColocated === "Yes") { + if (monthsDifference >= 3) { + data.isServiceFeeEnabled = true; + return; // Exit early once we find a qualifying pool + } + } + // For partial colocated, we'll treat it as not colocated + } +} + +// Helper function to calculate months difference between two dates +function getMonthsDifference(startDate, endDate) { + const years = endDate.getFullYear() - startDate.getFullYear(); + const months = endDate.getMonth() - startDate.getMonth(); + return years * 12 + months; +} diff --git a/src/api/solver/content-types/solver/schema.json b/src/api/solver/content-types/solver/schema.json index 5dda4ca..c1dd525 100644 --- a/src/api/solver/content-types/solver/schema.json +++ b/src/api/solver/content-types/solver/schema.json @@ -69,6 +69,14 @@ ], "required": true, "default": "No" + }, + "activeNetworks": { + "type": "json", + "private": false + }, + "hasActiveNetworks": { + "type": "boolean", + "default": false } } }