diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 224bf8e1..a9057f50 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -496,6 +496,11 @@ access(all) contract FlowALPv0 { ) } + /// Returns the IDs of all currently open positions in this pool + access(all) view fun getPositionIDs(): [UInt64] { + return self.positions.keys + } + /// Returns the queued deposit balances for a given position. access(all) fun getQueuedDeposits(pid: UInt64): {Type: UFix64} { let position = self._borrowPosition(pid: pid) diff --git a/cadence/scripts/flow-alp/get_open_position_ids.cdc b/cadence/scripts/flow-alp/get_open_position_ids.cdc new file mode 100644 index 00000000..0a26c52a --- /dev/null +++ b/cadence/scripts/flow-alp/get_open_position_ids.cdc @@ -0,0 +1,29 @@ +// Returns the IDs of all currently open positions in the pool. +// A position is considered open if it has at least one non-zero balance. +import "FlowALPv0" + +access(all) fun main(): [UInt64] { + let protocolAddress = Type<@FlowALPv0.Pool>().address! + let account = getAccount(protocolAddress) + let pool = account.capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) + ?? panic("Could not find Pool at path \(FlowALPv0.PoolPublicPath)") + + let allIDs = pool.getPositionIDs() + let openIDs: [UInt64] = [] + for id in allIDs { + let details = pool.getPositionDetails(pid: id) + var hasBalance = false + for balance in details.balances { + if balance.balance > 0.0 { + hasBalance = true + break + } + } + + if hasBalance { + openIDs.append(id) + } + } + + return openIDs +} diff --git a/cadence/scripts/flow-alp/get_open_positions_by_ids.cdc b/cadence/scripts/flow-alp/get_open_positions_by_ids.cdc new file mode 100644 index 00000000..d579b98f --- /dev/null +++ b/cadence/scripts/flow-alp/get_open_positions_by_ids.cdc @@ -0,0 +1,29 @@ +// Returns the details of open positions for the given IDs. +// Positions with no non-zero balances (closed) are excluded from the result. +import "FlowALPv0" +import "FlowALPModels" + +access(all) fun main(positionIDs: [UInt64]): [FlowALPModels.PositionDetails] { + let protocolAddress = Type<@FlowALPv0.Pool>().address! + let account = getAccount(protocolAddress) + let pool = account.capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) + ?? panic("Could not find Pool at path \(FlowALPv0.PoolPublicPath)") + + let details: [FlowALPModels.PositionDetails] = [] + for id in positionIDs { + let d = pool.getPositionDetails(pid: id) + var hasBalance = false + for balance in d.balances { + if balance.balance > 0.0 { + hasBalance = true + break + } + } + + if hasBalance { + details.append(d) + } + } + + return details +} diff --git a/cadence/scripts/flow-alp/get_position_by_id.cdc b/cadence/scripts/flow-alp/get_position_by_id.cdc index 785a89ca..f019f73d 100644 --- a/cadence/scripts/flow-alp/get_position_by_id.cdc +++ b/cadence/scripts/flow-alp/get_position_by_id.cdc @@ -1,12 +1,12 @@ +// Returns the details of a specific position by its ID. import "FlowALPv0" import "FlowALPModels" -access(all) fun main(poolAddress: Address, positionID: UInt64): FlowALPModels.PositionDetails { - let account = getAccount(poolAddress) +access(all) fun main(positionID: UInt64): FlowALPModels.PositionDetails { + let protocolAddress = Type<@FlowALPv0.Pool>().address! + let account = getAccount(protocolAddress) + let pool = account.capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) + ?? panic("Could not find Pool at path \(FlowALPv0.PoolPublicPath)") - let poolRef = account.capabilities - .borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) - ?? panic("Could not borrow Pool reference from \(poolAddress)") - - return poolRef.getPositionDetails(pid: positionID) -} \ No newline at end of file + return pool.getPositionDetails(pid: positionID) +} diff --git a/cadence/tests/get_open_position_ids_test.cdc b/cadence/tests/get_open_position_ids_test.cdc new file mode 100644 index 00000000..e15e7f6c --- /dev/null +++ b/cadence/tests/get_open_position_ids_test.cdc @@ -0,0 +1,83 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowALPv0" +import "test_helpers.cdc" + +// ----------------------------------------------------------------------------- +// getOpenPositionIDs Test +// +// Verifies that get_open_position_ids.cdc correctly returns only IDs of +// positions that have at least one non-zero balance. +// ----------------------------------------------------------------------------- + +access(all) +fun setup() { + deployContracts() +} + +// ============================================================================= +// Test: getOpenPositionIDs tracks opens and closes correctly +// ============================================================================= +access(all) +fun test_getOpenPositionIDs_lifecycle() { + // --- Setup --- + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + + // --- No positions yet --- + var ids = getOpenPositionIDs() + Test.assertEqual(0, ids.length) + + // --- Open position 0 (no borrow) --- + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + ids = getOpenPositionIDs() + Test.assertEqual(1, ids.length) + Test.assert(ids.contains(UInt64(0)), message: "Expected position 0 in IDs") + + // --- Open position 1 (no borrow) --- + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 200.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + ids = getOpenPositionIDs() + Test.assertEqual(2, ids.length) + Test.assert(ids.contains(UInt64(0)), message: "Expected position 0 in IDs") + Test.assert(ids.contains(UInt64(1)), message: "Expected position 1 in IDs") + + // --- Close position 0 --- + closePosition(user: user, positionID: 0) + + ids = getOpenPositionIDs() + Test.assertEqual(1, ids.length) + Test.assert(!ids.contains(UInt64(0)), message: "Position 0 should be removed after close") + Test.assert(ids.contains(UInt64(1)), message: "Position 1 should still exist") + + // --- Open position 2 --- + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + ids = getOpenPositionIDs() + Test.assertEqual(2, ids.length) + Test.assert(ids.contains(UInt64(1)), message: "Position 1 should still exist") + Test.assert(ids.contains(UInt64(2)), message: "Expected position 2 in IDs") + + // --- Close remaining positions --- + closePosition(user: user, positionID: 1) + closePosition(user: user, positionID: 2) + + ids = getOpenPositionIDs() + Test.assertEqual(0, ids.length) +} diff --git a/cadence/tests/get_open_positions_by_ids_test.cdc b/cadence/tests/get_open_positions_by_ids_test.cdc new file mode 100644 index 00000000..06399912 --- /dev/null +++ b/cadence/tests/get_open_positions_by_ids_test.cdc @@ -0,0 +1,75 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowALPv0" +import "test_helpers.cdc" + +// ----------------------------------------------------------------------------- +// getOpenPositionsByIDs Test +// +// Verifies that the get_open_positions_by_ids.cdc script correctly returns +// position details only for open positions (those with non-zero balances). +// ----------------------------------------------------------------------------- + +access(all) +fun setup() { + deployContracts() +} + +// ============================================================================= +// Test: getOpenPositionsByIDs returns correct details and filters closed +// ============================================================================= +access(all) +fun test_getOpenPositionsByIDs() { + // --- Setup --- + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + + // --- Open two positions (no borrow to avoid MOET cross-contamination) --- + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 200.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + // --- Fetch both positions by IDs --- + let details = getOpenPositionsByIDs(positionIDs: [UInt64(0), UInt64(1)]) + Test.assertEqual(2, details.length) + + // Verify each result matches the single-position helper + let details0 = getPositionDetails(pid: 0, beFailed: false) + let details1 = getPositionDetails(pid: 1, beFailed: false) + + Test.assertEqual(details0.health, details[0].health) + Test.assertEqual(details0.balances.length, details[0].balances.length) + + Test.assertEqual(details1.health, details[1].health) + Test.assertEqual(details1.balances.length, details[1].balances.length) + + // --- Empty input returns empty array --- + let emptyDetails = getOpenPositionsByIDs(positionIDs: []) + Test.assertEqual(0, emptyDetails.length) + + // --- Single ID works --- + let singleDetails = getOpenPositionsByIDs(positionIDs: [UInt64(0)]) + Test.assertEqual(1, singleDetails.length) + Test.assertEqual(details0.health, singleDetails[0].health) + + // --- Close position 1 and verify it's filtered out --- + closePosition(user: user, positionID: 1) + + let afterClose = getOpenPositionsByIDs(positionIDs: [UInt64(0), UInt64(1)]) + Test.assertEqual(1, afterClose.length) + Test.assertEqual(details0.health, afterClose[0].health) +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 1d422c6c..7f1d7dc1 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -1062,6 +1062,36 @@ fun withdrawReserve( Test.expect(txRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } +access(all) +fun getOpenPositionIDs(): [UInt64] { + let res = _executeScript( + "../scripts/flow-alp/get_open_position_ids.cdc", + [] + ) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! [UInt64] +} + +access(all) +fun getOpenPositionsByIDs(positionIDs: [UInt64]): [FlowALPModels.PositionDetails] { + let res = _executeScript( + "../scripts/flow-alp/get_open_positions_by_ids.cdc", + [positionIDs] + ) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! [FlowALPModels.PositionDetails] +} + +access(all) +fun closePosition(user: Test.TestAccount, positionID: UInt64) { + let res = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [positionID], + user + ) + Test.expect(res, Test.beSucceeded()) +} + /* --- Assertion Helpers --- */ access(all) fun equalWithinVariance(_ expected: AnyStruct, _ actual: AnyStruct, _ variance: AnyStruct): Bool {