From c7bce4d0ff88802d79220fcb8dda923c5eb28c9c Mon Sep 17 00:00:00 2001 From: jtourkos Date: Fri, 16 May 2025 15:45:37 +0200 Subject: [PATCH 1/2] feat: sprinkle to sub projects --- src/{ => contracts}/drips-abi.ts | 0 src/{ => contracts}/drips-client.ts | 22 +- src/contracts/repoSubAccountDriver.ts | 78 ++++ src/contracts/repoSubAccountDriverAbi.ts | 366 ++++++++++++++++++ src/contracts/unwrapEthersResult.ts | 14 + src/getNetwork.ts | 6 + src/index.ts | 6 +- .../getAllDripListsSortedByCreationDate.ts | 2 +- .../getAllProjectsSortedByCreationDate.ts | 32 +- src/queries/getCurrentSplitsReceivers.ts | 26 +- src/queries/getTokens.ts | 8 +- src/types.ts | 1 + 12 files changed, 505 insertions(+), 56 deletions(-) rename src/{ => contracts}/drips-abi.ts (100%) rename src/{ => contracts}/drips-client.ts (81%) create mode 100644 src/contracts/repoSubAccountDriver.ts create mode 100644 src/contracts/repoSubAccountDriverAbi.ts create mode 100644 src/contracts/unwrapEthersResult.ts diff --git a/src/drips-abi.ts b/src/contracts/drips-abi.ts similarity index 100% rename from src/drips-abi.ts rename to src/contracts/drips-abi.ts diff --git a/src/drips-client.ts b/src/contracts/drips-client.ts similarity index 81% rename from src/drips-client.ts rename to src/contracts/drips-client.ts index 7d4bcdb..ef2e11d 100644 --- a/src/drips-client.ts +++ b/src/contracts/drips-client.ts @@ -4,10 +4,11 @@ import type { ExtractAbiFunction, ExtractAbiFunctionNames, } from 'abitype'; -import {dripsAbi, type DripsAbi} from './drips-abi'; import {Contract, TransactionResponse} from 'ethers'; -import appSettings from './appSettings'; -import {getContractRunner} from './getWalletInstance'; +import appSettings from '../appSettings'; +import {getContractRunner} from '../getWalletInstance'; +import {dripsAbi, DripsAbi} from './drips-abi'; +import {unwrapEthersResult, UnwrappedEthersResult} from './unwrapEthersResult'; const { network: { @@ -89,18 +90,3 @@ export async function dripsWriteContract< ); } } - -export function unwrapEthersResult( - result: T | T[], -): UnwrappedEthersResult | UnwrappedEthersResult { - if (Array.isArray(result) && result.length === 1) { - return result[0] as UnwrappedEthersResult; - } - return result as UnwrappedEthersResult; -} - -export type UnwrappedEthersResult = T extends [infer U] - ? U - : T extends readonly [infer U] - ? U - : T; diff --git a/src/contracts/repoSubAccountDriver.ts b/src/contracts/repoSubAccountDriver.ts new file mode 100644 index 0000000..188c567 --- /dev/null +++ b/src/contracts/repoSubAccountDriver.ts @@ -0,0 +1,78 @@ +import type { + AbiFunction, + AbiParametersToPrimitiveTypes, + ExtractAbiFunction, + ExtractAbiFunctionNames, +} from 'abitype'; +import {Contract} from 'ethers'; +import appSettings from '../appSettings'; +import {getContractRunner} from '../getWalletInstance'; +import {unwrapEthersResult, UnwrappedEthersResult} from './unwrapEthersResult'; +import { + repoSubAccountDriverAbi, + RepoSubAccountDriverAbi, +} from './repoSubAccountDriverAbi'; + +const { + network: { + contracts: {repoSubAccountDriver: contractAddress}, + name: networkName, + }, +} = appSettings; + +let contractInstance: Contract | null = null; + +async function getRepoSubAccountContract(): Promise { + if (contractInstance) { + return contractInstance; + } + + if (!contractAddress) { + throw new Error(`No contract address configured for chain: ${networkName}`); + } + + try { + contractInstance = new Contract( + contractAddress, + repoSubAccountDriverAbi, + await getContractRunner(), + ); + return contractInstance; + } catch (error) { + throw new Error( + `Failed to initialize contract: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } +} + +export async function repoSubAccountReadContract< + functionName extends ExtractAbiFunctionNames< + RepoSubAccountDriverAbi, + 'pure' | 'view' + >, + abiFunction extends AbiFunction = ExtractAbiFunction< + RepoSubAccountDriverAbi, + functionName + >, +>(config: { + functionName: + | functionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes; +}): Promise< + UnwrappedEthersResult< + AbiParametersToPrimitiveTypes + > +> { + try { + const repoSubAccount = await getRepoSubAccountContract(); + const {functionName: func, args} = config; + const result = await repoSubAccount[func](...args); + + return unwrapEthersResult(result); + } catch (error: any) { + throw new Error( + `Read operation '${config.functionName}' failed: ${error.message}`, + ); + } +} diff --git a/src/contracts/repoSubAccountDriverAbi.ts b/src/contracts/repoSubAccountDriverAbi.ts new file mode 100644 index 0000000..f5ea1cb --- /dev/null +++ b/src/contracts/repoSubAccountDriverAbi.ts @@ -0,0 +1,366 @@ +export const repoSubAccountDriverAbi = [ + { + inputs: [ + { + internalType: 'contract RepoDriver', + name: 'repoDriver_', + type: 'address', + }, + {internalType: 'address', name: 'forwarder', type: 'address'}, + {internalType: 'uint32', name: 'driverId_', type: 'uint32'}, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'previousAdmin', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'newAdmin', + type: 'address', + }, + ], + name: 'AdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + {indexed: true, internalType: 'address', name: 'beacon', type: 'address'}, + ], + name: 'BeaconUpgraded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'currentAdmin', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newAdmin', + type: 'address', + }, + ], + name: 'NewAdminProposed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + {indexed: true, internalType: 'address', name: 'pauser', type: 'address'}, + ], + name: 'Paused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + {indexed: true, internalType: 'address', name: 'pauser', type: 'address'}, + {indexed: true, internalType: 'address', name: 'admin', type: 'address'}, + ], + name: 'PauserGranted', + type: 'event', + }, + { + anonymous: false, + inputs: [ + {indexed: true, internalType: 'address', name: 'pauser', type: 'address'}, + {indexed: true, internalType: 'address', name: 'admin', type: 'address'}, + ], + name: 'PauserRevoked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + {indexed: true, internalType: 'address', name: 'pauser', type: 'address'}, + ], + name: 'Unpaused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'implementation', + type: 'address', + }, + ], + name: 'Upgraded', + type: 'event', + }, + { + inputs: [], + name: 'acceptAdmin', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'admin', + outputs: [{internalType: 'address', name: '', type: 'address'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'allPausers', + outputs: [ + {internalType: 'address[]', name: 'pausersList', type: 'address[]'}, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{internalType: 'uint256', name: 'accountId', type: 'uint256'}], + name: 'calcAccountId', + outputs: [ + {internalType: 'uint256', name: 'resultAccountId', type: 'uint256'}, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + {internalType: 'uint256', name: 'accountId', type: 'uint256'}, + {internalType: 'contract IERC20', name: 'erc20', type: 'address'}, + {internalType: 'address', name: 'transferTo', type: 'address'}, + ], + name: 'collect', + outputs: [{internalType: 'uint128', name: 'amt', type: 'uint128'}], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'drips', + outputs: [{internalType: 'contract Drips', name: '', type: 'address'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'driverId', + outputs: [{internalType: 'uint32', name: '', type: 'uint32'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + {internalType: 'uint256', name: 'accountId', type: 'uint256'}, + { + components: [ + {internalType: 'bytes32', name: 'key', type: 'bytes32'}, + {internalType: 'bytes', name: 'value', type: 'bytes'}, + ], + internalType: 'struct AccountMetadata[]', + name: 'accountMetadata', + type: 'tuple[]', + }, + ], + name: 'emitAccountMetadata', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + {internalType: 'uint256', name: 'accountId', type: 'uint256'}, + {internalType: 'uint256', name: 'receiver', type: 'uint256'}, + {internalType: 'contract IERC20', name: 'erc20', type: 'address'}, + {internalType: 'uint128', name: 'amt', type: 'uint128'}, + ], + name: 'give', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{internalType: 'address', name: 'pauser', type: 'address'}], + name: 'grantPauser', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'implementation', + outputs: [{internalType: 'address', name: '', type: 'address'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'isPaused', + outputs: [{internalType: 'bool', name: '', type: 'bool'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{internalType: 'address', name: 'pauser', type: 'address'}], + name: 'isPauser', + outputs: [{internalType: 'bool', name: 'isAddrPauser', type: 'bool'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{internalType: 'address', name: 'forwarder', type: 'address'}], + name: 'isTrustedForwarder', + outputs: [{internalType: 'bool', name: '', type: 'bool'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{internalType: 'uint256', name: 'accountId', type: 'uint256'}], + name: 'ownerOf', + outputs: [{internalType: 'address', name: 'owner', type: 'address'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'pause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{internalType: 'address', name: 'newAdmin', type: 'address'}], + name: 'proposeNewAdmin', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'proposedAdmin', + outputs: [{internalType: 'address', name: '', type: 'address'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'proxiableUUID', + outputs: [{internalType: 'bytes32', name: '', type: 'bytes32'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'renounceAdmin', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'repoDriver', + outputs: [{internalType: 'contract RepoDriver', name: '', type: 'address'}], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{internalType: 'address', name: 'pauser', type: 'address'}], + name: 'revokePauser', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + {internalType: 'uint256', name: 'accountId', type: 'uint256'}, + { + components: [ + {internalType: 'uint256', name: 'accountId', type: 'uint256'}, + {internalType: 'uint32', name: 'weight', type: 'uint32'}, + ], + internalType: 'struct SplitsReceiver[]', + name: 'receivers', + type: 'tuple[]', + }, + ], + name: 'setSplits', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + {internalType: 'uint256', name: 'accountId', type: 'uint256'}, + {internalType: 'contract IERC20', name: 'erc20', type: 'address'}, + { + components: [ + {internalType: 'uint256', name: 'accountId', type: 'uint256'}, + {internalType: 'StreamConfig', name: 'config', type: 'uint256'}, + ], + internalType: 'struct StreamReceiver[]', + name: 'currReceivers', + type: 'tuple[]', + }, + {internalType: 'int128', name: 'balanceDelta', type: 'int128'}, + { + components: [ + {internalType: 'uint256', name: 'accountId', type: 'uint256'}, + {internalType: 'StreamConfig', name: 'config', type: 'uint256'}, + ], + internalType: 'struct StreamReceiver[]', + name: 'newReceivers', + type: 'tuple[]', + }, + {internalType: 'uint32', name: 'maxEndHint1', type: 'uint32'}, + {internalType: 'uint32', name: 'maxEndHint2', type: 'uint32'}, + {internalType: 'address', name: 'transferTo', type: 'address'}, + ], + name: 'setStreams', + outputs: [ + {internalType: 'int128', name: 'realBalanceDelta', type: 'int128'}, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'unpause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + {internalType: 'address', name: 'newImplementation', type: 'address'}, + ], + name: 'upgradeTo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + {internalType: 'address', name: 'newImplementation', type: 'address'}, + {internalType: 'bytes', name: 'data', type: 'bytes'}, + ], + name: 'upgradeToAndCall', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, +] as const; + +export type RepoSubAccountDriverAbi = typeof repoSubAccountDriverAbi; diff --git a/src/contracts/unwrapEthersResult.ts b/src/contracts/unwrapEthersResult.ts new file mode 100644 index 0000000..827c566 --- /dev/null +++ b/src/contracts/unwrapEthersResult.ts @@ -0,0 +1,14 @@ +export type UnwrappedEthersResult = T extends [infer U] + ? U + : T extends readonly [infer U] + ? U + : T; + +export function unwrapEthersResult( + result: T | T[], +): UnwrappedEthersResult | UnwrappedEthersResult { + if (Array.isArray(result) && result.length === 1) { + return result[0] as UnwrappedEthersResult; + } + return result as UnwrappedEthersResult; +} diff --git a/src/getNetwork.ts b/src/getNetwork.ts index e5a76f6..71391a5 100644 --- a/src/getNetwork.ts +++ b/src/getNetwork.ts @@ -11,6 +11,7 @@ export default function getNetwork(chain: ChainId): Network { symbol: 'ETH', contracts: { drips: '0xd0Dd053392db676D57317CD4fe96Fc2cCf42D0b4', + repoSubAccountDriver: '0x0000000000000000000000000000000000000000', }, }; case 11155111: @@ -20,6 +21,7 @@ export default function getNetwork(chain: ChainId): Network { symbol: 'SepoliaETH', contracts: { drips: '0x74A32a38D945b9527524900429b083547DeB9bF4', + repoSubAccountDriver: '0x0000000000000000000000000000000000000000', }, }; case 314: @@ -29,6 +31,7 @@ export default function getNetwork(chain: ChainId): Network { symbol: 'FIL', contracts: { drips: '0xd320F59F109c618b19707ea5C5F068020eA333B3', + repoSubAccountDriver: '0x0000000000000000000000000000000000000000', }, }; case 1088: @@ -38,6 +41,7 @@ export default function getNetwork(chain: ChainId): Network { symbol: 'METIS', contracts: { drips: '0xd320F59F109c618b19707ea5C5F068020eA333B3', + repoSubAccountDriver: '0x0000000000000000000000000000000000000000', }, }; case 10: @@ -47,6 +51,7 @@ export default function getNetwork(chain: ChainId): Network { symbol: 'ETH', contracts: { drips: '0xd320F59F109c618b19707ea5C5F068020eA333B3', + repoSubAccountDriver: '0x0000000000000000000000000000000000000000', }, }; case 31337: @@ -56,6 +61,7 @@ export default function getNetwork(chain: ChainId): Network { symbol: 'ETH', contracts: { drips: '0x7CBbD3FdF9E5eb359E6D9B12848c5Faa81629944', + repoSubAccountDriver: '0xB8743C2bB8DF7399273aa7EE4cE8d4109Bec327F', }, }; } diff --git a/src/index.ts b/src/index.ts index fa8e3cf..e699e63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,10 @@ import {Client} from 'pg'; import {formatEther} from 'ethers'; import {getAllDripListsSortedByCreationDate} from './queries/getAllDripListsSortedByCreationDate'; -import {getAllProjectsSortedByCreationDate} from './queries/getAllProjectsSortedByCreationDate'; import getCurrentSplitsReceivers from './queries/getCurrentSplitsReceivers'; import getTokens from './queries/getTokens'; import getWalletInstance from './getWalletInstance'; -import {dripsReadContract, dripsWriteContract} from './drips-client'; +import {dripsReadContract, dripsWriteContract} from './contracts/drips-client'; import appSettings from './appSettings'; import retry from 'async-retry'; import { @@ -15,6 +14,7 @@ import { WriteOperation, } from './types'; import {notifyDiscord} from './notifyDiscord'; +import {getAllProjectsAndSubProjectSortedByCreationDate} from './queries/getAllProjectsSortedByCreationDate'; const MAX_CYCLES = 1000; const SCRIPT_ITERATIONS = 3; @@ -175,7 +175,7 @@ async function processProjects( console.log('\nProcessing projects...'); const writeOperations: WriteOperation[] = []; - const {rows: projects} = await getAllProjectsSortedByCreationDate(db); + const projects = await getAllProjectsAndSubProjectSortedByCreationDate(db); for (const project of projects) { const id = project.id.toString(); diff --git a/src/queries/getAllDripListsSortedByCreationDate.ts b/src/queries/getAllDripListsSortedByCreationDate.ts index 006014d..6a23291 100644 --- a/src/queries/getAllDripListsSortedByCreationDate.ts +++ b/src/queries/getAllDripListsSortedByCreationDate.ts @@ -10,6 +10,6 @@ export async function getAllDripListsSortedByCreationDate(db: Client) { id: bigint; createdAt: Date; }>({ - text: `SELECT * FROM "${dbSchema}"."DripLists" ORDER BY "createdAt" DESC`, + text: `SELECT * FROM ${dbSchema}."drip_lists" ORDER BY "created_at" DESC`, }); } diff --git a/src/queries/getAllProjectsSortedByCreationDate.ts b/src/queries/getAllProjectsSortedByCreationDate.ts index 2094d9a..5a67499 100644 --- a/src/queries/getAllProjectsSortedByCreationDate.ts +++ b/src/queries/getAllProjectsSortedByCreationDate.ts @@ -1,15 +1,33 @@ import {Client} from 'pg'; import appSettings from '../appSettings'; +import {toBigInt} from 'ethers'; +import {repoSubAccountReadContract} from '../contracts/repoSubAccountDriver'; const { network: {name: dbSchema}, } = appSettings; -export async function getAllProjectsSortedByCreationDate(db: Client) { - return db.query<{ - id: bigint; - createdAt: Date; - }>({ - text: `SELECT * FROM "${dbSchema}"."GitProjects" WHERE "verificationStatus" = 'Claimed' ORDER BY "createdAt" DESC`, - }); +export async function getAllProjectsAndSubProjectSortedByCreationDate( + db: Client, +) { + const projects = ( + await db.query<{ + id: bigint; + createdAt: Date; + }>({ + text: `SELECT * FROM ${dbSchema}.projects WHERE "verification_status" = 'claimed' ORDER BY "created_at" DESC`, + }) + ).rows; + + const subProjects = await Promise.all( + projects.map(async project => ({ + id: await repoSubAccountReadContract({ + functionName: 'calcAccountId', + args: [toBigInt(project.id)], + }), + createdAt: project.createdAt, + })), + ); + + return [...projects, ...subProjects]; } diff --git a/src/queries/getCurrentSplitsReceivers.ts b/src/queries/getCurrentSplitsReceivers.ts index 9364a73..b7b32ff 100644 --- a/src/queries/getCurrentSplitsReceivers.ts +++ b/src/queries/getCurrentSplitsReceivers.ts @@ -19,33 +19,13 @@ export default async function getCurrentSplitsReceivers( accountId: string, type: 'dripList' | 'project', ): Promise { - const idColumn = type === 'dripList' ? 'funderDripListId' : 'funderProjectId'; - - const {rows: addressSplits} = await db.query({ - text: `SELECT * FROM "${dbSchema}"."AddressDriverSplitReceivers" WHERE "${idColumn}" = $1`, - values: [accountId], - }); - - const {rows: dripListSplits} = await db.query({ - text: `SELECT * FROM "${dbSchema}"."DripListSplitReceivers" WHERE "${idColumn}" = $1`, - values: [accountId], - }); - - const {rows: projectSplits} = await db.query({ - text: `SELECT * FROM "${dbSchema}"."RepoDriverSplitReceivers" WHERE "${idColumn}" = $1`, + const {rows: splits} = await db.query({ + text: `SELECT * FROM "${dbSchema}"."splits_receivers" WHERE sender_account_id = $1`, values: [accountId], }); const splitsReceivers = sortSplitsReceivers([ - ...addressSplits.map(({fundeeAccountId, weight}) => ({ - accountId: BigInt(fundeeAccountId!), - weight: Number(weight), - })), - ...dripListSplits.map(({fundeeDripListId, weight}) => ({ - accountId: BigInt(fundeeDripListId!), - weight: Number(weight), - })), - ...projectSplits.map(({fundeeProjectId, weight}) => ({ + ...splits.map(({fundeeProjectId, weight}) => ({ accountId: BigInt(fundeeProjectId!), weight: Number(weight), })), diff --git a/src/queries/getTokens.ts b/src/queries/getTokens.ts index 20b5d5b..2d0182e 100644 --- a/src/queries/getTokens.ts +++ b/src/queries/getTokens.ts @@ -10,28 +10,28 @@ export default async function getTokens(db: Client) { const distinctGivenTokens = await db.query({ text: ` SELECT DISTINCT ON ("erc20") "erc20" - FROM "${dbSchema}"."GivenEvents" + FROM ${dbSchema}.given_events `, }); const distinctSplitTokens = await db.query({ text: ` SELECT DISTINCT ON ("erc20") "erc20" - FROM "${dbSchema}"."SplitEvents" + FROM ${dbSchema}.split_events `, }); const distinctStreamSetTokens = await db.query({ text: ` SELECT DISTINCT ON ("erc20") "erc20" - FROM "${dbSchema}"."StreamsSetEvents" + FROM ${dbSchema}.streams_set_events `, }); const distinctSqueezedStreamsTokens = await db.query({ text: ` SELECT DISTINCT ON ("erc20") "erc20" - FROM "${dbSchema}"."SqueezedStreamsEvents" + FROM ${dbSchema}.squeezed_streams_events `, }); diff --git a/src/types.ts b/src/types.ts index 9d6706b..91eaba8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export type Network = { symbol: string; contracts: { drips: string; + repoSubAccountDriver: string; }; }; From b3c6059171f97557109af66ffab790a7d45faabe Mon Sep 17 00:00:00 2001 From: jtourkos Date: Fri, 16 May 2025 15:52:43 +0200 Subject: [PATCH 2/2] fix: wrong sorting logic in project query --- src/queries/getAllProjectsSortedByCreationDate.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/queries/getAllProjectsSortedByCreationDate.ts b/src/queries/getAllProjectsSortedByCreationDate.ts index 5a67499..e3a31eb 100644 --- a/src/queries/getAllProjectsSortedByCreationDate.ts +++ b/src/queries/getAllProjectsSortedByCreationDate.ts @@ -29,5 +29,7 @@ export async function getAllProjectsAndSubProjectSortedByCreationDate( })), ); - return [...projects, ...subProjects]; + return [...projects, ...subProjects].sort( + (a, b) => b.createdAt.getTime() - a.createdAt.getTime(), + ); }