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
73 changes: 64 additions & 9 deletions src/components/settings/ConfigurationTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,70 @@ export function ConfigurationTab() {
</div>
)}

{/* Username */}
{(config.translator?.user_identity || config.jdc?.user_identity) && (
<div className="p-4 rounded-lg border border-border/50 bg-muted/20">
<p className="font-medium mb-1">{isSoloMode ? 'Bitcoin Address' : 'Pool Username'}</p>
<p className="font-mono text-sm truncate">
{config.translator?.user_identity || config.jdc?.user_identity}
</p>
</div>
)}
{/* Username / Identity */}
{(config.translator?.user_identity || config.jdc?.user_identity) && (() => {
const identity = config.translator?.user_identity || config.jdc?.user_identity || '';

if (isSoloMode && (identity.startsWith('sri/solo/') || identity.startsWith('sri/donate'))) {
let addr = '';
let worker = '';
let donation = '';

if (identity.startsWith('sri/solo/')) {
const rest = identity.slice('sri/solo/'.length);
const idx = rest.indexOf('/');
addr = idx === -1 ? rest : rest.slice(0, idx);
worker = idx === -1 ? '' : rest.slice(idx + 1);
donation = '0%';
} else if (identity === 'sri/donate') {
donation = '100%';
} else if (identity.startsWith('sri/donate/')) {
const rest = identity.slice('sri/donate/'.length);
const parts = rest.split('/');
const pct = parseInt(parts[0], 10);
if (!isNaN(pct) && String(pct) === parts[0] && parts.length >= 2) {
donation = `${pct}%`;
addr = parts[1];
worker = parts.slice(2).join('/');
} else {
donation = '100%';
worker = rest;
}
}

return (
<div className="p-4 rounded-lg border border-border/50 bg-muted/20 space-y-2">
{addr && (
<div>
<p className="font-medium mb-1">Payout Address</p>
<p className="font-mono text-sm truncate">{addr}</p>
</div>
)}
{worker && (
<div>
<p className="font-medium mb-1">Worker Name</p>
<p className="font-mono text-sm">{worker}</p>
</div>
)}
<div>
<p className="font-medium mb-1">Donation</p>
<p className="text-sm">{donation}</p>
</div>
<div>
<p className="text-xs text-muted-foreground mb-1">User Identity</p>
<p className="font-mono text-xs text-muted-foreground truncate">{identity}</p>
</div>
</div>
);
}

return (
<div className="p-4 rounded-lg border border-border/50 bg-muted/20">
<p className="font-medium mb-1">{isSoloMode ? 'Bitcoin Address' : 'Pool Username'}</p>
<p className="font-mono text-sm truncate">{identity}</p>
</div>
);
})()}

