Skip to content

Commit 6a9c489

Browse files
authored
feat: add pagination to _getRailsForAddressAndToken (#237)
1 parent 8313e55 commit 6a9c489

File tree

2 files changed

+108
-34
lines changed

2 files changed

+108
-34
lines changed

src/Payments.sol

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,42 +1688,61 @@ contract Payments is ReentrancyGuard {
16881688
* @notice Gets all rails where the given address is the payer for a specific token.
16891689
* @param payer The address of the payer to get rails for.
16901690
* @param token The token address to filter rails by.
1691-
* @return Array of RailInfo structs containing rail IDs and termination status.
1691+
* @param offset The offset to start from.
1692+
* @param limit Maximum number of entries to return
1693+
* @return results Array of RailInfo structs containing rail IDs and termination status.
1694+
* @return nextOffset The next offset to use for pagination.
1695+
* @return total The total number of rails.
16921696
*/
1693-
function getRailsForPayerAndToken(address payer, IERC20 token) external view returns (RailInfo[] memory) {
1694-
return _getRailsForAddressAndToken(payer, token, true);
1697+
function getRailsForPayerAndToken(address payer, IERC20 token, uint256 offset, uint256 limit)
1698+
external
1699+
view
1700+
returns (RailInfo[] memory results, uint256 nextOffset, uint256 total)
1701+
{
1702+
return _getRailsForAddressAndToken(payerRails[token][payer], offset, limit);
16951703
}
16961704

16971705
/**
16981706
* @notice Gets all rails where the given address is the payee for a specific token.
16991707
* @param payee The address of the payee to get rails for.
17001708
* @param token The token address to filter rails by.
1701-
* @return Array of RailInfo structs containing rail IDs and termination status.
1709+
* @param offset The offset to start from.
1710+
* @param limit Maximum number of entries to return
1711+
* @return results Array of RailInfo structs containing rail IDs and termination status.
1712+
* @return nextOffset The next offset to use for pagination.
1713+
* @return total The total number of rails.
17021714
*/
1703-
function getRailsForPayeeAndToken(address payee, IERC20 token) external view returns (RailInfo[] memory) {
1704-
return _getRailsForAddressAndToken(payee, token, false);
1715+
function getRailsForPayeeAndToken(address payee, IERC20 token, uint256 offset, uint256 limit)
1716+
external
1717+
view
1718+
returns (RailInfo[] memory results, uint256 nextOffset, uint256 total)
1719+
{
1720+
return _getRailsForAddressAndToken(payeeRails[token][payee], offset, limit);
17051721
}
17061722

17071723
/**
17081724
* @dev Internal function to get rails for either a payer or payee.
1709-
* @param addr The address to get rails for (either payer or payee).
1710-
* @param token The token address to filter rails by.
1711-
* @param isPayer If true, search for rails where addr is the payer, otherwise search for rails where addr is the payee.
1712-
* @return Array of RailInfo structs containing rail IDs and termination status.
1725+
* @param allRailIds The array of rail IDs to filter rails by.
1726+
* @param offset The offset to start from.
1727+
* @param limit Maximum number of entries to return
1728+
* @return results Array of RailInfo structs containing rail IDs and termination status.
1729+
* @return nextOffset The next offset to use for pagination.
1730+
* @return total The total number of rails.
17131731
*/
1714-
function _getRailsForAddressAndToken(address addr, IERC20 token, bool isPayer)
1732+
function _getRailsForAddressAndToken(uint256[] storage allRailIds, uint256 offset, uint256 limit)
17151733
internal
17161734
view
1717-
returns (RailInfo[] memory)
1735+
returns (RailInfo[] memory results, uint256 nextOffset, uint256 total)
17181736
{
1719-
// Get the appropriate list of rails based on whether we're looking for payer or payee
1720-
uint256[] storage allRailIds = isPayer ? payerRails[token][addr] : payeeRails[token][addr];
17211737
uint256 railsLength = allRailIds.length;
1738+
if (limit == 0) limit = railsLength;
1739+
if (offset >= railsLength) return (new RailInfo[](0), railsLength, railsLength);
1740+
uint256 end = offset + limit > railsLength ? railsLength : offset + limit;
17221741

1723-
RailInfo[] memory results = new RailInfo[](railsLength);
1742+
results = new RailInfo[](end - offset);
17241743
uint256 resultCount = 0;
17251744

1726-
for (uint256 i = 0; i < railsLength; i++) {
1745+
for (uint256 i = offset; i < end; i++) {
17271746
uint256 railId = allRailIds[i];
17281747
Rail storage rail = rails[railId];
17291748

@@ -1740,7 +1759,7 @@ contract Payments is ReentrancyGuard {
17401759
mstore(results, resultCount)
17411760
}
17421761

1743-
return results;
1762+
return (results, end, railsLength);
17441763
}
17451764

17461765
/// @notice Number of pending rate-change entries for a rail

test/RailGetters.t.sol

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ contract PayeeRailsTest is Test, BaseTestHelper {
149149

150150
function testGetRailsForPayeeAndToken() public view {
151151
// Test getting all rails for USER2 and token1 (should include terminated)
152-
Payments.RailInfo[] memory rails = payments.getRailsForPayeeAndToken(USER2, token);
152+
(Payments.RailInfo[] memory rails,,) = payments.getRailsForPayeeAndToken(USER2, token, 0, 3);
153153

154154
// Should include 3 rails: rail1Id, rail2Id, and rail3Id (terminated)
155155
assertEq(rails.length, 3, "Should have 3 rails for USER2 with token1");
@@ -180,14 +180,14 @@ contract PayeeRailsTest is Test, BaseTestHelper {
180180
assertTrue(foundRail3, "Rail 3 not found");
181181

182182
// Test different token (should only return rails for that token)
183-
Payments.RailInfo[] memory token2Rails = payments.getRailsForPayeeAndToken(USER2, token2);
183+
(Payments.RailInfo[] memory token2Rail,,) = payments.getRailsForPayeeAndToken(USER2, token2, 0, 0);
184184

185185
// Should include only 1 rail with token2: rail4Id
186-
assertEq(token2Rails.length, 1, "Should have 1 rail for USER2 with token2");
187-
assertEq(token2Rails[0].railId, rail4Id, "Rail ID should match rail4Id");
186+
assertEq(token2Rail.length, 1, "Should have 1 rail for USER2 with token2");
187+
assertEq(token2Rail[0].railId, rail4Id, "Rail ID should match rail4Id");
188188

189189
// Test different payee (should only return rails for that payee)
190-
Payments.RailInfo[] memory user3Rails = payments.getRailsForPayeeAndToken(USER3, token);
190+
(Payments.RailInfo[] memory user3Rails,,) = payments.getRailsForPayeeAndToken(USER3, token, 0, 0);
191191

192192
// Should include only 1 rail for USER3: rail5Id
193193
assertEq(user3Rails.length, 1, "Should have 1 rail for USER3 with token1");
@@ -196,7 +196,7 @@ contract PayeeRailsTest is Test, BaseTestHelper {
196196

197197
function testGetRailsForPayerAndToken() public view {
198198
// Test getting all rails for USER1 (payer) and token1 (should include terminated)
199-
Payments.RailInfo[] memory rails = payments.getRailsForPayerAndToken(USER1, token);
199+
(Payments.RailInfo[] memory rails,,) = payments.getRailsForPayerAndToken(USER1, token, 0, 4);
200200

201201
// Should include 4 rails: rail1Id, rail2Id, rail3Id (terminated), and rail5Id
202202
assertEq(rails.length, 4, "Should have 4 rails for USER1 with token1");
@@ -233,7 +233,7 @@ contract PayeeRailsTest is Test, BaseTestHelper {
233233
assertTrue(foundRail5, "Rail 5 not found");
234234

235235
// Test different token (should only return rails for that token)
236-
Payments.RailInfo[] memory token2Rails = payments.getRailsForPayerAndToken(USER1, token2);
236+
(Payments.RailInfo[] memory token2Rails,,) = payments.getRailsForPayerAndToken(USER1, token2, 0, 0);
237237

238238
// Should include only 1 rail with token2: rail4Id
239239
assertEq(token2Rails.length, 1, "Should have 1 rail for USER1 with token2");
@@ -242,8 +242,8 @@ contract PayeeRailsTest is Test, BaseTestHelper {
242242

243243
function testRailsBeyondEndEpoch() public {
244244
// Get the initial rails when Rail 3 is terminated but not beyond its end epoch
245-
Payments.RailInfo[] memory initialPayeeRails = payments.getRailsForPayeeAndToken(USER2, token);
246-
Payments.RailInfo[] memory initialPayerRails = payments.getRailsForPayerAndToken(USER1, token);
245+
(Payments.RailInfo[] memory initialPayeeRails,,) = payments.getRailsForPayeeAndToken(USER2, token, 0, 3);
246+
(Payments.RailInfo[] memory initialPayerRails,,) = payments.getRailsForPayerAndToken(USER1, token, 0, 4);
247247

248248
// Should include all 3 rails for payee
249249
assertEq(initialPayeeRails.length, 3, "Should have 3 rails initially for payee");
@@ -269,8 +269,8 @@ contract PayeeRailsTest is Test, BaseTestHelper {
269269
payments.settleRail(rail3Id, endEpoch);
270270

271271
// Get rails again for both payee and payer
272-
Payments.RailInfo[] memory finalPayeeRails = payments.getRailsForPayeeAndToken(USER2, token);
273-
Payments.RailInfo[] memory finalPayerRails = payments.getRailsForPayerAndToken(USER1, token);
272+
(Payments.RailInfo[] memory finalPayeeRails,,) = payments.getRailsForPayeeAndToken(USER2, token, 0, 3);
273+
(Payments.RailInfo[] memory finalPayerRails,,) = payments.getRailsForPayerAndToken(USER1, token, 0, 4);
274274

275275
// Should include only 2 rails now for payee, as Rail 3 is beyond its end epoch
276276
assertEq(finalPayeeRails.length, 2, "Should have 2 rails for payee after advancing beyond end epoch");
@@ -303,21 +303,76 @@ contract PayeeRailsTest is Test, BaseTestHelper {
303303

304304
function testEmptyResult() public view {
305305
// Test non-existent payee
306-
Payments.RailInfo[] memory nonExistentPayee = payments.getRailsForPayeeAndToken(address(0x123), token);
306+
(Payments.RailInfo[] memory nonExistentPayee,,) = payments.getRailsForPayeeAndToken(address(0x123), token, 0, 0);
307307
assertEq(nonExistentPayee.length, 0, "Should return empty array for non-existent payee");
308308

309309
// Test non-existent payer
310-
Payments.RailInfo[] memory nonExistentPayer = payments.getRailsForPayerAndToken(address(0x123), token);
310+
(Payments.RailInfo[] memory nonExistentPayer,,) = payments.getRailsForPayerAndToken(address(0x123), token, 0, 0);
311311
assertEq(nonExistentPayer.length, 0, "Should return empty array for non-existent payer");
312312

313313
// Test non-existent token for payee
314-
Payments.RailInfo[] memory nonExistentTokenForPayee =
315-
payments.getRailsForPayeeAndToken(USER2, IERC20(address(0x456)));
314+
(Payments.RailInfo[] memory nonExistentTokenForPayee,,) =
315+
payments.getRailsForPayeeAndToken(USER2, IERC20(address(0x456)), 0, 0);
316316
assertEq(nonExistentTokenForPayee.length, 0, "Should return empty array for non-existent token with payee");
317317

318318
// Test non-existent token for payer
319-
Payments.RailInfo[] memory nonExistentTokenForPayer =
320-
payments.getRailsForPayerAndToken(USER1, IERC20(address(0x456)));
319+
(Payments.RailInfo[] memory nonExistentTokenForPayer,,) =
320+
payments.getRailsForPayerAndToken(USER1, IERC20(address(0x456)), 0, 0);
321321
assertEq(nonExistentTokenForPayer.length, 0, "Should return empty array for non-existent token with payer");
322322
}
323+
324+
function testPagination() public view {
325+
// Test pagination for payee rails (USER2 has 3 rails with token1)
326+
327+
// Test getting first 2 rails
328+
(Payments.RailInfo[] memory page1, uint256 nextOffset1, uint256 total1) =
329+
payments.getRailsForPayeeAndToken(USER2, token, 0, 2);
330+
331+
assertEq(page1.length, 2, "First page should have 2 rails");
332+
assertEq(nextOffset1, 2, "Next offset should be 2");
333+
assertEq(total1, 3, "Total should be 3");
334+
335+
// Test getting remaining rail
336+
(Payments.RailInfo[] memory page2, uint256 nextOffset2, uint256 total2) =
337+
payments.getRailsForPayeeAndToken(USER2, token, nextOffset1, 2);
338+
339+
assertEq(page2.length, 1, "Second page should have 1 rail");
340+
assertEq(nextOffset2, 3, "Next offset should be 3 (end of array)");
341+
assertEq(total2, 3, "Total should still be 3");
342+
343+
// Verify no duplicate rails between pages
344+
bool duplicateFound = false;
345+
for (uint256 i = 0; i < page1.length; i++) {
346+
for (uint256 j = 0; j < page2.length; j++) {
347+
if (page1[i].railId == page2[j].railId) {
348+
duplicateFound = true;
349+
break;
350+
}
351+
}
352+
}
353+
assertFalse(duplicateFound, "No duplicate rails should exist between pages");
354+
355+
// Test offset beyond array length
356+
(Payments.RailInfo[] memory emptyPage, uint256 nextOffset3, uint256 total3) =
357+
payments.getRailsForPayeeAndToken(USER2, token, 10, 2);
358+
359+
assertEq(emptyPage.length, 0, "Should return empty array for offset beyond length");
360+
assertEq(nextOffset3, 3, "Next offset should equal total length");
361+
assertEq(total3, 3, "Total should still be 3");
362+
363+
// Test pagination for payer rails (USER1 has 4 rails with token1)
364+
(Payments.RailInfo[] memory payerPage1, uint256 payerNext1, uint256 payerTotal1) =
365+
payments.getRailsForPayerAndToken(USER1, token, 0, 3);
366+
367+
assertEq(payerPage1.length, 3, "Payer first page should have 3 rails");
368+
assertEq(payerNext1, 3, "Payer next offset should be 3");
369+
assertEq(payerTotal1, 4, "Payer total should be 4");
370+
371+
(Payments.RailInfo[] memory payerPage2, uint256 payerNext2, uint256 payerTotal2) =
372+
payments.getRailsForPayerAndToken(USER1, token, payerNext1, 3);
373+
374+
assertEq(payerPage2.length, 1, "Payer second page should have 1 rail");
375+
assertEq(payerNext2, 4, "Payer next offset should be 4 (end of array)");
376+
assertEq(payerTotal2, 4, "Payer total should still be 4");
377+
}
323378
}

0 commit comments

Comments
 (0)