Skip to content

Commit 8b08ae4

Browse files
authored
Merge pull request #102 from nolus-protocol/feature/rust-backend
Fix: Short Positions Asset Type
2 parents 49f087f + 7833032 commit 8b08ae4

11 files changed

Lines changed: 187 additions & 139 deletions

File tree

backend/src/handlers/leases.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -421,17 +421,21 @@ pub async fn get_leases(
421421
}
422422

423423
/// GET /api/leases/:address?protocol=...
424-
/// Returns details for a specific lease
424+
/// Returns details for a specific lease.
425+
/// Protocol is required — the backend cannot infer it from the lease address alone,
426+
/// and defaulting to a Long protocol produces wrong data for Short positions.
425427
pub async fn get_lease(
426428
State(state): State<Arc<AppState>>,
427429
Path(address): Path<String>,
428430
Query(query): Query<OptionalProtocolQuery>,
429431
) -> Result<Json<LeaseInfo>, AppError> {
430432
debug!("Getting lease: {}", address);
431433

432-
let protocol = query
433-
.protocol
434-
.unwrap_or_else(|| "OSMOSIS-OSMOSIS-USDC_NOBLE".to_string());
434+
let protocol = query.protocol.ok_or_else(|| AppError::Validation {
435+
message: "protocol query parameter is required".to_string(),
436+
field: Some("protocol".to_string()),
437+
details: None,
438+
})?;
435439

436440
fetch_lease_info(&state, &address, &protocol)
437441
.await

src/common/api/BackendApi.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,10 @@ export class BackendApiClient {
257257
return response.leases;
258258
}
259259

260-
async getLease(address: string): Promise<LeaseInfo> {
261-
return this.request<LeaseInfo>("GET", `/api/leases/${address}`);
260+
async getLease(address: string, protocol?: string): Promise<LeaseInfo> {
261+
return this.request<LeaseInfo>("GET", `/api/leases/${address}`, {
262+
params: protocol ? { protocol } : undefined
263+
});
262264
}
263265

264266
async getLeaseHistory(address: string, skip?: number, limit?: number): Promise<LeaseHistoryEntry[]> {

src/common/stores/leases/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,12 @@ export const useLeasesStore = defineStore("leases", () => {
186186
/**
187187
* Fetch details for a specific lease
188188
*/
189-
async function fetchLeaseDetails(address: string): Promise<LeaseInfo | null> {
189+
async function fetchLeaseDetails(address: string, protocol?: string): Promise<LeaseInfo | null> {
190190
try {
191-
const lease = await BackendApi.getLease(address);
191+
// Resolve protocol from cache if not provided — the backend defaults
192+
// to a Long protocol when none is supplied, which is wrong for Shorts.
193+
const resolvedProtocol = protocol || getLease(address)?.protocol;
194+
const lease = await BackendApi.getLease(address, resolvedProtocol);
192195
leaseDetails.value.set(address, lease);
193196

194197
// Update in the main list if present

src/modules/leases/components/Leases.vue

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
tableWrapperClasses="md:min-w-auto md:p-0"
3535
tableClasses="md:min-w-[1000px]"
3636
:scrollable="!mobile"
37-
:hide-values="{text: $t('message.toggle-values'), value: hide}"
37+
:hide-values="{ text: $t('message.toggle-values'), value: hide }"
3838
@hide-value="onHide"
3939
@onSearchClear="onSearch('')"
4040
@on-input="(e: Event) => onSearch((e.target as HTMLInputElement).value)"
@@ -122,7 +122,13 @@ import { useBalancesStore } from "@/common/stores/balances";
122122
import { usePricesStore } from "@/common/stores/prices";
123123
import { useConfigStore } from "@/common/stores/config";
124124
import { NATIVE_CURRENCY, UPDATE_LEASES } from "@/config/global";
125-
import { formatUsd, formatDecAsUsd, formatTokenBalance, formatMobileAmount, formatMobileUsd } from "@/common/utils/NumberFormatUtils";
125+
import {
126+
formatUsd,
127+
formatDecAsUsd,
128+
formatTokenBalance,
129+
formatMobileAmount,
130+
formatMobileUsd
131+
} from "@/common/utils/NumberFormatUtils";
126132
import { useRouter } from "vue-router";
127133
import type { IAction } from "./single-lease/Action.vue";
128134
import Action from "./single-lease/Action.vue";
@@ -159,22 +165,23 @@ let openMenuId: string | null;
159165
160166
let timeOut: NodeJS.Timeout;
161167
162-
const columns = computed<TableColumnProps[]>(() => isMobile()
163-
? [
164-
{ label: i18n.t("message.asset"), variant: "left" },
165-
{ label: i18n.t("message.lease-size") },
166-
{ label: i18n.t("message.pnl") },
167-
{ label: "", class: "!flex-none w-[40px]" }
168-
]
169-
: [
170-
{ label: i18n.t("message.lease"), variant: "left", class: "max-w-[150px]" },
171-
{ label: i18n.t("message.asset"), variant: "left" },
172-
{ label: i18n.t("message.type"), variant: "left", class: "max-w-[45px]" },
173-
{ label: i18n.t("message.pnl"), class: "max-w-[200px]" },
174-
{ label: i18n.t("message.lease-size") },
175-
{ label: i18n.t("message.liquidation-lease-table"), class: "max-w-[200px]" },
176-
{ label: "", class: "max-w-[220px]" }
177-
]
168+
const columns = computed<TableColumnProps[]>(() =>
169+
isMobile()
170+
? [
171+
{ label: i18n.t("message.asset"), variant: "left" },
172+
{ label: i18n.t("message.lease-size") },
173+
{ label: i18n.t("message.pnl") },
174+
{ label: "", class: "!flex-none w-[40px]" }
175+
]
176+
: [
177+
{ label: i18n.t("message.lease"), variant: "left", class: "max-w-[150px]" },
178+
{ label: i18n.t("message.asset"), variant: "left" },
179+
{ label: i18n.t("message.type"), variant: "left", class: "max-w-[45px]" },
180+
{ label: i18n.t("message.pnl"), class: "max-w-[200px]" },
181+
{ label: i18n.t("message.lease-size") },
182+
{ label: i18n.t("message.liquidation-lease-table"), class: "max-w-[200px]" },
183+
{ label: "", class: "max-w-[220px]" }
184+
]
178185
);
179186
180187
const isProtocolDisabled = computed(() => {
@@ -255,22 +262,31 @@ const leasesData = computed<TableRowItemProps[]>(() => {
255262
component: () =>
256263
loading
257264
? h("div", { class: "skeleton-box mb-2 rounded-[4px] w-[70px] h-[20px]" })
258-
: h("span", {
259-
class: `text-14 font-normal ${pnlData.status ? "text-typography-success" : "text-typography-error"}`
260-
}, `${pnlData.status ? "+" : ""}${pnlData.percent}%`),
265+
: h(
266+
"span",
267+
{
268+
class: `text-14 font-normal ${pnlData.status ? "text-typography-success" : "text-typography-error"}`
269+
},
270+
`${pnlData.status ? "+" : ""}${pnlData.percent}%`
271+
),
261272
click: navigate,
262273
class: "cursor-pointer"
263274
},
264275
{
265-
component: () => h<IAction>(Action, {
266-
lease: item,
267-
showClose: isOpened && !displayData.inProgressType,
268-
showDetails: true,
269-
key: `mob-action-${item.address}`,
270-
opened: openMenuId == item.address,
271-
onClick: (data: boolean) => { openMenuId = data ? item.address : null; },
272-
onSharePnl: () => { sharePnlDialog.value?.show(item, displayData); }
273-
}),
276+
component: () =>
277+
h<IAction>(Action, {
278+
lease: item,
279+
showClose: isOpened && !displayData.inProgressType,
280+
showDetails: true,
281+
key: `mob-action-${item.address}`,
282+
opened: openMenuId == item.address,
283+
onClick: (data: boolean) => {
284+
openMenuId = data ? item.address : null;
285+
},
286+
onSharePnl: () => {
287+
sharePnlDialog.value?.show(item, displayData);
288+
}
289+
}),
274290
class: "!flex-none w-[40px]"
275291
}
276292
]
@@ -410,6 +426,7 @@ function getAssetIcon(item: LeaseInfo) {
410426
if (item.status === "opening" && item.opening_info) {
411427
return configStore.assetIcons?.[`${item.opening_info.loan.ticker}@${item.protocol}`]!;
412428
}
429+
return configStore.assetIcons?.[`${item.debt.ticker}@${item.protocol}`] as string;
413430
}
414431
return configStore.assetIcons?.[`${positionTicker}@${item.protocol}`] as string;
415432
}
@@ -425,9 +442,9 @@ function getAsset(lease: LeaseInfo) {
425442
const asset = getCurrencyByDenom(item?.ibcData as string);
426443
return asset;
427444
} else if (positionType === "Short") {
428-
const positionTicker = lease.etl_data?.lease_position_ticker ?? lease.amount.ticker;
429-
if (!positionTicker) return null;
430-
const item = getCurrencyByTicker(positionTicker as string);
445+
const debtTicker = lease.debt.ticker;
446+
if (!debtTicker) return null;
447+
const item = getCurrencyByTicker(debtTicker as string);
431448
const asset = getCurrencyByDenom(item?.ibcData as string);
432449
return asset;
433450
}
@@ -527,7 +544,6 @@ function setLeases() {
527544
if (!(amount.isZero() || am.isZero())) {
528545
pnl_percent.value = am.quo(dp.add(rp)).mul(new Dec(100));
529546
}
530-
531547
} catch (e) {
532548
Logger.error(e);
533549
}

src/modules/leases/components/SingleLease.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ let timeOut: NodeJS.Timeout;
105105
// Lease state
106106
const lease = ref<LeaseInfo | null>(null);
107107
const leaseAddress = computed(() => route.params.id as string);
108-
const protocol = computed(() => lease.value?.protocol ?? "");
108+
const protocol = computed(() => lease.value?.protocol ?? leasesStore.getLease(leaseAddress.value)?.protocol ?? "");
109109
110110
// Computed display data for child components
111111
const displayData = computed<LeaseDisplayData | null>(() => {
@@ -115,7 +115,18 @@ const displayData = computed<LeaseDisplayData | null>(() => {
115115
116116
async function getLease() {
117117
try {
118-
const result = await leasesStore.fetchLeaseDetails(leaseAddress.value);
118+
// We must know the protocol before fetching — the backend defaults to a
119+
// Long protocol when none is supplied, which produces wrong data for Shorts.
120+
let proto = protocol.value;
121+
if (!proto) {
122+
// Protocol unknown (direct URL navigation). Fetch the full lease list
123+
// so the store discovers which protocol this lease belongs to.
124+
await leasesStore.fetchLeases();
125+
proto = leasesStore.getLease(leaseAddress.value)?.protocol ?? "";
126+
}
127+
if (!proto) return; // still unknown — don't fetch with wrong default
128+
129+
const result = await leasesStore.fetchLeaseDetails(leaseAddress.value, proto);
119130
if (result) {
120131
lease.value = result;
121132
}

src/modules/leases/components/single-lease/CloseDialog.vue

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -311,26 +311,30 @@ const lease = ref<LeaseInfo | null>(null);
311311
const displayData = ref<LeaseDisplayData | null>(null);
312312
const minAsset = ref<AmountSpec | null>(null);
313313
314-
async function fetchLease() {
315-
try {
316-
const result = await leasesStore.fetchLeaseDetails(route.params.id as string);
317-
if (result) {
318-
lease.value = result;
319-
displayData.value = leasesStore.getLeaseDisplayData(result);
320-
if (result.status === "closed") {
321-
router.push(`/${RouteNames.LEASES}`);
322-
}
323-
const positionSpec = await getLeasePositionSpec(result.protocol);
314+
async function initLease() {
315+
// Read from store cache — the parent already fetched this lease.
316+
// Avoid calling fetchLeaseDetails here: it mutates store state which
317+
// triggers the parent's watcher, re-renders, and unmounts this dialog.
318+
const cached = leasesStore.getLease(route.params.id as string);
319+
if (cached) {
320+
lease.value = cached;
321+
displayData.value = leasesStore.getLeaseDisplayData(cached);
322+
if (cached.status === "closed") {
323+
router.push(`/${RouteNames.LEASES}`);
324+
return;
325+
}
326+
try {
327+
const positionSpec = await getLeasePositionSpec(cached.protocol);
324328
minAsset.value = positionSpec.min_asset;
329+
} catch (error) {
330+
Logger.error(error);
325331
}
326-
} catch (error) {
327-
Logger.error(error);
328332
}
329333
}
330334
331335
onMounted(() => {
332336
dialog?.value?.show();
333-
fetchLease();
337+
initLease();
334338
});
335339
336340
onBeforeUnmount(() => {
@@ -419,20 +423,23 @@ const debtData = computed(() => {
419423
const debt = getRepayment(100);
420424
const d = debt?.repayment;
421425
if (price && d && lease.value) {
422-
const ticker = lease.value.etl_data?.lease_position_ticker ?? lease.value.amount.ticker;
423-
const currecy = configStore.currenciesData![`${ticker}@${lease.value.protocol}`];
424426
const positionType = configStore.getPositionType(lease.value.protocol);
425427
426428
if (positionType === "Short") {
429+
// For Short: debt is in the underlying asset (e.g. ATOM), use debt.ticker
430+
const debtTicker = lease.value.debt.ticker;
431+
const debtCurrency = configStore.currenciesData![`${debtTicker}@${lease.value.protocol}`];
427432
const asset = d.quo(price);
428433
const value = new Dec(amount.value).mul(new Dec(swapFee.value));
429434
return {
430435
fee: `${formatPercent(swapFee.value * PERCENT, NATIVE_CURRENCY.maximumFractionDigits)} (${formatDecAsUsd(value)})`,
431-
asset: currecy.shortName,
436+
asset: debtCurrency?.shortName ?? debtTicker,
432437
price: formatPriceUsd(price.toString(MAX_DECIMALS)),
433-
debt: `${formatTokenBalance(asset)} ${currecy.shortName}`
438+
debt: `${formatTokenBalance(asset)} ${debtCurrency?.shortName ?? debtTicker}`
434439
};
435440
} else {
441+
const ticker = lease.value.etl_data?.lease_position_ticker ?? lease.value.amount.ticker;
442+
const currecy = configStore.currenciesData![`${ticker}@${lease.value.protocol}`];
436443
const asset = d.mul(price);
437444
const value = new Dec(amount.value).mul(price).mul(new Dec(swapFee.value));
438445
let lpn = getLpnByProtocol(lease.value.protocol);

src/modules/leases/components/single-lease/PositionSummaryWidget.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161
></span>
162162
<template v-if="stopLoss">
163163
<span class="flex text-14 font-semibold text-typography-default">
164-
{{ stopLoss.amount }} {{ $t("message.per") }} {{ asset?.shortName }}
164+
{{ stopLoss.amount }} {{ $t("message.per") }} {{ pricerPerAsset?.shortName }}
165165
</span>
166166
</template>
167167
<div class="flex">
@@ -201,7 +201,7 @@
201201
></span>
202202
<template v-if="takeProfit">
203203
<span class="flex text-14 font-semibold text-typography-default">
204-
{{ takeProfit.amount }} {{ $t("message.per") }} {{ asset?.shortName }}
204+
{{ takeProfit.amount }} {{ $t("message.per") }} {{ pricerPerAsset?.shortName }}
205205
</span>
206206
</template>
207207
<div class="flex">
@@ -340,8 +340,8 @@ const pricerPerAsset = computed(() => {
340340
return asset.value;
341341
} else if (posType === "short") {
342342
const p = props.lease?.protocol!;
343-
const ticker = props.lease?.etl_data?.lease_position_ticker;
344-
const currency = configStore.currenciesData![`${ticker}@${p}`];
343+
const debtTicker = props.lease?.debt?.ticker;
344+
const currency = configStore.currenciesData![`${debtTicker}@${p}`];
345345
return currency;
346346
}
347347
return asset.value;

src/modules/leases/components/single-lease/PriceOverTimeChart.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ watch(
7878
);
7979
8080
const currency = computed(() => {
81-
const ticker = props.lease?.etl_data?.lease_position_ticker ?? props.lease?.amount?.ticker;
81+
const positionType = configStore.getPositionType(props.lease?.protocol!);
82+
const ticker = positionType === "Short"
83+
? props.lease?.debt?.ticker
84+
: (props.lease?.etl_data?.lease_position_ticker ?? props.lease?.amount?.ticker);
8285
const c = configStore.currenciesData?.[`${ticker}@${props.lease?.protocol}`];
8386
const price = pricesStore.prices[`${ticker}@${props.lease?.protocol}`];
8487
return {

0 commit comments

Comments
 (0)