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,22 +4,78 @@
|
||||||
<p class="swap-warning">Connect your wallet to swap ETH for $KRK on the local Uniswap pool.</p>
|
<p class="swap-warning">Connect your wallet to swap ETH for $KRK on the local Uniswap pool.</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="swap-field">
|
<div class="swap-tabs">
|
||||||
<label for="local-swap-amount" class="swap-label">ETH to spend</label>
|
<button
|
||||||
<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" />
|
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>
|
</div>
|
||||||
<button class="swap-button" data-testid="swap-buy-button" :disabled="swapping" @click="buyKrk">
|
|
||||||
{{ swapping ? 'Submitting…' : 'Buy KRK' }}
|
<template v-if="mode === 'buy'">
|
||||||
</button>
|
<div class="swap-field">
|
||||||
<p class="swap-hint">Wraps ETH → WETH, approves the swap router, then trades into $KRK.</p>
|
<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" />
|
||||||
|
</div>
|
||||||
|
<button class="swap-button" data-testid="swap-buy-button" :disabled="swapping" @click="buyKrk">
|
||||||
|
{{ swapping ? 'Submitting…' : 'Buy KRK' }}
|
||||||
|
</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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useSwapKrk } from '@/composables/useSwapKrk';
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
|
|
@ -33,16 +89,55 @@ const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
||||||
color: #FFB347
|
color: #FFB347
|
||||||
font-size: 14px
|
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
|
.swap-label
|
||||||
display: block
|
display: block
|
||||||
font-size: 13px
|
font-size: 13px
|
||||||
color: #a3a3a3
|
color: #a3a3a3
|
||||||
margin-bottom: 6px
|
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
|
.swap-field
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
|
.swap-input-row
|
||||||
|
display: flex
|
||||||
|
gap: 8px
|
||||||
|
|
||||||
.swap-input
|
.swap-input
|
||||||
background: #111111
|
background: #111111
|
||||||
border: 1px solid #3a3a3a
|
border: 1px solid #3a3a3a
|
||||||
|
|
@ -52,6 +147,8 @@ const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
||||||
padding: 12px 16px
|
padding: 12px 16px
|
||||||
outline: none
|
outline: none
|
||||||
transition: border-color 0.2s
|
transition: border-color 0.2s
|
||||||
|
flex: 1
|
||||||
|
min-width: 0
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
border-color: #60a5fa
|
border-color: #60a5fa
|
||||||
|
|
@ -60,6 +157,26 @@ const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
||||||
opacity: 0.5
|
opacity: 0.5
|
||||||
cursor: not-allowed
|
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
|
.swap-button
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
|
|
@ -85,6 +202,14 @@ const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
||||||
cursor: not-allowed
|
cursor: not-allowed
|
||||||
transform: none
|
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
|
.swap-hint
|
||||||
margin: 0
|
margin: 0
|
||||||
font-size: 12px
|
font-size: 12px
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useAccount } from '@wagmi/vue';
|
import { useAccount } from '@wagmi/vue';
|
||||||
import { readContract, writeContract } from '@wagmi/core';
|
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 { useToast } from 'vue-toastification';
|
||||||
import { config as wagmiConfig } from '@/wagmi';
|
import { config as wagmiConfig } from '@/wagmi';
|
||||||
import { getChain, DEFAULT_CHAIN_ID } from '@/config';
|
import { getChain, DEFAULT_CHAIN_ID } from '@/config';
|
||||||
|
|
@ -16,6 +16,13 @@ export const WETH_ABI = [
|
||||||
stateMutability: 'payable',
|
stateMutability: 'payable',
|
||||||
type: 'function',
|
type: 'function',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
inputs: [{ internalType: 'uint256', name: 'wad', type: 'uint256' }],
|
||||||
|
name: 'withdraw',
|
||||||
|
outputs: [],
|
||||||
|
stateMutability: 'nonpayable',
|
||||||
|
type: 'function',
|
||||||
|
},
|
||||||
] as const satisfies Abi;
|
] as const satisfies Abi;
|
||||||
|
|
||||||
export const SWAP_ROUTER_ABI = [
|
export const SWAP_ROUTER_ABI = [
|
||||||
|
|
@ -97,6 +104,10 @@ export function useSwapKrk() {
|
||||||
|
|
||||||
const swapAmount = ref('0.1');
|
const swapAmount = ref('0.1');
|
||||||
const swapping = ref(false);
|
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 resolvedChainId = computed(() => chainId.value ?? DEFAULT_CHAIN_ID);
|
||||||
const chainConfig = computed(() => getChain(resolvedChainId.value));
|
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)
|
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() {
|
async function buyKrk() {
|
||||||
if (!canSwap.value || swapping.value) return;
|
if (!canSwap.value || swapping.value) return;
|
||||||
try {
|
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