2025-09-23 14:18:04 +02:00
< template >
2025-10-03 16:51:44 +02:00
< FCollapse class = "f-collapse-active" @collapse:opened ="loadActivePositionData" :loading = "loading" >
< template v -slot : header >
< div class = "collapse-header" >
< div class = "collapse-header-row1" >
< div > < span class = "subheader2" > Tax < / span > { { props . taxRate } } % < / div >
< FButton size = "tiny" @click ="payTax(props.id)" > Pay Tax < / FButton >
< div class = "position-id" >
< span class = "subheader2" > ID < / span > < span class = "number-small" > { { props . id } } < / span >
< / div >
< / div >
< div class = "collapse-header-row2" >
< div >
< div class = "profit-stats-item" >
< div > < b > Initial Stake < / b > < / div >
< div > { { compactNumber ( props . amount ) } } $KRK < / div >
< / div >
< / div >
< div class = "tags-list" >
< FTag v-if = "tag" > {{ tag }} < / FTag >
< / div >
< / div >
2026-02-18 00:19:05 +01:00
< div class = "pnl-metrics" >
< div class = "pnl-line1" : class = "{ 'pnl-positive': netReturn > 0, 'pnl-negative': netReturn < 0 }" >
Gross : { { formatPercent ( grossReturn ) } } · Tax : { { formatPercent ( - taxCostPercent ) } } · Net : { { formatPercent ( netReturn ) } }
< / div >
< div class = "pnl-line2" >
Held { { timeHeldFormatted } } · Snatched { { props . position . snatched } } time { { props . position . snatched === 1 ? '' : 's' } }
< / div >
< / div >
2025-10-03 16:51:44 +02:00
<!-- < div class = "collapse-amount" >
2025-09-23 14:18:04 +02:00
< span class = "number-small" > { { compactNumber ( props . amount ) } } < / span >
< span class = "caption" > $KRK < / span >
< / div > -- >
2025-10-03 16:51:44 +02:00
< / div >
< / template >
< div class = "collapsed-body" >
2026-03-03 01:51:48 +00:00
< div v-if = "error" class="collapsed-body--error" >
< span > { { error } } < / span >
< FButton size = "tiny" outlined @click ="loadActivePositionData" > Retry < / FButton >
< / div >
2026-03-03 02:23:06 +00:00
< div v-if = "!error || taxPaidGes !== undefined" class="profit-stats-wrapper" >
2025-10-03 16:51:44 +02:00
< div class = "profit-stats-item" >
< div > < b > Tax Paid < / b > < / div >
2026-03-03 03:03:22 +00:00
< div > { { taxPaidGes !== undefined ? formatTokenAmount ( taxPaidGes ) : '...' } } $KRK < / div >
2025-10-03 16:51:44 +02:00
< / div >
< div class = "profit-stats-item" >
< div > < b > Issuance Earned < / b > < / div >
2026-03-03 03:03:22 +00:00
< div > { { profit !== undefined ? formatTokenAmount ( profit ) : '...' } } $KRK < / div >
2025-10-03 16:51:44 +02:00
< / div >
< div class = "profit-stats-item profit-stats-total" >
< div > < b > Total < / b > < / div >
2026-03-03 03:32:06 +00:00
< div > { { taxPaidGes !== undefined && profit !== undefined ? formatTokenAmount ( total ) : '...' } } $KRK < / div >
2025-10-03 16:51:44 +02:00
< / div >
< / div >
< / div >
< div class = "collapsed-body--actions" >
< div : class = "{ 'collapse-menu-open': showTaxMenu }" >
< FButton size = "small" dense block outlined v-if = "adjustTaxRate.state === 'SignTransaction'" > Sign Transaction ... < / FButton >
< FButton size = "small" dense outlined block v -else -if = " adjustTaxRate.state = = = ' Waiting ' " @click ="unstakePosition"
> Waiting ... < / F B u t t o n
>
< FButton size = "small" dense block v -else -if = " adjustTaxRate.state = = = ' Action ' & & ! showTaxMenu " @ click = "showTaxMenu = true"
> Adjust Tax Rate < / F B u t t o n
>
< template v-else >
< div class = "collapse-menu-input" >
2025-10-07 19:26:08 +02:00
< FSelect :items = "filteredTaxRates" v-model = "newTaxRateIndex" > < / FSelect >
2025-10-03 16:51:44 +02:00
< / div >
< div >
2025-10-07 19:26:08 +02:00
< FButton size = "small" dense @ click = "changeTax(props.id, newTaxRateIndex)" > Confirm < / FButton >
2025-10-03 16:51:44 +02:00
< FButton size = "small" dense outlined @ click = "showTaxMenu = false" > Cancel < / FButton >
< / div >
< / template >
< / div >
< div > < / div >
< div >
< FButton size = "small" dense block outlined v-if = "unstake.state === 'SignTransaction'" > Sign Transaction ... < / FButton >
< FButton size = "small" dense outlined block v -else -if = " unstake.state = = = ' Waiting ' " > Waiting ... < / FButton >
< FButton size = "small" dense block v -else -if = " unstake.state = = = ' Unstakeable ' " @click ="unstakePosition" > Unstake < / FButton >
< / div >
< / div >
< / FCollapse >
2025-09-23 14:18:04 +02:00
< / template >
< script setup lang = "ts" >
2025-10-03 16:51:44 +02:00
import FButton from '@/components/fcomponents/FButton.vue' ;
import FTag from '@/components/fcomponents/FTag.vue' ;
import FSelect from '@/components/fcomponents/FSelect.vue' ;
import FCollapse from '@/components/fcomponents/FCollapse.vue' ;
2026-03-03 03:03:22 +00:00
import { compactNumber , weiToNumber , formatTokenAmount } from 'kraiken-lib/format' ;
2025-10-03 16:51:44 +02:00
import { useUnstake } from '@/composables/useUnstake' ;
import { useAdjustTaxRate } from '@/composables/useAdjustTaxRates' ;
import { computed , ref , onMounted } from 'vue' ;
import { getTaxDue , payTax } from '@/contracts/stake' ;
import { type Position , loadPositions } from '@/composables/usePositions' ;
import { useStatCollection } from '@/composables/useStatCollection' ;
2025-10-11 10:55:49 +00:00
import { useWallet } from '@/composables/useWallet' ;
import { DEFAULT _CHAIN _ID } from '@/config' ;
2025-11-20 19:44:10 +01:00
import { calculateActivePositionProfit } from 'kraiken-lib/position' ;
2025-09-23 14:18:04 +02:00
const unstake = useUnstake ( ) ;
const adjustTaxRate = useAdjustTaxRate ( ) ;
2025-10-11 10:55:49 +00:00
const wallet = useWallet ( ) ;
const initialChainId = wallet . account . chainId ? ? DEFAULT _CHAIN _ID ;
const statCollection = useStatCollection ( initialChainId ) ;
const currentChainId = computed ( ( ) => wallet . account . chainId ? ? DEFAULT _CHAIN _ID ) ;
2025-09-23 14:18:04 +02:00
const props = defineProps < {
2025-10-03 16:51:44 +02:00
taxRate : number ;
2025-10-07 19:26:08 +02:00
taxRateIndex ? : number ;
tresholdIndex : number ;
2025-10-03 16:51:44 +02:00
id : bigint ;
amount : number ;
position : Position ;
2025-09-23 14:18:04 +02:00
} > ( ) ;
const showTaxMenu = ref ( false ) ;
2025-10-07 19:26:08 +02:00
const newTaxRateIndex = ref < number | null > ( null ) ;
2026-02-28 13:51:04 +00:00
const taxDue = ref < bigint > ( 0 n ) ;
2026-02-25 08:06:49 +00:00
const taxPaidGes = ref < number > ( ) ;
2025-09-23 14:18:04 +02:00
const profit = ref < number > ( ) ;
const loading = ref < boolean > ( false ) ;
2026-03-03 01:51:48 +00:00
const error = ref < string | null > ( null ) ;
2025-09-23 14:18:04 +02:00
const tag = computed ( ( ) => {
2025-10-07 19:26:08 +02:00
// Compare by index instead of decimal to avoid floating-point issues
const idx = props . taxRateIndex ? ? props . position . taxRateIndex ;
if ( typeof idx === 'number' && idx < props . tresholdIndex ) {
2025-10-03 16:51:44 +02:00
return 'Low Tax!' ;
}
return '' ;
2025-09-23 14:18:04 +02:00
} ) ;
2026-02-26 20:42:17 +00:00
const total = computed ( ( ) => props . amount + ( profit . value ? ? 0 ) - ( taxPaidGes . value ? ? 0 ) ) ;
2025-09-23 14:18:04 +02:00
2026-02-18 00:19:05 +01:00
// P&L calculations (FIXED: Use BigInt math to preserve precision)
const grossReturn = computed ( ( ) => {
try {
const currentSupply = BigInt ( statCollection . kraikenTotalSupply ) ;
const initSupply = BigInt ( props . position . totalSupplyInit ) ;
2026-02-25 08:06:49 +00:00
2026-02-18 00:19:05 +01:00
if ( initSupply === 0 n ) return 0 ;
2026-02-25 08:06:49 +00:00
2026-02-18 00:19:05 +01:00
// Calculate percentage change using BigInt to avoid precision loss
// Formula: ((current - init) / init) * 100
// To maintain precision, multiply by 10000 first, then divide by 100 for display
const diff = currentSupply - initSupply ;
const percentBigInt = ( diff * 10000 n ) / initSupply ;
2026-02-25 08:06:49 +00:00
2026-02-18 00:19:05 +01:00
return Number ( percentBigInt ) / 100 ;
} catch ( error ) {
void error ; // suppress lint
return 0 ;
}
} ) ;
const taxCostPercent = computed ( ( ) => {
try {
const taxPaid = BigInt ( props . position . taxPaid ) ;
const deposit = BigInt ( props . position . harbDeposit ) ;
2026-02-25 08:06:49 +00:00
2026-02-18 00:19:05 +01:00
if ( deposit === 0 n ) return 0 ;
2026-02-25 08:06:49 +00:00
2026-02-18 00:19:05 +01:00
// Calculate percentage using BigInt precision
const percentBigInt = ( taxPaid * 10000 n ) / deposit ;
2026-02-25 08:06:49 +00:00
2026-02-18 00:19:05 +01:00
return Number ( percentBigInt ) / 100 ;
} catch ( error ) {
void error ; // suppress lint
return 0 ;
}
} ) ;
const netReturn = computed ( ( ) => {
return grossReturn . value - taxCostPercent . value ;
} ) ;
const timeHeldFormatted = computed ( ( ) => {
if ( ! props . position . creationTime ) return '0d 0h' ;
// Handle both Date objects and bigint/number timestamps
let creationTimestamp : number ;
if ( props . position . creationTime instanceof Date ) {
creationTimestamp = Math . floor ( props . position . creationTime . getTime ( ) / 1000 ) ;
} else {
creationTimestamp = Number ( props . position . creationTime ) ;
}
const nowTimestamp = Math . floor ( Date . now ( ) / 1000 ) ;
const secondsHeld = nowTimestamp - creationTimestamp ;
const days = Math . floor ( secondsHeld / 86400 ) ;
const hours = Math . floor ( ( secondsHeld % 86400 ) / 3600 ) ;
return ` ${ days } d ${ hours } h ` ;
} ) ;
function formatPercent ( value : number ) : string {
const sign = value >= 0 ? '+' : '' ;
return ` ${ sign } ${ value . toFixed ( 1 ) } % ` ;
}
2025-10-07 19:26:08 +02:00
const filteredTaxRates = computed ( ( ) => {
const currentIndex = props . position . taxRateIndex ? ? - 1 ;
return adjustTaxRate . taxRates . filter ( option => option . index > currentIndex ) ;
} ) ;
async function changeTax ( id : bigint , nextTaxRateIndex : number | null ) {
if ( typeof nextTaxRateIndex !== 'number' ) {
return ;
}
await adjustTaxRate . changeTax ( id , nextTaxRateIndex ) ;
2025-10-03 16:51:44 +02:00
showTaxMenu . value = false ;
2025-09-23 14:18:04 +02:00
}
async function unstakePosition ( ) {
2025-10-03 16:51:44 +02:00
await unstake . exitPosition ( props . id ) ;
loading . value = true ;
2026-03-03 22:10:06 +00:00
// eslint-disable-next-line no-restricted-syntax -- Polling with timeout: no push event exists for Ponder indexing completion; Ponder GraphQL has no subscription endpoint. See AGENTS.md #Engineering Principles.
2025-10-03 16:51:44 +02:00
await new Promise ( resolve => setTimeout ( resolve , 5000 ) ) ;
2025-10-11 10:55:49 +00:00
await loadPositions ( currentChainId . value ) ;
2025-10-03 16:51:44 +02:00
loading . value = false ;
2025-09-23 14:18:04 +02:00
}
async function loadActivePositionData ( ) {
2026-03-03 01:51:48 +00:00
loading . value = true ;
error . value = null ;
try {
//loadTaxDue
taxDue . value = await getTaxDue ( props . id ) ;
taxPaidGes . value = weiToNumber ( taxDue . value + props . position . taxPaid ) ;
2025-09-23 14:18:04 +02:00
2026-03-03 01:51:48 +00:00
//loadTotalSupply
2025-09-23 14:18:04 +02:00
2026-03-03 01:51:48 +00:00
// Calculate issuance earned using kraiken-lib profit calculation
profit . value = calculateActivePositionProfit ( props . position . totalSupplyInit , statCollection . kraikenTotalSupply , props . position . share ) ;
} catch ( err ) {
2026-03-03 02:00:05 +00:00
// eslint-disable-next-line no-console
2026-03-03 01:51:48 +00:00
console . error ( 'Failed to load active position data:' , err ) ;
error . value = 'Failed to load position data.' ;
} finally {
loading . value = false ;
}
2025-09-23 14:18:04 +02:00
}
onMounted ( ( ) => {
2025-10-07 19:26:08 +02:00
const availableRates = filteredTaxRates . value ;
if ( availableRates . length > 0 ) {
newTaxRateIndex . value = availableRates [ 0 ] ? . index ? ? null ;
} else if ( typeof props . position . taxRateIndex === 'number' ) {
newTaxRateIndex . value = props . position . taxRateIndex ;
} else {
newTaxRateIndex . value = adjustTaxRate . taxRates [ 0 ] ? . index ? ? null ;
2025-10-03 16:51:44 +02:00
}
2025-09-23 14:18:04 +02:00
} ) ;
< / script >
< style lang = "sass" >
@ use 'collapse'
. f - collapse
& . f - collapse - active
background - color : # 07111 B
. collapse - header
width : 100 %
display : flex
justify - content : space - between
flex - direction : column
position : relative
gap : 16 px
. collapse - header - row1
display : flex
flex - direction : row
gap : 16 px
align - items : center
// margin-right: 32px
. position - id
margin - left : auto
. collapse - header - row2
display : flex
justify - content : space - between
> div
width : 50 %
min - height : 22 px
. profit - stats - item
display : grid
grid - template - columns : 1 fr 1 fr
gap : 8 px
div
& : last - child
text - align : right
. tags - list
margin - right : 32 px
text - align : right
2026-02-18 00:19:05 +01:00
. pnl - metrics
display : flex
flex - direction : column
gap : 4 px
padding - top : 8 px
font - size : 13 px
. pnl - line1
font - weight : 500
& . pnl - positive
color : # 4 ade80
& . pnl - negative
color : # f87171
. pnl - line2
color : # 9 A9898
font - size : 12 px
2025-09-23 14:18:04 +02:00
. collapsableDiv
. collapsed - body
display : flex
flex - direction : column
margin - right : 32 px
. collapsed - body -- header
justify - content : space - between
. profit - stats - wrapper
display : flex
flex - direction : column
gap : 4 px
margin : 0
margin - top : 4 px
width : 100 %
color : white
@ media ( min - width : 768 px )
width : 50 %
margin : 4 px auto 0 0
. profit - stats - item
display : grid
grid - template - columns : 1 fr 1 fr
gap : 8 px
div
& : last - child
text - align : right
& . profit - stats - total
border - top : 2 px solid var ( -- color - font )
padding - top : 2 px
2026-03-03 01:51:48 +00:00
. collapsed - body -- error
display : flex
2026-03-03 02:23:06 +00:00
flex - wrap : wrap
2026-03-03 01:51:48 +00:00
align - items : center
gap : 8 px
color : # f87171
font - size : 13 px
margin - bottom : 8 px
2025-09-23 14:18:04 +02:00
. collapsed - body -- actions
. collapse - menu - open
display : flex
align - items : center
gap : 4 px
flex : 1 1 auto
. collapse - menu - input
display : flex
align - items : center
< / style >