Skip to content

Commit 43270a9

Browse files
Merge pull request #297 from onflow/illia-malachyn/get-positions-should-not-panic
Get positions script should not panic
2 parents 1eb19fc + a2f3fd6 commit 43270a9

7 files changed

Lines changed: 137 additions & 4 deletions

File tree

cadence/contracts/FlowALPv0.cdc

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,13 +1927,29 @@ access(all) contract FlowALPv0 {
19271927
)
19281928

19291929
return PositionDetails(
1930+
id: pid,
19301931
balances: balances,
19311932
poolDefaultToken: self.defaultToken,
19321933
defaultTokenAvailableBalance: defaultTokenAvailable,
19331934
health: health
19341935
)
19351936
}
19361937

1938+
/// Returns the details of a given position, or nil if the position does not exist.
1939+
/// This is the non-panicking variant of getPositionDetails.
1940+
access(all) fun tryGetPositionDetails(pid: UInt64): PositionDetails? {
1941+
if self.debugLogging {
1942+
log(" [CONTRACT] tryGetPositionDetails(pid: \(pid))")
1943+
}
1944+
1945+
if self._tryBorrowPosition(pid: pid) == nil {
1946+
return nil
1947+
}
1948+
1949+
return self.getPositionDetails(pid: pid)
1950+
}
1951+
1952+
19371953
/// Any external party can perform a manual liquidation on a position under the following circumstances:
19381954
/// - the position has health < 1
19391955
/// - the liquidation price offered is better than what is available on a DEX
@@ -4079,9 +4095,14 @@ access(all) contract FlowALPv0 {
40794095
}
40804096
}
40814097

