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
67 changes: 51 additions & 16 deletions ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import "./App.css";
import { cancelDemo, claimDemo, createPlanDemo, isDevnetRpc } from "./devnet-writer.js";
import { fetchBatches, fetchPlan, fetchStatus, makeProvider, planCommitment } from "./iceberg.js";
import { cancel as poolCancel, claim as poolClaim, createPlan as poolCreatePlan } from "./strk20.js";
import { detectPrivacyCapable } from "./wallet-account-v6.js";

const stored = (key, fallback) => localStorage.getItem(key) ?? fallback;

Expand Down Expand Up @@ -120,6 +122,30 @@ export default function App() {
const [actionResult, setActionResult] = useState("");
const demoMode = isDevnetRpc(rpcUrl);

// Pool mode's write buttons are gated on an actually-connected, STRK20-capable
// wallet rather than just "not demo mode" — same check strk20.js itself makes
// before submitting, so the button state never promises something a click
// would immediately fail on.
const [walletCapable, setWalletCapable] = useState(false);
useEffect(() => {
if (demoMode) {
setWalletCapable(false);
return;
}
let cancelled = false;
detectPrivacyCapable(provider)
.then((capable) => {
if (!cancelled) setWalletCapable(capable);
})
.catch(() => {
if (!cancelled) setWalletCapable(false);
});
return () => {
cancelled = true;
};
}, [provider, demoMode]);
const canAct = demoMode || walletCapable;

const runAction = async (label, action) => {
setBusy(true);
setActionResult(`${label}…`);
Expand All @@ -134,21 +160,28 @@ export default function App() {
}
};

const onCreate = () =>
runAction("create", () =>
createPlanDemo({
rpcUrl,
icebergAddress: address,
chunkAmount:
BigInt(Math.round(Number(chunkInput) * 100)) * 10n ** (BigInt(inDecimals) - 2n),
numChunks: Number(chunksInput),
secret,
}),
const onCreate = () => {
const chunkAmount =
BigInt(Math.round(Number(chunkInput) * 100)) * 10n ** (BigInt(inDecimals) - 2n);
const numChunks = Number(chunksInput);
return runAction("create", () =>
demoMode
? createPlanDemo({ rpcUrl, icebergAddress: address, chunkAmount, numChunks, secret })
: poolCreatePlan(provider, { icebergAddress: address, chunkAmount, numChunks, secret }),
);
};
const onClaim = () =>
runAction("claim", () => claimDemo({ rpcUrl, icebergAddress: address, secret }));
runAction("claim", () =>
demoMode
? claimDemo({ rpcUrl, icebergAddress: address, secret })
: poolClaim(provider, { icebergAddress: address, secret }),
);
const onCancel = () =>
runAction("cancel", () => cancelDemo({ rpcUrl, icebergAddress: address, secret }));
runAction("cancel", () =>
demoMode
? cancelDemo({ rpcUrl, icebergAddress: address, secret })
: poolCancel(provider, { icebergAddress: address, secret }),
);

const executedChunks = useCallback(
(currentPlan) => {
Expand Down Expand Up @@ -630,7 +663,7 @@ export default function App() {
</label>
<button
className="btn-accent"
disabled={!demoMode || busy || !secret}
disabled={!canAct || busy || !secret}
onClick={onCreate}
>
create plan
Expand All @@ -639,14 +672,14 @@ export default function App() {
<div className="act-buttons">
<button
className="btn-dark strong"
disabled={!demoMode || busy || !plan?.exists}
disabled={!canAct || busy || !plan?.exists}
onClick={onClaim}
>
claim accrued
</button>
<button
className="btn-dark"
disabled={!demoMode || busy || !plan?.exists}
disabled={!canAct || busy || !plan?.exists}
onClick={onCancel}
>
cancel &amp; refund
Expand All @@ -656,7 +689,9 @@ export default function App() {
{actionResult ||
(demoMode
? "Devnet demo mode — the prefunded account stands in for the STRK20 pool."
: "Pool mode: flows implemented via the Wallet API (strk20.js) — connect a STRK20-capable wallet (Ready) to use them. Not wired to these buttons yet.")}
: walletCapable
? "Pool mode: connected to a STRK20-capable wallet — these submit real mainnet transactions."
: "Pool mode: connect a STRK20-capable wallet (Ready, wallet API ≥ 0.10) to use these actions.")}
</span>
</div>

Expand Down
28 changes: 25 additions & 3 deletions ui/src/strk20.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ const planCommitment = (secret) =>
/// INVALID_REQUEST_PAYLOAD.
const toWalletFelt = (value) => `0x${BigInt(value).toString(16)}`;

const viewCall = async (provider, contractAddress, entrypoint) => {
const result = await provider.callContract({ contractAddress, entrypoint, calldata: [] });
return result[0];
};

/// in_token/out_token are immutable on Iceberg once deployed, so caching by
/// address is always safe — same convention as devnet-writer.js's viewCall.
const tokenCache = new Map();
async function resolveTokens(provider, icebergAddress) {
if (tokenCache.has(icebergAddress)) return tokenCache.get(icebergAddress);
const [inToken, outToken] = await Promise.all([
viewCall(provider, icebergAddress, "in_token"),
viewCall(provider, icebergAddress, "out_token"),
]);
const tokens = { inToken, outToken };
tokenCache.set(icebergAddress, tokens);
return tokens;
}

/// Connects to a STRK20-capable wallet, throwing a clear, user-facing error
/// if none is found or it doesn't support the wallet API — never a silent
/// fallback to a public flow.
Expand Down Expand Up @@ -75,7 +94,8 @@ export async function shield(provider, token, amount) {
/// Iceberg helper and invokes privacy_invoke(CreatePlan) in the same proof.
export async function createPlan(provider, params) {
const account = await connectPrivacyWallet(provider);
const { icebergAddress, inToken, chunkAmount, numChunks, secret } = params;
const { icebergAddress, chunkAmount, numChunks, secret } = params;
const { inToken } = await resolveTokens(provider, icebergAddress);
const total = chunkAmount * BigInt(numChunks);
const actions = [
{ type: "withdraw", token: inToken, amount: toWalletFelt(total), recipient: icebergAddress },
Expand All @@ -98,7 +118,8 @@ export async function createPlan(provider, params) {
/// note's id — the pool credits it with the plan's accrued OpenNoteDeposit.
export async function claim(provider, params) {
const account = await connectPrivacyWallet(provider);
const { icebergAddress, outToken, secret } = params;
const { icebergAddress, secret } = params;
const { outToken } = await resolveTokens(provider, icebergAddress);
const actions = [
{ type: "transfer", token: outToken, amount: "OPEN", recipient: account.address },
{
Expand All @@ -115,7 +136,8 @@ export async function claim(provider, params) {
/// note's id — the pool credits it with the unswapped refund.
export async function cancel(provider, params) {
const account = await connectPrivacyWallet(provider);
const { icebergAddress, inToken, secret } = params;
const { icebergAddress, secret } = params;
const { inToken } = await resolveTokens(provider, icebergAddress);
const actions = [
{ type: "transfer", token: inToken, amount: "OPEN", recipient: account.address },
{
Expand Down
Loading