{/* Bitcoin Core (JD mode) */}
{isJdMode && config.bitcoin && (
Expand Down
229 changes: 193 additions & 36 deletions src/components/setup/steps/MiningIdentityStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,66 +3,223 @@ import { StepProps } from '../types';
import { Info } from 'lucide-react';
import { isValidBitcoinAddress, getBitcoinAddressError } from '@/lib/utils';

const SRI_POOL_AUTHORITY_KEY = '9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72';

interface SriIdentityParts {
address: string;
workerName: string;
donationPercent: number;
}

function parseSriIdentity(identity: string): SriIdentityParts {
if (identity.startsWith('sri/solo/')) {
const rest = identity.slice('sri/solo/'.length);
const idx = rest.indexOf('/');
if (idx === -1) return { address: rest, workerName: '', donationPercent: 0 };
return { address: rest.slice(0, idx), workerName: rest.slice(idx + 1), donationPercent: 0 };
}

if (identity === 'sri/donate') {
return { address: '', workerName: '', donationPercent: 100 };
}

if (identity.startsWith('sri/donate/')) {
const rest = identity.slice('sri/donate/'.length);
const parts = rest.split('/');
const pct = parseInt(parts[0], 10);
if (!isNaN(pct) && String(pct) === parts[0] && parts.length >= 2) {
return { address: parts[1], workerName: parts.slice(2).join('/'), donationPercent: pct };
}
return { address: '', workerName: rest, donationPercent: 100 };
}

return { address: identity, workerName: '', donationPercent: 0 };
}

function buildSriIdentity(address: string, workerName: string, donationPercent: number): string {
const addr = address.trim();
const worker = workerName.trim();

if (donationPercent >= 100) {
return worker ? `sri/donate/${worker}` : 'sri/donate';
}

if (donationPercent > 0 && donationPercent < 100) {
if (!addr) return '';
return worker
? `sri/donate/${donationPercent}/${addr}/${worker}`
: `sri/donate/${donationPercent}/${addr}`;
}

if (!addr) return '';
return worker ? `sri/solo/${addr}/${worker}` : `sri/solo/${addr}`;
}

export function MiningIdentityStep({ data, updateData, onNext }: StepProps) {
const isSoloMode = data.miningMode === 'solo';
const isJdMode = data.mode === 'jd';
const isSriPool = data.pool?.authority_public_key === SRI_POOL_AUTHORITY_KEY;
const useSriConventions = isSoloMode && isSriPool;

const [userIdentity, setUserIdentity] = useState(
data.translator?.user_identity || data.jdc?.user_identity || ''
);
const existingIdentity = data.translator?.user_identity || data.jdc?.user_identity || '';
const parsed = useSriConventions ? parseSriIdentity(existingIdentity) : { address: '', workerName: '', donationPercent: 0 };

const [payoutAddress, setPayoutAddress] = useState(useSriConventions ? parsed.address : '');
const [workerName, setWorkerName] = useState(useSriConventions ? parsed.workerName : '');
const [donationPercent, setDonationPercent] = useState(useSriConventions ? parsed.donationPercent : 0);

const [userIdentity, setUserIdentity] = useState(!useSriConventions ? existingIdentity : '');
const [coinbaseAddress, setCoinbaseAddress] = useState(data.jdc?.coinbase_reward_address || '');

const finalIdentity = useSriConventions
? buildSriIdentity(payoutAddress, workerName, donationPercent)
: userIdentity;

useEffect(() => {
updateData({
jdc: isJdMode
? { user_identity: userIdentity, coinbase_reward_address: coinbaseAddress, jdc_signature: data.jdc?.jdc_signature || '' }
? { user_identity: finalIdentity, coinbase_reward_address: coinbaseAddress, jdc_signature: data.jdc?.jdc_signature || '' }
: null,
translator: data.translator
? { ...data.translator, user_identity: userIdentity, enable_vardiff: true }
: { user_identity: userIdentity, enable_vardiff: true, aggregate_channels: false, min_hashrate: 0 },
? { ...data.translator, user_identity: finalIdentity, enable_vardiff: true }
: { user_identity: finalIdentity, enable_vardiff: true, aggregate_channels: false, min_hashrate: 0 },
});
}, [userIdentity, coinbaseAddress, isJdMode, data.jdc?.jdc_signature, data.translator, updateData]);
}, [finalIdentity, coinbaseAddress, isJdMode, data.jdc?.jdc_signature, data.translator, updateData]);

const network = data.bitcoin?.network ?? 'mainnet';
const needsAddress = donationPercent < 100;

const isValid =
userIdentity.length > 0 &&
(!isSoloMode || isValidBitcoinAddress(userIdentity, network)) &&
(!isJdMode || isValidBitcoinAddress(coinbaseAddress, network));
const isValid = useSriConventions
? (!needsAddress || (payoutAddress.trim().length > 0 && isValidBitcoinAddress(payoutAddress.trim(), network)))
: (userIdentity.length > 0 &&
(!isSoloMode || isValidBitcoinAddress(userIdentity, network)) &&
(!isJdMode || isValidBitcoinAddress(coinbaseAddress, network)));

return (
<div className="space-y-8">
<div className="text-center">
<h2 className="text-2xl md:text-3xl font-semibold tracking-tight mb-3">Mining Identity</h2>
<p className="text-lg text-muted-foreground">
{isSoloMode ? 'Configure your payout address' : 'Configure your pool credentials'}
{useSriConventions
? 'Configure your solo mining payout'
: isSoloMode
? 'Configure your mining identity'
: 'Configure your pool credentials'}
</p>
</div>

<div>
<label htmlFor="user-identity" className="block text-sm font-medium mb-2">
{isSoloMode ? 'Bitcoin Address' : 'Pool Username'} <span className="text-primary" aria-hidden="true">*</span>
<span className="sr-only">(required)</span>
</label>
<input
id="user-identity"
type="text"
value={userIdentity}
onChange={(e) => setUserIdentity(e.target.value)}
placeholder={isSoloMode ? 'bc1q...' : 'username.worker1'}
aria-required="true"
autoComplete="off"
className="w-full h-10 px-3 rounded-lg border border-input bg-background focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all font-mono text-sm"
/>
{isSoloMode && getBitcoinAddressError(userIdentity, network) && (
<p className="text-xs text-destructive mt-1">{getBitcoinAddressError(userIdentity, network)}</p>
)}
<p className="text-xs text-muted-foreground mt-2">
{isSoloMode
? 'Your Bitcoin address where you want to receive mining rewards'
: 'Your pool account username (e.g., username.workername)'}
</p>
</div>
{useSriConventions ? (
<>
{needsAddress && (
<div>
<label htmlFor="payout-address" className="block text-sm font-medium mb-2">
Bitcoin Payout Address <span className="text-primary" aria-hidden="true">*</span>
<span className="sr-only">(required)</span>
</label>
<input
id="payout-address"
type="text"
value={payoutAddress}
onChange={(e) => setPayoutAddress(e.target.value)}
placeholder="bc1q..."
aria-required="true"
autoComplete="off"
className="w-full h-10 px-3 rounded-lg border border-input bg-background focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all font-mono text-sm"
/>
{getBitcoinAddressError(payoutAddress, network) && (
<p className="text-xs text-destructive mt-1">{getBitcoinAddressError(payoutAddress, network)}</p>
)}
<p className="text-xs text-muted-foreground mt-2">
Your Bitcoin address where you want to receive mining rewards
</p>
</div>
)}

<div>
<label htmlFor="worker-name" className="block text-sm font-medium mb-2">
Worker Name <span className="text-muted-foreground text-xs font-normal">(optional)</span>
</label>
<input
id="worker-name"
type="text"
value={workerName}
onChange={(e) => setWorkerName(e.target.value)}
placeholder="worker1"
autoComplete="off"
className="w-full h-10 px-3 rounded-lg border border-input bg-background focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-2">
A name to identify this mining device
</p>
</div>

<div>
<label htmlFor="donation-slider" className="block text-sm font-medium mb-2">
Donation to SRI Development <span className="text-muted-foreground text-xs font-normal">(optional)</span>
</label>
<div className="p-4 rounded-xl bg-muted/40 space-y-3">
<input
id="donation-slider"
type="range"
min={0}
max={100}
value={donationPercent}
onChange={(e) => setDonationPercent(Number(e.target.value))}
aria-label={`Donation: ${donationPercent}%`}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={donationPercent}
className="w-full accent-primary"
/>
<div className="flex justify-between text-xs text-muted-foreground select-none">
<span>0%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span>
</div>
</div>
<p className="text-xs text-muted-foreground mt-2">
{donationPercent === 0
? 'Full block reward goes to your payout address'
: donationPercent >= 100
? 'Full block reward is donated to SRI development'
: `${donationPercent}% of the block reward goes to SRI development, ${100 - donationPercent}% to your address`}
</p>
</div>

{finalIdentity && (
<div className="p-3 rounded-xl bg-muted/40 flex gap-3" role="note">
<Info className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-0.5" aria-hidden="true" />
<div className="text-sm">
<p className="text-muted-foreground mb-1">User identity that will be sent to the pool:</p>
<p className="font-mono text-xs text-foreground break-all">{finalIdentity}</p>
</div>
</div>
)}
</>
) : (
<div>
<label htmlFor="user-identity" className="block text-sm font-medium mb-2">
{isSoloMode ? 'Bitcoin Address' : 'Pool Username'} <span className="text-primary" aria-hidden="true">*</span>
<span className="sr-only">(required)</span>
</label>
<input
id="user-identity"
type="text"
value={userIdentity}
onChange={(e) => setUserIdentity(e.target.value)}
placeholder={isSoloMode ? 'bc1q...' : 'username.worker1'}
aria-required="true"
autoComplete="off"
className="w-full h-10 px-3 rounded-lg border border-input bg-background focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all font-mono text-sm"
/>
{isSoloMode && getBitcoinAddressError(userIdentity, network) && (
<p className="text-xs text-destructive mt-1">{getBitcoinAddressError(userIdentity, network)}</p>
)}
<p className="text-xs text-muted-foreground mt-2">
{isSoloMode
? 'Your Bitcoin address where you want to receive mining rewards'
: 'Your pool account username (e.g., username.workername)'}
</p>
</div>
)}

{isJdMode && (
<div>
Expand Down
44 changes: 43 additions & 1 deletion src/components/setup/steps/ReviewStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,49 @@ export function ReviewStart({ data, onComplete }: ReviewStartProps) {
<div className="p-5 rounded-b-xl border-x border-b border-border bg-card">
<SectionLabel n={isSoloMode ? '3' : (isJdMode ? '5' : '4')} label="Mining Identity" />
<div className="text-sm text-muted-foreground space-y-1 pl-7">
<div className="font-mono text-xs">{data.translator?.user_identity ?? data.jdc?.user_identity ?? '—'}</div>
{(() => {
const identity = data.translator?.user_identity ?? data.jdc?.user_identity ?? '';
if (!identity) return <div className="font-mono text-xs">—</div>;

if (isSoloMode && (identity.startsWith('sri/solo/') || identity.startsWith('sri/donate'))) {
let addr = '';
let worker = '';
let donation = '';

if (identity.startsWith('sri/solo/')) {
const rest = identity.slice('sri/solo/'.length);
const idx = rest.indexOf('/');
addr = idx === -1 ? rest : rest.slice(0, idx);
worker = idx === -1 ? '' : rest.slice(idx + 1);
donation = '0%';
} else if (identity === 'sri/donate') {
donation = '100%';
} else if (identity.startsWith('sri/donate/')) {
const rest = identity.slice('sri/donate/'.length);
const parts = rest.split('/');
const pct = parseInt(parts[0], 10);
if (!isNaN(pct) && String(pct) === parts[0] && parts.length >= 2) {
donation = `${pct}%`;
addr = parts[1];
worker = parts.slice(2).join('/');
} else {
donation = '100%';
worker = rest;
}
}

return (
<>
{addr && <div><span className="text-muted-foreground text-xs">Payout Address:</span> <span className="font-mono text-xs text-foreground">{addr}</span></div>}
{worker && <div><span className="text-muted-foreground text-xs">Worker:</span> <span className="font-mono text-xs text-foreground">{worker}</span></div>}
<div><span className="text-muted-foreground text-xs">Donation:</span> <span className="text-xs text-foreground">{donation}</span></div>
<div className="font-mono text-xs text-muted-foreground/70 break-all">{identity}</div>
</>
);
}

return <div className="font-mono text-xs">{identity}</div>;
})()}
{isJdMode && data.jdc?.coinbase_reward_address && (
<div className="font-mono text-xs text-muted-foreground/70">{data.jdc.coinbase_reward_address}</div>
)}
Expand Down
Loading