diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 31a11385..c53b2c44 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -1882,6 +1882,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 details of a given position as a PositionDetails external struct access(all) fun getPositionDetails(pid: UInt64): PositionDetails { if self.debugLogging { diff --git a/cadence/scripts/flow-alp/get_position_by_id.cdc b/cadence/scripts/flow-alp/get_position_by_id.cdc index 3586ba50..1fe5083d 100644 --- a/cadence/scripts/flow-alp/get_position_by_id.cdc +++ b/cadence/scripts/flow-alp/get_position_by_id.cdc @@ -1,11 +1,11 @@ +// Returns the details of a specific position by its ID. import "FlowALPv0" -access(all) fun main(poolAddress: Address, positionID: UInt64): FlowALPv0.PositionDetails { - let account = getAccount(poolAddress) +access(all) fun main(positionID: UInt64): FlowALPv0.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/scripts/flow-alp/get_position_ids.cdc b/cadence/scripts/flow-alp/get_position_ids.cdc new file mode 100644 index 00000000..016dacec --- /dev/null +++ b/cadence/scripts/flow-alp/get_position_ids.cdc @@ -0,0 +1,11 @@ +// Returns the IDs of all currently open positions in the pool. +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)") + + return pool.getPositionIDs() +} diff --git a/cadence/scripts/flow-alp/get_positions_by_ids.cdc b/cadence/scripts/flow-alp/get_positions_by_ids.cdc new file mode 100644 index 00000000..ce4ca48f --- /dev/null +++ b/cadence/scripts/flow-alp/get_positions_by_ids.cdc @@ -0,0 +1,15 @@ +// Returns the details of multiple positions by their IDs. +import "FlowALPv0" + +access(all) fun main(positionIDs: [UInt64]): [FlowALPv0.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: [FlowALPv0.PositionDetails] = [] + for id in positionIDs { + details.append(pool.getPositionDetails(pid: id)) + } + return details +} diff --git a/cadence/tests/get_position_ids_test.cdc b/cadence/tests/get_position_ids_test.cdc new file mode 100644 index 00000000..0873a7ee --- /dev/null +++ b/cadence/tests/get_position_ids_test.cdc @@ -0,0 +1,99 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowALPv0" +import "test_helpers.cdc" + +// ----------------------------------------------------------------------------- +// getPositionIDs Test +// +// Verifies that Pool.getPositionIDs() correctly reflects opened and closed +// positions via the get_position_ids.cdc script. +// ----------------------------------------------------------------------------- + +access(all) +fun setup() { + deployContracts() +} + +// ============================================================================= +// Test: getPositionIDs tracks opens and closes correctly +// ============================================================================= +access(all) +fun test_getPositionIDs_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 = getPositionIDs() + Test.assertEqual(0, ids.length) + + // --- Open position 0 (with borrow) --- + createPosition(signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + + ids = getPositionIDs() + Test.assertEqual(1, ids.length) + Test.assert(ids.contains(UInt64(0)), message: "Expected position 0 in IDs") + + // --- Open position 1 (with borrow) --- + createPosition(signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + + ids = getPositionIDs() + 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") + + // --- Open position 2 (no borrow, so closing won't need MOET repay) --- + createPosition(signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + ids = getPositionIDs() + Test.assertEqual(3, ids.length) + Test.assert(ids.contains(UInt64(2)), message: "Expected position 2 in IDs") + + // --- Close position 2 (no debt, straightforward) --- + closePosition(user: user, positionID: 2) + + ids = getPositionIDs() + Test.assertEqual(2, ids.length) + Test.assert(!ids.contains(UInt64(2)), message: "Position 2 should be removed after close") + Test.assert(ids.contains(UInt64(0)), message: "Position 0 should still exist") + Test.assert(ids.contains(UInt64(1)), message: "Position 1 should still exist") + + // --- Close position 0 (has debt, repay needed) --- + closePosition(user: user, positionID: 0) + + ids = getPositionIDs() + 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 3 (new position after some closures) --- + createPosition(signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + + ids = getPositionIDs() + Test.assertEqual(2, ids.length) + Test.assert(ids.contains(UInt64(1)), message: "Position 1 should still exist") + Test.assert(ids.contains(UInt64(3)), message: "Expected position 3 in IDs") + + // --- Close remaining positions --- + closePosition(user: user, positionID: 1) + closePosition(user: user, positionID: 3) + + ids = getPositionIDs() + Test.assertEqual(0, ids.length) +} diff --git a/cadence/tests/get_positions_by_ids_test.cdc b/cadence/tests/get_positions_by_ids_test.cdc new file mode 100644 index 00000000..4648f3bc --- /dev/null +++ b/cadence/tests/get_positions_by_ids_test.cdc @@ -0,0 +1,68 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowALPv0" +import "test_helpers.cdc" + +// ----------------------------------------------------------------------------- +// getPositionsByIDs Test +// +// Verifies that the get_positions_by_ids.cdc script correctly returns position +// details for the requested IDs. +// ----------------------------------------------------------------------------- + +access(all) +fun setup() { + deployContracts() +} + +// ============================================================================= +// Test: getPositionsByIDs returns correct details for multiple positions +// ============================================================================= +access(all) +fun test_getPositionsByIDs() { + // --- 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 --- + createPosition(signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(signer: user, amount: 200.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + // --- Fetch both positions by IDs --- + let details = getPositionsByIDs(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 = getPositionsByIDs(positionIDs: []) + Test.assertEqual(0, emptyDetails.length) + + // --- Single ID works --- + let singleDetails = getPositionsByIDs(positionIDs: [UInt64(0)]) + Test.assertEqual(1, singleDetails.length) + Test.assertEqual(details0.health, singleDetails[0].health) +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 2b9dbdd1..6afe0cfa 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -790,6 +790,36 @@ fun withdrawReserve( Test.expect(txRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } +access(all) +fun getPositionIDs(): [UInt64] { + let res = _executeScript( + "../scripts/flow-alp/get_position_ids.cdc", + [] + ) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! [UInt64] +} + +access(all) +fun getPositionsByIDs(positionIDs: [UInt64]): [FlowALPv0.PositionDetails] { + let res = _executeScript( + "../scripts/flow-alp/get_positions_by_ids.cdc", + [positionIDs] + ) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! [FlowALPv0.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): Bool {