fix: feat: Add sell KRK widget to get-krk page (#456)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
466e0d7767
commit
36c798605f
2 changed files with 245 additions and 10 deletions
|
|
@ -4,6 +4,22 @@
|
|||
<p class="swap-warning">Connect your wallet to swap ETH for $KRK on the local Uniswap pool.</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="swap-tabs">
|
||||
<button
|
||||
class="swap-tab"
|
||||
:class="{ active: mode === 'buy' }"
|
||||
data-testid="swap-mode-buy"
|
||||
@click="setMode('buy')"
|
||||
>Buy</button>
|
||||
<button
|
||||
class="swap-tab"
|
||||
:class="{ active: mode === 'sell' }"
|
||||
data-testid="swap-mode-sell"
|
||||
@click="setMode('sell')"
|
||||
>Sell</button>
|
||||
</div>
|
||||
|
||||
<template v-if="mode === 'buy'">
|
||||
<div class="swap-field">
|
||||
<label for="local-swap-amount" class="swap-label">ETH to spend</label>
|
||||
<input id="local-swap-amount" data-testid="swap-amount-input" :value="swapAmount" @input="swapAmount = ($event.target as HTMLInputElement).value" type="number" min="0" step="0.01" class="swap-input" :disabled="swapping" />
|
||||
|
|
@ -13,13 +29,53 @@
|
|||
</button>
|
||||
<p class="swap-hint">Wraps ETH → WETH, approves the swap router, then trades into $KRK.</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="swap-field">
|
||||
<div class="swap-label-row">
|
||||
<label for="local-sell-amount" class="swap-label">KRK to sell</label>
|
||||
<span class="swap-balance">Balance: {{ formattedKrkBalance }} KRK</span>
|
||||
</div>
|
||||
<div class="swap-input-row">
|
||||
<input id="local-sell-amount" data-testid="swap-sell-amount-input" :value="sellAmount" @input="sellAmount = ($event.target as HTMLInputElement).value" type="number" min="0" step="1" class="swap-input" :disabled="selling" />
|
||||
<button class="max-button" :disabled="selling" @click="setMax">Max</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="swap-button sell-button" data-testid="swap-sell-button" :disabled="selling" @click="sellKrk">
|
||||
{{ selling ? (sellPhase === 'approving' ? 'Approving…' : 'Selling…') : 'Sell KRK' }}
|
||||
</button>
|
||||
<p class="swap-hint">Approves the swap router, then trades $KRK → WETH.</p>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useSwapKrk } from '@/composables/useSwapKrk';
|
||||
import { formatUnits } from 'viem';
|
||||
|
||||
const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
||||
const { swapAmount, swapping, canSwap, buyKrk, sellAmount, selling, sellPhase, krkBalance, loadKrkBalance, sellKrk } = useSwapKrk();
|
||||
|
||||
const mode = ref<'buy' | 'sell'>('buy');
|
||||
|
||||
const formattedKrkBalance = computed(() => {
|
||||
const val = Number(formatUnits(krkBalance.value, 18));
|
||||
return val.toLocaleString(undefined, { maximumFractionDigits: 4 });
|
||||
});
|
||||
|
||||
function setMode(m: 'buy' | 'sell') {
|
||||
mode.value = m;
|
||||
if (m === 'sell') loadKrkBalance();
|
||||
}
|
||||
|
||||
function setMax() {
|
||||
sellAmount.value = formatUnits(krkBalance.value, 18);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (canSwap.value) loadKrkBalance();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
|
@ -33,16 +89,55 @@ const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
|||
color: #FFB347
|
||||
font-size: 14px
|
||||
|
||||
.swap-tabs
|
||||
display: flex
|
||||
gap: 8px
|
||||
|
||||
.swap-tab
|
||||
flex: 1
|
||||
padding: 8px 0
|
||||
border-radius: 8px
|
||||
border: 1px solid #3a3a3a
|
||||
background: #111111
|
||||
color: #a3a3a3
|
||||
font-size: 14px
|
||||
font-weight: 600
|
||||
cursor: pointer
|
||||
transition: all 0.2s
|
||||
|
||||
&.active
|
||||
background: #1e3a5f
|
||||
border-color: #60a5fa
|
||||
color: #ffffff
|
||||
|
||||
&:hover:not(.active)
|
||||
border-color: #60a5fa
|
||||
color: #ffffff
|
||||
|
||||
.swap-label
|
||||
display: block
|
||||
font-size: 13px
|
||||
color: #a3a3a3
|
||||
margin-bottom: 6px
|
||||
|
||||
.swap-label-row
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: baseline
|
||||
margin-bottom: 6px
|
||||
|
||||
.swap-balance
|
||||
font-size: 12px
|
||||
color: #a3a3a3
|
||||
|
||||
.swap-field
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.swap-input-row
|
||||
display: flex
|
||||
gap: 8px
|
||||
|
||||
.swap-input
|
||||
background: #111111
|
||||
border: 1px solid #3a3a3a
|
||||
|
|
@ -52,6 +147,8 @@ const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
|||
padding: 12px 16px
|
||||
outline: none
|
||||
transition: border-color 0.2s
|
||||
flex: 1
|
||||
min-width: 0
|
||||
|
||||
&:focus
|
||||
border-color: #60a5fa
|
||||
|
|
@ -60,6 +157,26 @@ const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
|||
opacity: 0.5
|
||||
cursor: not-allowed
|
||||
|
||||
.max-button
|
||||
padding: 0 16px
|
||||
border-radius: 8px
|
||||
border: 1px solid #3a3a3a
|
||||
background: #1a1a1a
|
||||
color: #60a5fa
|
||||
font-size: 13px
|
||||
font-weight: 600
|
||||
cursor: pointer
|
||||
transition: all 0.2s
|
||||
white-space: nowrap
|
||||
|
||||
&:hover:not(:disabled)
|
||||
border-color: #60a5fa
|
||||
background: #1e3a5f
|
||||
|
||||
&:disabled
|
||||
opacity: 0.5
|
||||
cursor: not-allowed
|
||||
|
||||
.swap-button
|
||||
display: flex
|
||||
align-items: center
|
||||
|
|
@ -85,6 +202,14 @@ const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
|||
cursor: not-allowed
|
||||
transform: none
|
||||
|
||||
&.sell-button
|
||||
background: linear-gradient(135deg, #f472b6 0%, #ec4899 100%)
|
||||
box-shadow: 0 4px 12px rgba(244, 114, 182, 0.3)
|
||||
|
||||
&:hover:not(:disabled)
|
||||
box-shadow: 0 6px 16px rgba(244, 114, 182, 0.4)
|
||||
background: linear-gradient(135deg, #ec4899 0%, #db2777 100%)
|
||||
|
||||
.swap-hint
|
||||
margin: 0
|
||||
font-size: 12px
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { computed, ref } from 'vue';
|
||||
import { useAccount } from '@wagmi/vue';
|
||||
import { readContract, writeContract } from '@wagmi/core';
|
||||
import { erc20Abi, maxUint256, parseEther, zeroAddress, type Abi } from 'viem';
|
||||
import { erc20Abi, maxUint256, parseEther, parseUnits, zeroAddress, type Abi } from 'viem';
|
||||
import { useToast } from 'vue-toastification';
|
||||
import { config as wagmiConfig } from '@/wagmi';
|
||||
import { getChain, DEFAULT_CHAIN_ID } from '@/config';
|
||||
|
|
@ -16,6 +16,13 @@ export const WETH_ABI = [
|
|||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'uint256', name: 'wad', type: 'uint256' }],
|
||||
name: 'withdraw',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
] as const satisfies Abi;
|
||||
|
||||
export const SWAP_ROUTER_ABI = [
|
||||
|
|
@ -97,6 +104,10 @@ export function useSwapKrk() {
|
|||
|
||||
const swapAmount = ref('0.1');
|
||||
const swapping = ref(false);
|
||||
const sellAmount = ref('');
|
||||
const selling = ref(false);
|
||||
const sellPhase = ref<'approving' | 'selling'>('approving');
|
||||
const krkBalance = ref<bigint>(0n);
|
||||
|
||||
const resolvedChainId = computed(() => chainId.value ?? DEFAULT_CHAIN_ID);
|
||||
const chainConfig = computed(() => getChain(resolvedChainId.value));
|
||||
|
|
@ -106,6 +117,23 @@ export function useSwapKrk() {
|
|||
Boolean(address.value && cheatConfig.value?.weth && cheatConfig.value?.swapRouter && chainConfig.value?.harb)
|
||||
);
|
||||
|
||||
async function loadKrkBalance() {
|
||||
if (!address.value || !chainConfig.value?.harb) return;
|
||||
try {
|
||||
const harb = ensureAddress(chainConfig.value.harb, 'KRAIKEN token');
|
||||
const caller = ensureAddress(address.value, 'Wallet address');
|
||||
krkBalance.value = await readContract(wagmiConfig, {
|
||||
abi: erc20Abi,
|
||||
address: harb,
|
||||
functionName: 'balanceOf',
|
||||
args: [caller],
|
||||
chainId: resolvedChainId.value,
|
||||
});
|
||||
} catch {
|
||||
krkBalance.value = 0n;
|
||||
}
|
||||
}
|
||||
|
||||
async function buyKrk() {
|
||||
if (!canSwap.value || swapping.value) return;
|
||||
try {
|
||||
|
|
@ -224,5 +252,87 @@ export function useSwapKrk() {
|
|||
}
|
||||
}
|
||||
|
||||
return { swapAmount, swapping, canSwap, buyKrk };
|
||||
async function sellKrk() {
|
||||
if (!canSwap.value || selling.value) return;
|
||||
try {
|
||||
selling.value = true;
|
||||
|
||||
const chain = chainConfig.value;
|
||||
const cheat = cheatConfig.value;
|
||||
|
||||
if (!chain?.harb) throw new Error('No Kraiken deployment configured for this chain');
|
||||
if (!cheat) throw new Error('Swap helpers are not configured for this chain');
|
||||
|
||||
const weth = ensureAddress(cheat.weth, 'WETH');
|
||||
const router = ensureAddress(cheat.swapRouter, 'Swap router');
|
||||
const harb = ensureAddress(chain.harb, 'KRAIKEN token');
|
||||
const caller = ensureAddress(address.value!, 'Wallet address');
|
||||
const currentChainId = resolvedChainId.value;
|
||||
|
||||
let amount: bigint;
|
||||
try {
|
||||
amount = parseUnits(sellAmount.value || '0', 18);
|
||||
} catch {
|
||||
throw new Error('Enter a valid KRK amount');
|
||||
}
|
||||
if (amount <= 0n) throw new Error('Amount must be greater than zero');
|
||||
|
||||
const balance = await readContract(wagmiConfig, {
|
||||
abi: erc20Abi,
|
||||
address: harb,
|
||||
functionName: 'balanceOf',
|
||||
args: [caller],
|
||||
chainId: currentChainId,
|
||||
});
|
||||
if (amount > balance) throw new Error('Insufficient KRK balance');
|
||||
|
||||
sellPhase.value = 'approving';
|
||||
const allowance = await readContract(wagmiConfig, {
|
||||
abi: erc20Abi,
|
||||
address: harb,
|
||||
functionName: 'allowance',
|
||||
args: [caller, router],
|
||||
chainId: currentChainId,
|
||||
});
|
||||
if (allowance < amount) {
|
||||
await writeContract(wagmiConfig, {
|
||||
abi: erc20Abi,
|
||||
address: harb,
|
||||
functionName: 'approve',
|
||||
args: [router, maxUint256],
|
||||
chainId: currentChainId,
|
||||
});
|
||||
}
|
||||
|
||||
sellPhase.value = 'selling';
|
||||
await writeContract(wagmiConfig, {
|
||||
abi: SWAP_ROUTER_ABI,
|
||||
address: router,
|
||||
functionName: 'exactInputSingle',
|
||||
args: [
|
||||
{
|
||||
tokenIn: harb,
|
||||
tokenOut: weth,
|
||||
fee: 10_000,
|
||||
recipient: caller,
|
||||
amountIn: amount,
|
||||
amountOutMinimum: 0n,
|
||||
sqrtPriceLimitX96: 0n,
|
||||
},
|
||||
],
|
||||
chainId: currentChainId,
|
||||
});
|
||||
|
||||
await loadBalance();
|
||||
await loadKrkBalance();
|
||||
|
||||
toast.success('Sell complete. WETH has been added to your wallet.');
|
||||
} catch (error: unknown) {
|
||||
toast.error(getErrorMessage(error, 'Sell failed'));
|
||||
} finally {
|
||||
selling.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { swapAmount, swapping, canSwap, buyKrk, sellAmount, selling, sellPhase, krkBalance, loadKrkBalance, sellKrk };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue