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
4 changes: 4 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ https://stats.tangle.tools
# Something happened with conventional commits link, temporary disabled to fix the CI
https://www.conventionalcommits.org/en/v1.0.0/

# External sites that block bots or have intermittent Cloudflare errors
https://www.tangle.tools/
https://openai.com/index/harness-engineering/

# Files
/**/CHANGELOG.md

Expand Down
50 changes: 35 additions & 15 deletions apps/tangle-cloud/src/app/CreditsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useAccount } from 'wagmi';
Expand All @@ -25,6 +26,8 @@ interface CreditsContextValue {
generateAndStoreCreditKeys: (label?: string) => Promise<StoredCreditKeys>;
removeCreditAccount: (commitment: string) => Promise<void>;
isLoading: boolean;
/** True when keypair is unlocked and credit keys are decryptable */
isUnlocked: boolean;
}

const CreditsContext = createContext<CreditsContextValue | null>(null);
Expand All @@ -35,41 +38,51 @@ export const CreditsProvider: FC<PropsWithChildren> = ({ children }) => {
const [creditAccounts, setCreditAccounts] = useState<StoredCreditKeys[]>([]);
const [isLoading, setIsLoading] = useState(true);

// Generation counter to detect stale async loads
const genRef = useRef(0);

const encryptionKey = keypair
? keccak256(
encodePacked(['string'], [keypair.privateKey + ':credit-encryption']),
)
: undefined;

// Reload when address or encryption key changes
useEffect(() => {
setCreditAccounts([]);
setIsLoading(true);
const gen = ++genRef.current;

if (!address) {
setIsLoading(false);
return;
}

const currentAddress = address;
const load = async () => {
try {
const keys = await loadCreditKeysForAddress(
currentAddress,
encryptionKey,
);
setCreditAccounts(keys);
const keys = await loadCreditKeysForAddress(address, encryptionKey);
if (genRef.current === gen) {
setCreditAccounts(keys);
}
} catch {
setCreditAccounts([]);
if (genRef.current === gen) {
setCreditAccounts([]);
}
}
if (genRef.current === gen) {
setIsLoading(false);
}
setIsLoading(false);
};
load();
}, [address, encryptionKey]);

const generateAndStoreCreditKeys = useCallback(
async (label?: string): Promise<StoredCreditKeys> => {
if (!address) throw new Error('Wallet not connected');
if (!encryptionKey) {
throw new Error(
'Unlock your shielded keypair before generating credit keys',
);
}

const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
Expand All @@ -89,6 +102,7 @@ export const CreditsProvider: FC<PropsWithChildren> = ({ children }) => {
salt,
label,
createdAt: Date.now(),
isLocked: false,
};

await saveCreditKeys(keys, address, encryptionKey);
Expand All @@ -98,25 +112,31 @@ export const CreditsProvider: FC<PropsWithChildren> = ({ children }) => {
[address, encryptionKey],
);

const removeCreditAccount = useCallback(async (commitment: string) => {
await deleteCreditKeys(commitment);
setCreditAccounts((prev) =>
prev.filter((k) => k.commitment !== commitment),
);
}, []);
const removeCreditAccount = useCallback(
async (commitment: string) => {
if (!address) return;
await deleteCreditKeys(commitment, address);
setCreditAccounts((prev) =>
prev.filter((k) => k.commitment !== commitment),
);
},
[address],
);

const value = useMemo<CreditsContextValue>(
() => ({
creditAccounts,
generateAndStoreCreditKeys,
removeCreditAccount,
isLoading,
isUnlocked: !!encryptionKey,
}),
[
creditAccounts,
generateAndStoreCreditKeys,
removeCreditAccount,
isLoading,
encryptionKey,
],
);

Expand Down
4 changes: 3 additions & 1 deletion apps/tangle-cloud/src/app/ShieldedProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ export const ShieldedProvider: FC<PropsWithChildren> = ({ children }) => {
(mutateFn: (current: NoteData[]) => NoteData[]) => {
writeQueueRef.current = writeQueueRef.current.then(async () => {
const updated = mutateFn(notesRef.current);
// Update ref synchronously so the next queued write sees this result
notesRef.current = updated;
setNotes(updated);
if (storageRef.current) {
await storageRef.current.save(updated.map(serializeNote));
}
setNotes(updated);
});
return writeQueueRef.current;
},
Expand Down
2 changes: 1 addition & 1 deletion apps/tangle-cloud/src/components/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const PageLayout: FC<Props> = ({ children, className }) => {
return (
<div
className={twMerge(
'max-w-screen-xl px-4 mx-auto space-y-5 md:px-8 lg:px-10',
'max-w-screen-xl px-4 mx-auto space-y-5 md:px-5',
className,
)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const AmountInput: FC<Props> = ({
balance !== undefined ? formatUnits(balance, decimals) : undefined;

const handleMax = useCallback(() => {
if (formattedBalance) {
if (formattedBalance !== undefined) {
onChange(formattedBalance);
}
}, [formattedBalance, onChange]);
Expand Down
101 changes: 34 additions & 67 deletions apps/tangle-cloud/src/containers/payments/FundCreditsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,86 +6,61 @@ import {
Button,
Input,
} from '@tangle-network/ui-components';
import AmountInput from '../../components/payments/AmountInput';
import ProofProgressIndicator from '../../components/payments/ProofProgressIndicator';
import { useShieldedContext } from '../../app/ShieldedProvider';
import { useCreditsContext } from '../../app/CreditsProvider';
import { ProofStage, type ProofProgress } from '../../types/shielded';

const FundCreditsContainer: FC = () => {
const { address } = useAccount();
const { shieldedBalance, keypair, deriveKeypair } = useShieldedContext();
const { generateAndStoreCreditKeys, creditAccounts } = useCreditsContext();
const { hasDerivedKeypair } = useShieldedContext();
const { generateAndStoreCreditKeys, creditAccounts, isUnlocked } =
useCreditsContext();

const [amount, setAmount] = useState('');
const [label, setLabel] = useState('');
const [progress, setProgress] = useState<ProofProgress>({
stage: ProofStage.IDLE,
});
const [isGenerating, setIsGenerating] = useState(false);
const [fundedCommitment, setFundedCommitment] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

const handleFund = useCallback(async () => {
if (!address || !amount) return;
const handleGenerate = useCallback(async () => {
if (!address) return;

let kp = keypair;
if (!kp) {
kp = await deriveKeypair();
if (!kp) return;
}
setIsGenerating(true);
setError(null);
setFundedCommitment(null);

try {
setProgress({
stage: ProofStage.FETCHING_ARTIFACTS,
message: 'Generating ephemeral credit account keys...',
});

const creditKeys = await generateAndStoreCreditKeys(
label || `Account ${creditAccounts.length + 1}`,
);

setFundedCommitment(creditKeys.commitment);
setProgress({
stage: ProofStage.DONE,
message:
'Credit keys generated and stored locally. ' +
'On-chain funding requires @tangle-network/shielded-sdk integration.',
});
setAmount('');
setLabel('');
} catch (err) {
setProgress({
stage: ProofStage.ERROR,
message: err instanceof Error ? err.message : 'Fund credits failed',
});
setError(err instanceof Error ? err.message : 'Failed to generate keys');
} finally {
setIsGenerating(false);
}
}, [
address,
amount,
label,
keypair,
deriveKeypair,
generateAndStoreCreditKeys,
creditAccounts.length,
]);

const isProcessing =
progress.stage !== ProofStage.IDLE &&
progress.stage !== ProofStage.DONE &&
progress.stage !== ProofStage.ERROR;
}, [address, label, generateAndStoreCreditKeys, creditAccounts.length]);

return (
<div className="space-y-4">
<div>
<Typography variant="h5" fw="semibold">
Fund Credit Account
Create Credit Account
</Typography>

<Typography variant="body2" className="mt-1 text-mono-100">
Create an anonymous prepaid credit account funded from your shielded
pool balance. One ZK proof enables many cheap pay-per-job signatures.
Generate an ephemeral keypair for anonymous credit payments. The keys
are encrypted and stored in your browser. On-chain funding requires
SDK integration.
</Typography>
</div>

{!hasDerivedKeypair && (
<Alert
type="warning"
description="Unlock your shielded keypair before creating credit accounts. Credit keys are encrypted using your shielded key."
/>
)}

<div className="space-y-1">
<Typography variant="body2" fw="semibold" className="text-mono-120">
Account Label (optional)
Expand All @@ -96,23 +71,14 @@ const FundCreditsContainer: FC = () => {
value={label}
onChange={setLabel}
isControlled
isDisabled={isProcessing}
isDisabled={isGenerating || !isUnlocked}
/>
</div>

<AmountInput
value={amount}
onChange={setAmount}
balance={shieldedBalance}
symbol="SHIELDED"
label="Fund Amount"
disabled={isProcessing}
/>

<ProofProgressIndicator progress={progress} />
{error && <Alert type="error" description={error} />}

{fundedCommitment && (
<Alert type="success" title="Credit keys stored locally">
<Alert type="success" title="Credit keys generated and stored">
<Typography variant="mono2" className="mt-1 break-all">
{fundedCommitment}
</Typography>
Expand All @@ -121,12 +87,13 @@ const FundCreditsContainer: FC = () => {

<Button
isFullWidth
onClick={handleFund}
isDisabled={
!address || !amount || isProcessing || shieldedBalance === 0n
onClick={handleGenerate}
isDisabled={!address || !isUnlocked || isGenerating}
isLoading={isGenerating}
loadingText="Generating..."
disabledTooltip={
!isUnlocked ? 'Unlock your shielded keypair first' : undefined
}
isLoading={isProcessing}
loadingText="Processing..."
>
Generate Credit Keys
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ const SpendAuthContainer: FC = () => {
return;
}

if (!selectedAccount.spendingPrivateKey || selectedAccount.isLocked) {
setError(
'This credit account is locked. Unlock your shielded keypair to sign authorizations.',
);
return;
}

// Validate all numeric inputs
const parsedServiceId = validateServiceId(serviceId);
if (parsedServiceId === null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import NoteCard from '../../components/payments/NoteCard';
import { useShieldedContext } from '../../app/ShieldedProvider';

const WithdrawContainer: FC = () => {
const { notes, shieldedBalance } = useShieldedContext();
const { notes } = useShieldedContext();

const [amount, setAmount] = useState('');
const [recipient, setRecipient] = useState('');
Expand Down Expand Up @@ -68,7 +68,7 @@ const WithdrawContainer: FC = () => {
<AmountInput
value={amount}
onChange={setAmount}
balance={shieldedBalance}
balance={confirmedNotes.reduce((sum, n) => sum + n.amount, 0n)}
symbol="SHIELDED"
label="Withdraw Amount"
disabled
Expand Down
1 change: 1 addition & 0 deletions apps/tangle-cloud/src/hooks/payments/useKeypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const useKeypair = () => {
useEffect(() => {
setKeypair(null);
setError(null);
setIsLoading(false); // Cancel any stuck loading state from previous address
setHasStoredKeypair(
!!address && localStorage.getItem(`${STORAGE_KEY}:${address}`) !== null,
);
Expand Down
4 changes: 3 additions & 1 deletion apps/tangle-cloud/src/pages/blueprints/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { PropsWithChildren } from 'react';
import PageLayout from '../../components/PageLayout';

const Layout = ({ children }: PropsWithChildren) => {
return <PageLayout>{children}</PageLayout>;
return (
<PageLayout className="md:px-8 lg:px-10 relative">{children}</PageLayout>
);
};

export default Layout;
4 changes: 3 additions & 1 deletion apps/tangle-cloud/src/pages/operators/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { PropsWithChildren } from 'react';
import PageLayout from '../../components/PageLayout';

const Layout = ({ children }: PropsWithChildren) => {
return <PageLayout>{children}</PageLayout>;
return (
<PageLayout className="md:px-8 lg:px-10 relative">{children}</PageLayout>
);
};

export default Layout;
4 changes: 3 additions & 1 deletion apps/tangle-cloud/src/pages/operators/manage/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { PropsWithChildren } from 'react';
import PageLayout from '../../../components/PageLayout';

const Layout = ({ children }: PropsWithChildren) => {
return <PageLayout>{children}</PageLayout>;
return (
<PageLayout className="md:px-8 lg:px-10 relative">{children}</PageLayout>
);
};

export default Layout;
Loading
Loading