4082-
/// Returns an authorized reference to the requested InternalPosition or `nil` if the position does not exist
4083-
access(self) view fun _borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition {
4098+
/// Returns an authorized reference to the requested InternalPosition, or nil if it does not exist.
4099+
access(self) view fun _tryBorrowPosition(pid: UInt64): (auth(EImplementation) &InternalPosition)? {
40844100
return &self.positions[pid] as auth(EImplementation) &InternalPosition?
4101+
}
4102+
4103+
/// Returns an authorized reference to the requested InternalPosition or panics if the position does not exist.
4104+
access(self) view fun _borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition {
4105+
return self._tryBorrowPosition(pid: pid)
40854106
?? panic("Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool")
40864107
}
40874108

@@ -4762,6 +4783,9 @@ access(all) contract FlowALPv0 {
47624783
/// This structure is NOT used internally.
47634784
access(all) struct PositionDetails {
47644785

4786+
/// The unique identifier of the position
4787+
access(all) let id: UInt64
4788+
47654789
/// Balance details about each Vault Type deposited to the related Position
47664790
access(all) let balances: [PositionBalance]
47674791

@@ -4775,11 +4799,13 @@ access(all) contract FlowALPv0 {
47754799
access(all) let health: UFix128
47764800

47774801
init(
4802+
id: UInt64,
47784803
balances: [PositionBalance],
47794804
poolDefaultToken: Type,
47804805
defaultTokenAvailableBalance: UFix64,
47814806
health: UFix128
47824807
) {
4808+
self.id = id
47834809
self.balances = balances
47844810
self.poolDefaultToken = poolDefaultToken
47854811
self.defaultTokenAvailableBalance = defaultTokenAvailableBalance

cadence/scripts/flow-alp/get_positions_by_ids.cdc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// Returns the details of multiple positions by their IDs.
2+
// Positions that no longer exist (e.g., closed between fetching IDs and executing this script)
3+
// are silently skipped.
24
import "FlowALPv0"
35

46
access(all) fun main(positionIDs: [UInt64]): [FlowALPv0.PositionDetails] {
@@ -9,7 +11,9 @@ access(all) fun main(positionIDs: [UInt64]): [FlowALPv0.PositionDetails] {
911

1012
let details: [FlowALPv0.PositionDetails] = []
1113
for id in positionIDs {
12-
details.append(pool.getPositionDetails(pid: id))
14+
if let detail = pool.tryGetPositionDetails(pid: id) {
15+
details.append(detail)
16+
}
1317
}
1418
return details
1519
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Returns the details of a position by its ID, or nil if the position does not exist.
2+
import "FlowALPv0"
3+
4+
access(all) fun main(positionID: UInt64): FlowALPv0.PositionDetails? {
5+
let protocolAddress = Type<@FlowALPv0.Pool>().address!
6+
let account = getAccount(protocolAddress)
7+
let pool = account.capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath)
8+
?? panic("Could not find Pool at path \(FlowALPv0.PoolPublicPath)")
9+
10+
return pool.tryGetPositionDetails(pid: positionID)
11+
}

cadence/tests/get_positions_by_ids_test.cdc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,20 @@ fun test_getPositionsByIDs() {
6565
let singleDetails = getPositionsByIDs(positionIDs: [UInt64(0)])
6666
Test.assertEqual(1, singleDetails.length)
6767
Test.assertEqual(details0.health, singleDetails[0].health)
68+
69+
// --- Closed positions are silently skipped ---
70+
// Close position 1, then request both IDs — should only return position 0
71+
closePosition(user: user, positionID: 1)
72+
let afterClose = getPositionsByIDs(positionIDs: [UInt64(0), UInt64(1)])
73+
Test.assertEqual(1, afterClose.length)
74+
Test.assertEqual(details0.health, afterClose[0].health)
75+
76+
// --- All IDs closed/invalid returns empty array ---
77+
closePosition(user: user, positionID: 0)
78+
let allClosed = getPositionsByIDs(positionIDs: [UInt64(0), UInt64(1)])
79+
Test.assertEqual(0, allClosed.length)
80+
81+
// --- Non-existent IDs are skipped ---
82+
let nonExistent = getPositionsByIDs(positionIDs: [UInt64(999), UInt64(1000)])
83+
Test.assertEqual(0, nonExistent.length)
6884
}

cadence/tests/test_helpers.cdc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,16 @@ fun getPositionsByIDs(positionIDs: [UInt64]): [FlowALPv0.PositionDetails] {
810810
return res.returnValue as! [FlowALPv0.PositionDetails]
811811
}
812812

813+
access(all)
814+
fun tryGetPositionDetails(pid: UInt64): FlowALPv0.PositionDetails? {
815+
let res = _executeScript(
816+
"../scripts/flow-alp/try_get_position_details.cdc",
817+
[pid]
818+
)
819+
Test.expect(res, Test.beSucceeded())
820+
return res.returnValue as! FlowALPv0.PositionDetails?
821+
}
822+
813823
access(all)
814824
fun closePosition(user: Test.TestAccount, positionID: UInt64) {
815825
let res = _executeTransaction(
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import Test
2+
import BlockchainHelpers
3+
4+
import "MOET"
5+
import "FlowALPv0"
6+
import "test_helpers.cdc"
7+
8+
// -----------------------------------------------------------------------------
9+
// tryGetPositionDetails Test
10+
//
11+
// Verifies that Pool.tryGetPositionDetails() returns position details for
12+
// existing positions and nil for non-existent or closed positions.
13+
// -----------------------------------------------------------------------------
14+
15+
access(all)
16+
fun setup() {
17+
deployContracts()
18+
}
19+
20+
// =============================================================================
21+
// Test: tryGetPositionDetails returns details for open positions and nil otherwise
22+
// =============================================================================
23+
access(all)
24+
fun test_tryGetPositionDetails() {
25+
// --- Setup ---
26+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
27+
28+
createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false)
29+
addSupportedTokenZeroRateCurve(
30+
signer: PROTOCOL_ACCOUNT,
31+
tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER,
32+
collateralFactor: 0.8,
33+
borrowFactor: 1.0,
34+
depositRate: 1_000_000.0,
35+
depositCapacityCap: 1_000_000.0
36+
)
37+
38+
let user = Test.createAccount()
39+
setupMoetVault(user, beFailed: false)
40+
mintFlow(to: user, amount: 10_000.0)
41+
42+
// --- Non-existent position returns nil ---
43+
let nonExistent = tryGetPositionDetails(pid: 0)
44+
Test.assertEqual(nil, nonExistent)
45+
46+
// --- Open a position ---
47+
createPosition(signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false)
48+
49+
// --- Existing position returns details ---
50+
let details = tryGetPositionDetails(pid: 0)
51+
Test.assert(details != nil, message: "Expected non-nil details for open position")
52+
53+
// --- Result matches getPositionDetails ---
54+
let expected = getPositionDetails(pid: 0, beFailed: false)
55+
Test.assertEqual(expected.health, details!.health)
56+
Test.assertEqual(expected.balances.length, details!.balances.length)
57+
58+
// --- Still nil for non-existent ID ---
59+
let stillNil = tryGetPositionDetails(pid: 999)
60+
Test.assertEqual(nil, stillNil)
61+
62+
// --- Close the position, should return nil ---
63+
closePosition(user: user, positionID: 0)
64+
let afterClose = tryGetPositionDetails(pid: 0)
65+
Test.assertEqual(nil, afterClose)
66+
}

0 commit comments

Comments
 (0)