mirror of
https://github.com/vacp2p/staking-demo-app.git
synced 2026-01-08 04:23:58 -05:00
unstaking
This commit is contained in:
26
src/lib/components/UnstakeButton.svelte
Normal file
26
src/lib/components/UnstakeButton.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { Address } from 'viem';
|
||||
import { vaultAccounts } from '$lib/viem';
|
||||
|
||||
export let vault: Address;
|
||||
export let vaultId: number;
|
||||
export let isLocked: boolean;
|
||||
export let onUnstake: () => void;
|
||||
export let fullWidth = false;
|
||||
|
||||
$: isEmpty = !$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n;
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={onUnstake}
|
||||
class="rounded-lg bg-white px-2 py-1.5 text-sm font-semibold text-blue-600 shadow-sm ring-1 ring-inset ring-blue-200 hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white {fullWidth ? 'w-full' : ''}"
|
||||
disabled={isLocked || isEmpty}
|
||||
>
|
||||
{#if isLocked}
|
||||
Locked
|
||||
{:else if isEmpty}
|
||||
Empty
|
||||
{:else}
|
||||
Unstake
|
||||
{/if}
|
||||
</button>
|
||||
321
src/lib/components/UnstakingModal.svelte
Normal file
321
src/lib/components/UnstakingModal.svelte
Normal file
@@ -0,0 +1,321 @@
|
||||
<script lang="ts">
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import type { Address } from 'viem';
|
||||
import { SNT_TOKEN, vaultAccounts, walletAddress, walletClient, publicClient, refreshBalances } from '$lib/viem';
|
||||
import { formatUnits, parseUnits, type Hash } from 'viem';
|
||||
|
||||
export let isOpen = false;
|
||||
export let onClose: () => void;
|
||||
export let vaultAddress: Address | undefined;
|
||||
export let vaultId: number;
|
||||
|
||||
let amount = '';
|
||||
let confirmUnderstand = false;
|
||||
let showError = false;
|
||||
let isUnstaking = false;
|
||||
let unstakeHash: Hash | undefined;
|
||||
let isCompleted = false;
|
||||
let unstakeError: string | undefined;
|
||||
|
||||
$: maxAmount = vaultAddress && $vaultAccounts[vaultAddress]
|
||||
? Number(formatUnits($vaultAccounts[vaultAddress].stakedBalance, SNT_TOKEN.decimals))
|
||||
: 0;
|
||||
|
||||
$: percentage = vaultAddress && $vaultAccounts[vaultAddress] && maxAmount > 0
|
||||
? (Number(amount) / maxAmount) * 100
|
||||
: 0;
|
||||
|
||||
$: mpsToBurn = vaultAddress && $vaultAccounts[vaultAddress]
|
||||
? (Number(formatMPs(vaultAddress)) * percentage / 100).toFixed(1)
|
||||
: '0.0';
|
||||
|
||||
$: if (vaultAddress && $vaultAccounts[vaultAddress] && !amount) {
|
||||
// Set initial amount to 10% of total
|
||||
amount = (maxAmount * 0.1).toFixed(2);
|
||||
}
|
||||
|
||||
function handleAmountInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const value = input.value;
|
||||
|
||||
if (!vaultAddress || !$vaultAccounts[vaultAddress]) return;
|
||||
|
||||
const numValue = Number(value);
|
||||
|
||||
if (numValue > maxAmount) {
|
||||
amount = maxAmount.toFixed(2);
|
||||
} else if (numValue < 1) {
|
||||
amount = '1.00';
|
||||
} else {
|
||||
amount = numValue.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSliderInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
amount = Number(input.value).toFixed(2);
|
||||
}
|
||||
|
||||
async function handleUnstake() {
|
||||
if (!confirmUnderstand) {
|
||||
showError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vaultAddress || !$walletAddress || !$walletClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isUnstaking = true;
|
||||
unstakeError = undefined;
|
||||
|
||||
// Convert amount to proper decimals
|
||||
const amountToUnstake = parseUnits(amount, SNT_TOKEN.decimals);
|
||||
|
||||
// Call unstake function
|
||||
unstakeHash = await $walletClient.writeContract({
|
||||
chain: publicClient.chain,
|
||||
account: $walletAddress,
|
||||
address: vaultAddress,
|
||||
abi: [{
|
||||
"inputs": [{"internalType": "uint256","name": "_amount","type": "uint256"}],
|
||||
"name": "unstake",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}],
|
||||
functionName: 'unstake',
|
||||
args: [amountToUnstake]
|
||||
});
|
||||
|
||||
// Wait for transaction confirmation
|
||||
const receipt = await publicClient.waitForTransactionReceipt({
|
||||
hash: unstakeHash,
|
||||
confirmations: 1
|
||||
});
|
||||
|
||||
if (receipt.status !== 'success') {
|
||||
throw new Error('Unstaking transaction failed');
|
||||
}
|
||||
|
||||
// Refresh balances and vault data
|
||||
await refreshBalances($walletAddress);
|
||||
isCompleted = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to unstake:', error);
|
||||
unstakeError = error instanceof Error ? error.message : 'Failed to unstake';
|
||||
} finally {
|
||||
isUnstaking = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatMPs(vault: Address): string {
|
||||
const account = $vaultAccounts[vault];
|
||||
if (!account?.mpAccrued) return '0.0';
|
||||
return Number(formatUnits(account.mpAccrued, SNT_TOKEN.decimals)).toFixed(1);
|
||||
}
|
||||
|
||||
function openTxOnEtherscan(hash: string | undefined) {
|
||||
if (hash) {
|
||||
window.open(`https://sepolia.etherscan.io/tx/${hash}`, '_blank');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
transition:fade={{ duration: 200 }}
|
||||
>
|
||||
<!-- Background overlay -->
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
||||
|
||||
<!-- Modal panel -->
|
||||
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<div
|
||||
class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"
|
||||
transition:fly={{ y: 20, duration: 200 }}
|
||||
>
|
||||
<!-- Close button -->
|
||||
<div class="absolute right-0 top-0 pr-4 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
on:click={onClose}
|
||||
disabled={isUnstaking}
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left w-full">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
|
||||
Unstake from Vault #{vaultId}
|
||||
</h3>
|
||||
|
||||
{#if isUnstaking || isCompleted}
|
||||
<div class="mt-6 space-y-6">
|
||||
<div class="flex items-center gap-3">
|
||||
{#if isUnstaking}
|
||||
<div class="h-8 w-8 flex items-center justify-center">
|
||||
<button
|
||||
class="animate-spin"
|
||||
on:click={() => openTxOnEtherscan(unstakeHash)}
|
||||
>
|
||||
<svg class="h-5 w-5 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900">Unstaking in progress...</p>
|
||||
</div>
|
||||
{:else if isCompleted}
|
||||
<div class="h-8 w-8 flex items-center justify-center">
|
||||
<svg class="h-8 w-8 text-green-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900">Successfully unstaked {amount} {SNT_TOKEN.symbol}</p>
|
||||
<button
|
||||
class="mt-1 text-sm text-blue-600 hover:text-blue-700"
|
||||
on:click={() => unstakeHash && openTxOnEtherscan(unstakeHash)}
|
||||
>
|
||||
View transaction
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isCompleted}
|
||||
<div class="mt-8 text-center">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex justify-center rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
on:click={onClose}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-6 space-y-6">
|
||||
<!-- Amount Slider -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Amount to Unstake
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max={maxAmount}
|
||||
step="0.01"
|
||||
value={amount}
|
||||
on:input={handleSliderInput}
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
<div class="mt-1 flex justify-between text-xs text-gray-500">
|
||||
<span>1 {SNT_TOKEN.symbol}</span>
|
||||
<span>{(maxAmount / 2).toFixed(2)} {SNT_TOKEN.symbol}</span>
|
||||
<span>{maxAmount.toFixed(2)} {SNT_TOKEN.symbol}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount Input -->
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
value={amount}
|
||||
on:input={handleAmountInput}
|
||||
min="1"
|
||||
max={maxAmount}
|
||||
step="0.01"
|
||||
class="block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<span class="text-sm text-gray-500">{SNT_TOKEN.symbol}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warning Box with Checkbox -->
|
||||
{#if vaultAddress && Number(amount) >= 1}
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0 pt-0.5">
|
||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-semibold text-red-700">
|
||||
Unstaking will burn MPs earned in this vault by {percentage.toFixed(0)}% ({mpsToBurn} MPs)
|
||||
</p>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<input
|
||||
id="confirmation"
|
||||
type="checkbox"
|
||||
bind:checked={confirmUnderstand}
|
||||
on:change={() => showError = false}
|
||||
class="h-4 w-4 rounded border-red-300 text-red-600 focus:ring-red-600"
|
||||
/>
|
||||
<label for="confirmation" class="text-sm text-red-700">
|
||||
I understand,
|
||||
</label>
|
||||
</div>
|
||||
{#if showError}
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
Please confirm that you understand the consequences of unstaking.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Unstake Button -->
|
||||
<div class="mt-8">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg bg-blue-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={Number(amount) < 1 || maxAmount === 0}
|
||||
on:click={handleUnstake}
|
||||
>
|
||||
{#if maxAmount === 0}
|
||||
No tokens to unstake
|
||||
{:else}
|
||||
Unstake {amount} {SNT_TOKEN.symbol}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if unstakeError}
|
||||
<p class="mt-2 text-sm text-red-600 text-center">
|
||||
{unstakeError}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -19,6 +19,12 @@
|
||||
goto('/stake');
|
||||
}
|
||||
|
||||
function handleStakeClick(vault: Address) {
|
||||
if (!isLocked(vault)) {
|
||||
goto('/stake?stakeVault=' + vault);
|
||||
}
|
||||
}
|
||||
|
||||
function shortenAddress(address: string): string {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
@@ -175,6 +181,7 @@
|
||||
<th class="px-6 py-3.5 text-right text-sm font-semibold text-gray-900">SNT Staked</th>
|
||||
<th class="px-6 py-3.5 text-right text-sm font-semibold text-gray-900">MPs</th>
|
||||
<th class="px-6 py-3.5 text-left text-sm font-semibold text-gray-900">Remaining Lock</th>
|
||||
<th class="w-[52px]"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@@ -223,6 +230,20 @@
|
||||
{formatRemainingLock(vault)}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
{#if !isLocked(vault)}
|
||||
<button
|
||||
on:click={() => handleStakeClick(vault)}
|
||||
class="rounded-full bg-blue-50 w-8 h-8 flex items-center justify-center text-blue-600 hover:bg-blue-100"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="w-8 h-8"></div>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
@@ -287,6 +308,21 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
{#if !isLocked(vault)}
|
||||
<button
|
||||
on:click={() => handleStakeClick(vault)}
|
||||
class="w-full rounded-lg bg-blue-50 px-2 py-1.5 text-sm font-semibold text-blue-600 hover:bg-blue-100 flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
Add Stake
|
||||
</button>
|
||||
{:else}
|
||||
<div class="h-[36px]"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { walletAddress, SNT_TOKEN, userVaults, vaultAccounts } from '$lib/viem';
|
||||
import { formatUnits, type Address } from 'viem';
|
||||
import UnstakingModal from '$lib/components/UnstakingModal.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let isUnstakingModalOpen = false;
|
||||
let selectedVaultAddress: Address | undefined;
|
||||
let selectedVaultId = 0;
|
||||
|
||||
function shortenAddress(address: string): string {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
@@ -14,8 +20,16 @@
|
||||
return Number(formatUnits(amount, SNT_TOKEN.decimals)).toFixed(2);
|
||||
}
|
||||
|
||||
function handleUnstake(vaultId: number) {
|
||||
alert(`Unstaking functionality for vault #${vaultId} will be implemented later`);
|
||||
function handleUnstake(vault: Address, vaultId: number) {
|
||||
selectedVaultAddress = vault;
|
||||
selectedVaultId = vaultId;
|
||||
isUnstakingModalOpen = true;
|
||||
}
|
||||
|
||||
function handleCloseUnstakingModal() {
|
||||
isUnstakingModalOpen = false;
|
||||
selectedVaultAddress = undefined;
|
||||
selectedVaultId = 0;
|
||||
}
|
||||
|
||||
function isLocked(vault: Address): boolean {
|
||||
@@ -54,6 +68,18 @@
|
||||
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
function handleLockClick(vault: Address) {
|
||||
if (!isLocked(vault) && $vaultAccounts[vault]?.stakedBalance && $vaultAccounts[vault].stakedBalance > 0n) {
|
||||
goto('/stake?vault=' + vault);
|
||||
}
|
||||
}
|
||||
|
||||
function handleStakeClick(vault: Address) {
|
||||
if (!isLocked(vault)) {
|
||||
goto('/stake?stakeVault=' + vault);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
@@ -103,9 +129,17 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
<button
|
||||
class="hover:text-blue-600"
|
||||
on:click={() => handleLockClick(vault)}
|
||||
disabled={!$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
class:opacity-50={!$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
class:cursor-not-allowed={!$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
>
|
||||
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
#{i + 1}
|
||||
</div>
|
||||
@@ -143,13 +177,33 @@
|
||||
{/if}
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<button
|
||||
on:click={() => handleUnstake(i + 1)}
|
||||
class="rounded-lg bg-white px-2 py-1.5 text-sm font-semibold text-blue-600 shadow-sm ring-1 ring-inset ring-blue-200 hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white"
|
||||
disabled={isLocked(vault)}
|
||||
>
|
||||
{isLocked(vault) ? 'Locked' : 'Unstake'}
|
||||
</button>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
on:click={() => handleUnstake(vault, i + 1)}
|
||||
class="rounded-lg bg-blue-50 px-2 py-1.5 text-sm font-semibold text-blue-600 hover:bg-blue-100 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-50"
|
||||
disabled={isLocked(vault) || !$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
>
|
||||
{#if isLocked(vault)}
|
||||
Locked
|
||||
{:else if !$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
Empty
|
||||
{:else}
|
||||
Unstake
|
||||
{/if}
|
||||
</button>
|
||||
{#if !isLocked(vault)}
|
||||
<button
|
||||
on:click={() => handleStakeClick(vault)}
|
||||
class="rounded-full bg-blue-50 w-8 h-8 flex items-center justify-center text-blue-600 hover:bg-blue-100"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="w-8 h-8"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
@@ -170,9 +224,17 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
<button
|
||||
class="hover:text-blue-600"
|
||||
on:click={() => handleLockClick(vault)}
|
||||
disabled={!$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
class:opacity-50={!$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
class:cursor-not-allowed={!$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
>
|
||||
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<h3 class="text-sm font-medium text-gray-900">Vault #{i + 1}</h3>
|
||||
</div>
|
||||
@@ -222,13 +284,33 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button
|
||||
on:click={() => handleUnstake(i + 1)}
|
||||
class="w-full rounded-lg bg-white px-2 py-1.5 text-sm font-semibold text-blue-600 shadow-sm ring-1 ring-inset ring-blue-200 hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white"
|
||||
disabled={isLocked(vault)}
|
||||
>
|
||||
{isLocked(vault) ? 'Locked' : 'Unstake'}
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
on:click={() => handleUnstake(vault, i + 1)}
|
||||
class="flex-1 rounded-lg bg-blue-50 px-2 py-1.5 text-sm font-semibold text-blue-600 hover:bg-blue-100 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-50"
|
||||
disabled={isLocked(vault) || !$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
>
|
||||
{#if isLocked(vault)}
|
||||
Locked
|
||||
{:else if !$vaultAccounts[vault]?.stakedBalance || $vaultAccounts[vault].stakedBalance === 0n}
|
||||
Empty
|
||||
{:else}
|
||||
Unstake
|
||||
{/if}
|
||||
</button>
|
||||
{#if !isLocked(vault)}
|
||||
<button
|
||||
on:click={() => handleStakeClick(vault)}
|
||||
class="rounded-full bg-blue-50 w-8 h-8 flex items-center justify-center text-blue-600 hover:bg-blue-100"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="w-8 h-8"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,4 +325,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UnstakingModal
|
||||
isOpen={isUnstakingModalOpen}
|
||||
onClose={handleCloseUnstakingModal}
|
||||
vaultAddress={selectedVaultAddress}
|
||||
vaultId={selectedVaultId}
|
||||
/>
|
||||
@@ -4,6 +4,7 @@
|
||||
import TransactionModal from '$lib/components/TransactionModal.svelte';
|
||||
import StakingModal from '$lib/components/StakingModal.svelte';
|
||||
import LockingModal from '$lib/components/LockingModal.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let amount = '';
|
||||
let selectedVaultId = '';
|
||||
@@ -45,8 +46,32 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Handle vault parameter in URL
|
||||
$: if ($page.url.searchParams.get('vault')) {
|
||||
const vaultFromUrl = $page.url.searchParams.get('vault') as Address;
|
||||
if ($userVaults.includes(vaultFromUrl) && !isLocked(vaultFromUrl) && $vaultAccounts[vaultFromUrl]?.stakedBalance && $vaultAccounts[vaultFromUrl].stakedBalance > 0n) {
|
||||
selectedLockVaultId = vaultFromUrl;
|
||||
// Scroll to the lock form
|
||||
setTimeout(() => {
|
||||
document.getElementById('lockForm')?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stakeVault parameter in URL
|
||||
$: if ($page.url.searchParams.get('stakeVault')) {
|
||||
const vaultFromUrl = $page.url.searchParams.get('stakeVault') as Address;
|
||||
if ($userVaults.includes(vaultFromUrl) && !isLocked(vaultFromUrl)) {
|
||||
selectedVaultId = vaultFromUrl;
|
||||
// Scroll to the stake form
|
||||
setTimeout(() => {
|
||||
document.getElementById('stakeForm')?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if vault is locked
|
||||
function isVaultLocked(vault: Address): boolean {
|
||||
function isLocked(vault: Address): boolean {
|
||||
const account = $vaultAccounts[vault];
|
||||
if (!account?.lockUntil) return false;
|
||||
return account.lockUntil > currentBlockTimestamp;
|
||||
@@ -321,6 +346,7 @@
|
||||
</p>
|
||||
|
||||
<form
|
||||
id="stakeForm"
|
||||
class="mt-6"
|
||||
on:submit|preventDefault={async (e) => {
|
||||
await handleStake();
|
||||
@@ -341,9 +367,11 @@
|
||||
>
|
||||
<option value="">Select a vault</option>
|
||||
{#each $userVaults as vault, i}
|
||||
<option value={vault}>
|
||||
Vault #{i + 1} - {shortenAddress(vault)}
|
||||
</option>
|
||||
{#if !isLocked(vault)}
|
||||
<option value={vault}>
|
||||
Vault #{i + 1} - {shortenAddress(vault)}
|
||||
</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
@@ -402,6 +430,7 @@
|
||||
|
||||
{#if $userVaults.some(vault => $vaultAccounts[vault] && $vaultAccounts[vault].stakedBalance > 0n)}
|
||||
<form
|
||||
id="lockForm"
|
||||
class="mt-6"
|
||||
on:submit|preventDefault={handleLock}
|
||||
>
|
||||
@@ -420,7 +449,7 @@
|
||||
>
|
||||
<option value="">Select a vault</option>
|
||||
{#each $userVaults as vault, i}
|
||||
{#if $vaultAccounts[vault] && $vaultAccounts[vault].stakedBalance > 0n && !isVaultLocked(vault)}
|
||||
{#if $vaultAccounts[vault] && $vaultAccounts[vault].stakedBalance > 0n && !isLocked(vault)}
|
||||
<option value={vault}>
|
||||
Vault #{i + 1} - {shortenAddress(vault)} ({formatAmount($vaultAccounts[vault].stakedBalance)} {SNT_TOKEN.symbol})
|
||||
</option>
|
||||
|
||||
Reference in New Issue
Block a user