Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions apps/web/src/components/dashboard/ReaperActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import {
} from '../../lib/format.ts';
import type { NetworkConfig } from '../../lib/networks.ts';
import type { ProtocolSnapshot } from '../../lib/protocol.ts';
import {
REAPER_ALLOWANCE_CONFIRMED,
REAPER_ALLOWANCE_PENDING,
REAPER_SALE_CONFIRMED,
REAPER_SALE_PENDING,
reaperSellStepCopy,
} from '../../lib/reaperSell.ts';
import { DEFAULT_SLIPPAGE_BPS, minOutFromQuote } from '../../lib/slippage.ts';
import { accentButtonClass } from '../wallet/ui.tsx';
import { ActionFeedback, AmountPercents, SlippageControl, TokenChip } from './ui.tsx';
Expand Down Expand Up @@ -64,6 +71,11 @@ export function ReaperActions({
const minEthOut = sellQuote != null ? minOutFromQuote(sellQuote, slippageBps) : 0n;
const showNeth = connected && onChain && nethBalance != null;
const needsApprove = nethAllowance != null && amount > 0n && nethAllowance < amount;
const justApproved = lastAction === 'approve' && !needsApprove && amount > 0n;
const sellCopy =
amount > 0n && nethAllowance != null
? reaperSellStepCopy(needsApprove, amount, justApproved)
: null;
const canSell =
connected &&
onChain &&
Expand Down Expand Up @@ -151,10 +163,20 @@ export function ReaperActions({
)}
{mobile && slippageOpen ? <SlippageControl bps={slippageBps} onChange={setSlippageBps} /> : null}
{mobile ? null : <SlippageControl bps={slippageBps} onChange={setSlippageBps} />}
{sellCopy ? (
<div id={`reaper-sell-steps-${layout}`} className="mt-4 space-y-1 text-xs leading-relaxed">
<p className={needsApprove ? 'text-paper' : 'text-muted'}>{sellCopy.stepOne}</p>
<p className={!needsApprove ? 'text-paper' : 'text-muted'}>{sellCopy.stepTwo}</p>
</div>
) : null}
<button
type="button"
disabled={!canSell}
className={accentButtonClass(!canSell, 'mt-4')}
className={accentButtonClass(
!canSell,
[sellCopy ? 'mt-3' : 'mt-4', needsApprove ? 'tracking-[0.12em]' : ''].join(' '),
)}
aria-describedby={sellCopy ? `reaper-sell-steps-${layout}` : undefined}
onClick={() => {
if (needsApprove) {
void onApprove(amount);
Expand All @@ -163,13 +185,8 @@ export function ReaperActions({
}
}}
>
{needsApprove ? 'Approve NETH' : 'Sell NETH'}
{sellCopy?.button ?? 'Sell NETH'}
</button>
{needsApprove ? (
<p className="mt-2 text-xs text-muted">
Exact NETH allowance for this sale. Approve is a separate transaction.
</p>
) : null}
{mobile ? (
<p className="mt-3 text-[0.72rem] leading-relaxed text-muted">
Waiting may improve the rate, but others can consume the budget. Unused NETH stays with
Expand All @@ -181,6 +198,20 @@ export function ReaperActions({
error={reaperFeedback ? error : null}
hash={reaperFeedback ? hash : null}
network={network}
pendingLabel={
pending === 'approve'
? REAPER_ALLOWANCE_PENDING
: pending === 'sell'
? REAPER_SALE_PENDING
: undefined
}
confirmedLabel={
lastAction === 'approve'
? REAPER_ALLOWANCE_CONFIRMED
: lastAction === 'sell'
? REAPER_SALE_CONFIRMED
: undefined
}
/>
</div>
);
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/components/dashboard/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,21 @@ export function ActionFeedback({
error,
hash,
network,
pendingLabel,
confirmedLabel,
}: {
pending: string | null;
error: string | null;
hash: string | null;
network: NetworkConfig;
pendingLabel?: string;
confirmedLabel?: string;
}) {
if (error) {
return <p className="mt-2 text-sm text-accent">{error}</p>;
}
if (pending) {
return <p className="mt-2 text-sm text-muted">Confirm in wallet…</p>;
return <p className="mt-2 text-sm text-muted">{pendingLabel ?? 'Confirm in wallet…'}</p>;
}
if (hash) {
return (
Expand All @@ -246,7 +250,7 @@ export function ActionFeedback({
rel="noopener noreferrer"
target="_blank"
>
Transaction confirmed
{confirmedLabel ?? 'Transaction confirmed'}
<ExternalLink className="h-3.5 w-3.5 text-accent" strokeWidth={1.5} />
</a>
</p>
Expand Down
27 changes: 27 additions & 0 deletions apps/web/src/lib/reaperSell.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
REAPER_ALLOWANCE_CONFIRMED,
formatSellNeth,
reaperSellStepCopy,
} from './reaperSell.ts';
import { WAD } from './format.ts';

describe('reaperSellStepCopy', () => {
it('asks for an exact Reaper allowance before any sale', () => {
const copy = reaperSellStepCopy(true, 50n * WAD, false);
assert.equal(formatSellNeth(50n * WAD), '50 $NETH');
assert.equal(copy.button, 'Allow Reaper to use $NETH');
assert.match(copy.stepOne, /Allow the Reaper to use 50 \$NETH from your account/);
assert.match(copy.stepOne, /This is not the sale/);
assert.match(copy.stepTwo, /you can sell 50 \$NETH/);
});

it('tells the seller to confirm the sale after allowance', () => {
const copy = reaperSellStepCopy(false, 12_500n * 10n ** 14n, true);
assert.equal(copy.button, 'Sell NETH');
assert.match(copy.stepOne, /The Reaper can now use 1\.25 \$NETH from your account/);
assert.match(copy.stepTwo, /Confirm Sell NETH to burn it and receive ETH/);
assert.equal(REAPER_ALLOWANCE_CONFIRMED, 'Allowance confirmed. This did not sell.');
});
});
28 changes: 28 additions & 0 deletions apps/web/src/lib/reaperSell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { formatWei } from './format.ts';

export const REAPER_ALLOWANCE_PENDING = 'Confirm allowance in wallet…';
export const REAPER_ALLOWANCE_CONFIRMED = 'Allowance confirmed. This did not sell.';
export const REAPER_SALE_PENDING = 'Confirm sale in wallet…';
export const REAPER_SALE_CONFIRMED = 'Sale confirmed';

export function formatSellNeth(amount: bigint): string {
return `${formatWei(amount, 4)} $NETH`;
}

export function reaperSellStepCopy(needsApprove: boolean, amount: bigint, justApproved: boolean) {
const neth = formatSellNeth(amount);
if (needsApprove) {
return {
button: 'Allow Reaper to use $NETH',
stepOne: `1. Allow the Reaper to use ${neth} from your account. This is not the sale.`,
stepTwo: `2. After that, you can sell ${neth}.`,
};
}
return {
button: 'Sell NETH',
stepOne: justApproved
? `1. The Reaper can now use ${neth} from your account.`
: `1. The Reaper can use ${neth} from your account.`,
stepTwo: `2. Confirm Sell NETH to burn it and receive ETH.`,
};
}
7 changes: 5 additions & 2 deletions docs/nip/0011-wallet-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,10 @@ Unlock **SELL NETH** when: connected, correct chain, auction active and not expi

Partial fills are protocol behavior (spec §8.5): unused NETH stays with the seller. The quote already reflects remaining auction ETH. Show estimated ETH out and the spec warning that waiting may improve the rate but others can consume the budget.

Two-step approve + sell is expected. Do not hide the approve transaction.
Two-step approve + sell is expected. Do not hide the approve transaction. The Reaper pane must label the steps separately so the first wallet confirm is not mistaken for a sale:

1. While `allowance < nethIn`, the primary control is **Allow Reaper to use $NETH**, with copy that this lets the Reaper use the exact sell amount and is not the sale.
2. After that allowance confirms, copy must say the Reaper can use that amount and the user still needs to confirm **Sell NETH**. Do not show a generic “Transaction confirmed” after approve as if the sale completed.

### 7.5 Start auction (and finalize)

Expand Down Expand Up @@ -396,7 +399,7 @@ This slice is done when:
- Other EIP-6963 wallets appear without a code change; **Other wallet** is the catch-all when a project ID is configured
- ETH and $NETH balances show for the connected account on the correct chain ($NETH on the account chip and $NETH bar; ETH optional on the chip)
- **BURY ETH** sends `bury(minNethOut)` with the spec warning visible before the wallet prompt
- **SELL NETH** approves exact NETH then `sellToReaper`; inactive/expired auctions cannot sell
- **SELL NETH** is two labeled steps (allow Reaper to use exact $NETH, then sell); inactive/expired auctions cannot sell
- When idle Reaper ETH exists, a connected user can **start** an auction; when an auction is expired they can **finalize** first
- On Base Sepolia, the account menu offers **Add Base Sepolia** when the chain is not already selected / added on this origin; wallets that implement EIP-3085 get a native prompt; others get RPC URLs, chain id `84532` / `0x14a34`, explorer, and currency
- The account menu offers **Add $NETH** for the selected network’s token address until the user has pressed it on this origin; native `wallet_watchAsset` (including `image: nethMarkUrl()`) on supported wallets; manual contract / symbol / decimals guide otherwise
Expand Down
Loading