Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [0.40.1](https://github.com/aragon/app-backend/compare/v0.40.0...v0.40.1) (2026-10-07)

### Bug Fixes

* **members:** count delegators from the index only ([5062eb6](https://github.com/aragon/app-backend/commit/5062eb637ea853b71516084ece0a0782c494d44a))
* **transactions:** check spam once per token before paging ([ccc66dd](https://github.com/aragon/app-backend/commit/ccc66ddf89a164401afe49d888faa601daeb287f))
## [0.40.0](https://github.com/aragon/app-backend/compare/v0.39.0...v0.40.0) (2026-10-05)

### Features
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "aragon-backend",
"description": "OpenSource backend services",
"version": "0.40.0",
"version": "0.40.1",
"main": "index.ts",
"license": "AGPL-3.0",
"author": "Aragon Development Team",
Expand Down
49 changes: 18 additions & 31 deletions src/models/schema/logDelegateChanged.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,37 +111,24 @@ export default class LogDelegateChanged extends Model {
): Promise<Record<string, number>> {
if (memberAddresses.length === 0) return {}

const results = await this.aggregate([
{
$match: {
network,
tokenAddress,
$or: [{ toDelegate: { $in: memberAddresses } }, { fromDelegate: { $in: memberAddresses } }],
},
},
{
$project: {
deltas: {
$concatArrays: [
{
$cond: [{ $in: ['$toDelegate', memberAddresses] }, [{ delegate: '$toDelegate', delta: 1 }], []],
},
{
$cond: [{ $in: ['$fromDelegate', memberAddresses] }, [{ delegate: '$fromDelegate', delta: -1 }], []],
},
],
},
},
},
{ $unwind: '$deltas' },
{ $group: { _id: '$deltas.delegate', count: { $sum: '$deltas.delta' } } },
{ $match: { count: { $gt: 0 } } },
]).allowDiskUse(true)

return results.reduce((acc: Record<string, number>, item: { _id: string; count: number }) => {
acc[item._id] = item.count
return acc
}, {})
const [incoming, outgoing] = await Promise.all(
(['toDelegate', 'fromDelegate'] as const).map(field =>
this.aggregate<{ _id: string; count: number }>([
{ $match: { network, tokenAddress, [field]: { $in: memberAddresses } } },
{ $group: { _id: `$${field}`, count: { $sum: 1 } } },
]).allowDiskUse(true),
),
)

const counts: Record<string, number> = {}
for (const { _id, count } of incoming) counts[_id] = count
for (const { _id, count } of outgoing) counts[_id] = (counts[_id] ?? 0) - count

const result: Record<string, number> = {}
for (const [member, count] of Object.entries(counts)) {
if (count > 0) result[member] = count
}
return result
}

static async findDelegatorsForMember(
Expand Down
111 changes: 51 additions & 60 deletions src/models/schema/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,74 +348,65 @@ export default class Transaction extends Model {
filter.daoAddress = { $in: extraParams.daoAddresses }
}

const spamFilterStages =
extraParams.includeSpam === true
? []
: [
{
$lookup: {
from: ICollectionNames.Token,
localField: 'token.address',
foreignField: 'address',
let: { txNetwork: '$network' },
pipeline: [
{ $match: { $expr: { $eq: ['$network', '$$txNetwork'] } } },
{ $project: { _id: 0, isSpam: 1 } },
{ $limit: 1 },
],
as: '_tokenRef',
},
},
{ $match: { '_tokenRef.isSpam': { $ne: true } } },
{ $project: { _tokenRef: 0 } },
]

const currentPage = request.skip / request.limit + 1

const dataPipeline: any[] = [
{ $match: filter },
const pageStages = [
{ $sort: request.sort },
{ $project: { rawActions: 0, actions: 0 } },
...spamFilterStages,
{ $skip: request.skip },
{ $limit: request.limit },
]

const countPipeline: any[] =
extraParams.includeSpam === true
? [{ $match: filter }, { $count: 'total' }]
: [
{ $match: filter },
{
$group: {
_id: { network: '$network', address: '$token.address' },
records: { $sum: 1 },
},
},
{
$lookup: {
from: ICollectionNames.Token,
localField: '_id.address',
foreignField: 'address',
let: { txNetwork: '$_id.network' },
pipeline: [
{ $match: { $expr: { $eq: ['$network', '$$txNetwork'] } } },
{ $project: { _id: 0, isSpam: 1 } },
{ $limit: 1 },
],
as: '_tokenRef',
},
},
{ $match: { '_tokenRef.isSpam': { $ne: true } } },
{ $group: { _id: null, total: { $sum: '$records' } } },
]

const [rawData, countResult] = await Promise.all([
this.aggregate(dataPipeline).allowDiskUse(true),
this.aggregate(countPipeline).allowDiskUse(true),
])

const totalRecords = countResult.length > 0 ? countResult[0].total : 0
let totalRecords = 0
let rawData: any[] = []

if (extraParams.includeSpam === true) {
const [countResult, rows] = await Promise.all([
this.aggregate([{ $match: filter }, { $count: 'total' }]).allowDiskUse(true),
this.aggregate([{ $match: filter }, ...pageStages]).allowDiskUse(true),
])
totalRecords = countResult.length > 0 ? countResult[0].total : 0
rawData = rows
} else {
const [summary] = await this.aggregate([
{ $match: filter },
{ $group: { _id: { network: '$network', address: '$token.address' }, records: { $sum: 1 } } },
{
$lookup: {
from: ICollectionNames.Token,
localField: '_id.address',
foreignField: 'address',
let: { txNetwork: '$_id.network' },
pipeline: [
{ $match: { $expr: { $eq: ['$network', '$$txNetwork'] } } },
{ $project: { _id: 0, isSpam: 1 } },
{ $limit: 1 },
],
as: '_tokenRef',
},
},
{ $set: { isSpam: { $in: [true, '$_tokenRef.isSpam'] } } },
{
$group: {
_id: null,
totalRecords: { $sum: { $cond: ['$isSpam', 0, '$records'] } },
spamTokens: { $push: { $cond: ['$isSpam', '$_id', '$$REMOVE'] } },
},
},
]).allowDiskUse(true)

const spamTokens: { network: string; address?: string }[] = summary?.spamTokens ?? []
totalRecords = summary?.totalRecords ?? 0

let pageFilter: any = filter
if (spamTokens.length > 0) {
const spamPairs = spamTokens.map(token => ({ network: token.network, 'token.address': token.address ?? null }))
pageFilter = { ...filter, $nor: spamPairs }
}

rawData = await this.aggregate([{ $match: pageFilter }, ...pageStages]).allowDiskUse(true)
}

const totalPages = Math.ceil(totalRecords / request.limit)

if (currentPage > totalPages) {
Expand Down
71 changes: 71 additions & 0 deletions test/unit/models/schema/logDelegateChanged.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,77 @@ describe('Model: LogDelegateChanged', () => {
const result = await Models.LogDelegateChanged.countActiveDelegationsForMembers(TOKEN_ADDRESS, NETWORK, [])
expect(result).to.deep.equal({})
})

describe('when counting both sides of a delegation', () => {
const delegate = (fromDelegate: string, toDelegate: string, transactionHash: string, extra = {}) =>
Models.LogDelegateChanged.create({
network: NETWORK,
tokenAddress: TOKEN_ADDRESS,
delegator: ALICE,
fromDelegate,
toDelegate,
blockNumber: 50,
blockTimestamp: 500,
transactionHash,
transactionIndex: 0,
logIndex: 0,
...extra,
})

it('moves the delegator from the old member to the new one', async () => {
await delegate(ZERO, BOB, '0xboth1')
await delegate(BOB, JORDAN, '0xboth2')

const result = await Models.LogDelegateChanged.countActiveDelegationsForMembers(TOKEN_ADDRESS, NETWORK, [
BOB,
JORDAN,
])

expect(result).to.deep.equal({ [JORDAN]: 1 })
})

it('keeps the count when a log delegates from and to the same member', async () => {
await delegate(ZERO, BOB, '0xself1')
await delegate(BOB, BOB, '0xself2')

const result = await Models.LogDelegateChanged.countActiveDelegationsForMembers(TOKEN_ADDRESS, NETWORK, [BOB])

expect(result).to.deep.equal({ [BOB]: 1 })
})

it('leaves out a member with no logs and a member with only outgoing logs', async () => {
await delegate(BOB, JORDAN, '0xout1')

const result = await Models.LogDelegateChanged.countActiveDelegationsForMembers(TOKEN_ADDRESS, NETWORK, [
ALICE,
BOB,
JORDAN,
])

expect(result).to.deep.equal({ [JORDAN]: 1 })
})

it('counts once when the same member is asked twice', async () => {
await delegate(ZERO, BOB, '0xdup1')

const result = await Models.LogDelegateChanged.countActiveDelegationsForMembers(TOKEN_ADDRESS, NETWORK, [
BOB,
BOB,
])

expect(result).to.deep.equal({ [BOB]: 1 })
})

it('ignores logs from another token or network', async () => {
await delegate(ZERO, BOB, '0xscope1')
await delegate(ZERO, BOB, '0xscope2', { tokenAddress: ZERO })
await delegate(BOB, JORDAN, '0xscope3', { network: NetworksEnum.polygonMainnet })

const result = await Models.LogDelegateChanged.countActiveDelegationsForMembers(TOKEN_ADDRESS, NETWORK, [BOB])

expect(result).to.deep.equal({ [BOB]: 1 })
})
})
})

describe('findDelegatorsForMember', () => {
Expand Down
Loading
Loading