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
86 changes: 60 additions & 26 deletions 00-LEGACY_EXAMPLES/anchor-counter/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ const COUNTER_PDA_SEED = "counter";
// Program ID comes from the local IDL so it stays in sync with declare_id! after redeploys.
const COUNTER_PROGRAM = new PublicKey(publicCounterIdl.address);
console.log("Counter program:", COUNTER_PROGRAM.toBase58());
// Default to a specific ER region (devnet-as) — see comment in non-legacy app for why
// not the router (devnet.magicblock.app): WS subscriptions stick to the picked ER but
// HTTP requests get routed per-call, so updates would never reach the UI.
// Default to a specific ER region (devnet-as) instead of the router (devnet.magicblock.app).
// The router proxies HTTP per-request but a WS subscription is bound to whichever ER it
// happened to pick at connect time — txs routed elsewhere wouldn't fire the WS callback,
// so the UI would stall. Override via REACT_APP_EPHEMERAL_PROVIDER_ENDPOINT for other regions.
const PUBLIC_ER_ENDPOINT = process.env.REACT_APP_EPHEMERAL_PROVIDER_ENDPOINT || "https://devnet-as.magicblock.app";

const App: React.FC = () => {
Expand All @@ -38,8 +39,8 @@ const App: React.FC = () => {
const [ephemeralCounter, setEphemeralCounter] = useState<number>(0);
const [isDelegated, setIsDelegated] = useState<boolean>(false);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [transactionError, setTransactionError] = useState<string | null>(null);
const [transactionSuccess, setTransactionSuccess] = useState<string | null>(null);
const [transactionError, setTransactionError] = useState<{ message: string; explorerUrl?: string } | null>(null);
const [transactionSuccess, setTransactionSuccess] = useState<{ message: string; explorerUrl?: string } | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const counterProgramClient = useRef<Program | null>(null);
const counterSubscriptionId = useRef<number | null>(null);
Expand Down Expand Up @@ -90,7 +91,9 @@ const App: React.FC = () => {
try { await ephemeralConnection.current.removeAccountChangeListener(ephemeralCounterSubscriptionId.current); } catch {}
}
console.log("Subscribing to ephemeral counter", counterPda.toBase58());
ephemeralCounterSubscriptionId.current = ephemeralConnection.current.onAccountChange(counterPda, handleEphemeralCounterChange, 'confirmed');
// Use 'processed' — ER doesn't reliably emit 'confirmed' WS notifications,
// and 'processed' fires at slot-time which is what we want for UI latency.
ephemeralCounterSubscriptionId.current = ephemeralConnection.current.onAccountChange(counterPda, handleEphemeralCounterChange, 'processed');
}, [counterPda, handleEphemeralCounterChange]);

// Init program client + base layer counter, then ER connection
Expand All @@ -110,9 +113,7 @@ const App: React.FC = () => {
setCounter(Number(c.count.valueOf()));
setIsDelegated(!accountInfo.owner.equals(COUNTER_PROGRAM));
}
// Subscribe unconditionally — even if the account doesn't exist yet, the
// listener will fire when init+increment creates it, so the UI updates
// without needing a page refresh.
// Subscribe unconditionally — listener fires when init+increment creates the account.
await subscribeToCounter();
setIsLoading(false);
}
Expand Down Expand Up @@ -180,9 +181,9 @@ const App: React.FC = () => {
useTempKeypair: boolean = false,
ephemeral: boolean = false,
confirmCommitment: Commitment = "processed",
// Optional: an account expected to change as a result of this tx. When provided,
// we refresh its state after confirmation so the UI updates immediately without
// waiting for the long-lived subscribe handler to fire.
// Optional: an account that's expected to change as a result of this tx.
// If provided, we resolve confirmation via onAccountChange (push, ~slot-time)
// instead of HTTP polling. Otherwise we fall back to getSignatureStatuses polling.
watchAccount?: PublicKey,
): Promise<string | null> => {
if (!tempKeypair.current) return null;
Expand All @@ -193,6 +194,7 @@ const App: React.FC = () => {
setTransactionSuccess(null);
const targetConnection = ephemeral ? ephemeralConnection.current : provider.current.connection;
const layerLabel = ephemeral ? "ER" : "Base";
let signature: string | null = null;
try {
const {
context: { slot: minContextSlot },
Expand All @@ -204,8 +206,6 @@ const App: React.FC = () => {
transaction.feePayer = useTempKeypair ? tempKeypair.current.publicKey : publicKey;
}
if (useTempKeypair) transaction.sign(tempKeypair.current);

let signature;
const sendStart = performance.now();
if (!ephemeral && !useTempKeypair) {
signature = await sendTransaction(transaction, targetConnection, { minContextSlot });
Expand All @@ -216,8 +216,8 @@ const App: React.FC = () => {

// Confirm via solana-web3.js's confirmTransaction (signature WS sub at the
// requested commitment). On ER at 'processed' this resolves at slot-time
// (~30-100ms). Known wart: ~500ms HTTP polling fallback when the WS sub
// setup is slow — rare in practice, simpler code wins.
// (~30-100ms). Known wart: ~500ms HTTP polling fallback when the WS sub setup
// is slow — rare in practice, simpler code wins.
const confirmStart = performance.now();
const sigConfirm = await targetConnection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
Expand All @@ -228,7 +228,9 @@ const App: React.FC = () => {
}
const confirmMs = Math.round(performance.now() - confirmStart);

// Refresh the watched account inline so the UI updates immediately.
// Refresh the watched account inline so the UI updates immediately without
// waiting for the long-lived subscribeToCounter/subscribeToEphemeralCounter
// handler to fire on its own.
let refreshMs = 0;
if (watchAccount && counterProgramClient.current) {
const refreshStart = performance.now();
Expand All @@ -251,10 +253,27 @@ const App: React.FC = () => {
console.log(
`[${layerLabel}] ${totalMs}ms total = send ${sendMs}ms + confirmTransaction(${confirmCommitment}) ${confirmMs}ms + refresh ${refreshMs}ms · sig ${signature}`,
);
setTransactionSuccess(`[${layerLabel}] confirmed in ${totalMs}ms`);

// Build an explorer URL so the success Alert links to tx details.
const explorerUrl = ephemeral
? `https://explorer.solana.com/tx/${signature}?cluster=custom&customUrl=${encodeURIComponent(PUBLIC_ER_ENDPOINT)}`
: `https://explorer.solana.com/tx/${signature}?cluster=devnet`;

setTransactionSuccess({
message: `[${layerLabel}] confirmed in ${totalMs}ms`,
explorerUrl,
});
return signature;
} catch (error) {
setTransactionError(`Transaction failed: ${error}`);
const explorerUrl = signature
? (ephemeral
? `https://explorer.solana.com/tx/${signature}?cluster=custom&customUrl=${encodeURIComponent(PUBLIC_ER_ENDPOINT)}`
: `https://explorer.solana.com/tx/${signature}?cluster=devnet`)
: undefined;
setTransactionError({
message: `Transaction failed: ${error}`,
explorerUrl,
});
} finally {
setIsSubmitting(false);
}
Expand All @@ -266,15 +285,17 @@ const App: React.FC = () => {
*/
const increaseCounterTx = useCallback(async () => {
if (!tempKeypair.current) return;
if (!counterProgramClient.current) {
console.error("counterProgramClient not initialized yet");
return;
}
if (!isDelegated) {
const accountTmpWallet = await connection.getAccountInfo(tempKeypair.current.publicKey);
if (!accountTmpWallet || accountTmpWallet.lamports <= 0.01 * LAMPORTS_PER_SOL) {
await transferToTempKeypair();
}
}

if (!counterProgramClient.current) return;

const transaction = await counterProgramClient.current.methods
.increment()
.accounts({
Expand Down Expand Up @@ -307,7 +328,10 @@ const App: React.FC = () => {
});
transaction.add(noopInstruction);

await submitTransaction(transaction, true, isDelegated);
// Base-layer txs use 'confirmed' (devnet has reorgs at 'processed'); ER uses
// 'processed' since it's a single sequencer and no slot can orphan.
const commitment = isDelegated ? "processed" : "confirmed";
await submitTransaction(transaction, true, isDelegated, commitment, counterPda);
}, [isDelegated, counterPda, submitTransaction, connection, transferToTempKeypair]);

const updateCounter = async (_: number): Promise<void> => {
Expand Down Expand Up @@ -348,7 +372,7 @@ const App: React.FC = () => {
.transaction() as Transaction;

setEphemeralCounter(Number(counter));
await submitTransaction(transaction, true, false, "confirmed");
await submitTransaction(transaction, true, false, "confirmed", counterPda);
}, [counterPda, connection, counter, submitTransaction, transferToTempKeypair]);

/**
Expand All @@ -366,7 +390,7 @@ const App: React.FC = () => {
})
.transaction() as Transaction;

await submitTransaction(transaction, true, true);
await submitTransaction(transaction, true, true, "processed", counterPda);
}, [counterPda, submitTransaction]);

const delegateTx = useCallback(async () => {
Expand Down Expand Up @@ -423,10 +447,20 @@ const App: React.FC = () => {
)}

{transactionError &&
<Alert type="error" message={transactionError} onClose={() => setTransactionError(null)}/>}
<Alert
type="error"
message={transactionError.message}
href={transactionError.explorerUrl}
onClose={() => setTransactionError(null)}
/>}

{transactionSuccess &&
<Alert type="success" message={transactionSuccess} onClose={() => setTransactionSuccess(null)}/>}
<Alert
type="success"
message={transactionSuccess.message}
href={transactionSuccess.explorerUrl}
onClose={() => setTransactionSuccess(null)}
/>}

<img src={`${process.env.PUBLIC_URL}/magicblock_white.png`} alt="Magic Block Logo"
className="magicblock-logo"/>
Expand Down
73 changes: 42 additions & 31 deletions 00-LEGACY_EXAMPLES/anchor-counter/app/src/components/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,65 @@ type AlertProps = {
type: 'success' | 'error';
message: string;
onClose: () => void;
href?: string;
};

const Alert: React.FC<AlertProps> = ({ type, message, onClose }) => {
const Alert: React.FC<AlertProps> = ({ type, message, onClose, href }) => {
const [opacity, setOpacity] = useState(0); // Start with 0 opacity for fade-in effect

useEffect(() => {
setOpacity(1);

// Linger longer when the notification is clickable so the user has time to click.
const visibleMs = href ? 6000 : 3000;
const fadeOutTimer = setTimeout(() => {
setOpacity(0);
}, 3000);
}, visibleMs);

const removeTimer = setTimeout(() => {
onClose();
}, 3500);
}, visibleMs + 500);

return () => {
clearTimeout(fadeOutTimer);
clearTimeout(removeTimer);
};
}, [onClose]);

return (
<div style={{
position: 'fixed',
bottom: '20px',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: type === 'success' ? 'lightgreen' : 'pink',
padding: '20px',
marginBottom: '10px',
borderRadius: '10px',
color: type === 'success' ? 'green' : 'red',
transition: 'opacity 1s ease-in-out',
opacity: opacity,
zIndex: 1000,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
maxWidth: '90%',
wordWrap: 'break-word',
wordBreak: 'break-word',
minHeight: '50px',
boxSizing: 'border-box',
}}>
{message}
</div>
);
}, [onClose, href]);

const color = type === 'success' ? 'green' : 'red';
const containerStyle: React.CSSProperties = {
position: 'fixed',
bottom: '20px',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: type === 'success' ? 'lightgreen' : 'pink',
padding: '20px',
marginBottom: '10px',
borderRadius: '10px',
color,
transition: 'opacity 1s ease-in-out',
opacity: opacity,
zIndex: 1000,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
maxWidth: '90%',
wordWrap: 'break-word',
wordBreak: 'break-word',
minHeight: '50px',
boxSizing: 'border-box',
textDecoration: 'none',
cursor: href ? 'pointer' : 'default',
};

if (href) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" style={containerStyle}>
{message} <span style={{ marginLeft: 4 }}>↗</span>
</a>
);
}
Comment on lines +58 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider adding aria-label for screen reader users.

The external link opens in a new tab but doesn't announce this to screen readers. Adding an aria-label would improve accessibility.

♿ Optional accessibility enhancement
 if (href) {
     return (
-        <a href={href} target="_blank" rel="noopener noreferrer" style={containerStyle}>
+        <a 
+            href={href} 
+            target="_blank" 
+            rel="noopener noreferrer" 
+            aria-label={`${message} (opens in new tab)`}
+            style={containerStyle}
+        >
             {message} <span style={{ marginLeft: 4 }}>↗</span>
         </a>
     );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (href) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" style={containerStyle}>
{message} <span style={{ marginLeft: 4 }}></span>
</a>
);
}
if (href) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={`${message} (opens in new tab)`}
style={containerStyle}
>
{message} <span style={{ marginLeft: 4 }}></span>
</a>
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@00-LEGACY_EXAMPLES/anchor-counter/app/src/components/Alert.tsx` around lines
58 - 64, The anchor rendered in the Alert component when href is present should
include an aria-label to inform screen reader users that the link opens in a new
tab; update the <a> element in the Alert component (the block handling href) to
add an aria-label that combines the visible message and a clear hint like
"(opens in a new tab)" (e.g., use aria-label={`${message} (opens in a new
tab)`}) so the link text and behavior are announced together.

return <div style={containerStyle}>{message}</div>;
};

export default Alert;
38 changes: 31 additions & 7 deletions 00-LEGACY_EXAMPLES/private-counter/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const App: React.FC = () => {
// gesture, and the resulting token is baked into the URL below.
const [explorerToken, setExplorerToken] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [transactionError, setTransactionError] = useState<string | null>(null);
const [transactionError, setTransactionError] = useState<{ message: string; explorerUrl?: string } | null>(null);
const [transactionSuccess, setTransactionSuccess] = useState<{ message: string; explorerUrl?: string } | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const counterProgramClient = useRef<Program | null>(null);
Expand Down Expand Up @@ -317,22 +317,25 @@ const App: React.FC = () => {
): Promise<string | null> => {
if (!tempKeypair) return null;
if (ephemeral && !ephemeralConnection.current) {
setTransactionError("TEE ER connection not ready — auth token missing");
setTransactionError({ message: "TEE ER connection not ready — auth token missing" });
return null;
}
setIsSubmitting(true);
setTransactionError(null);
setTransactionSuccess(null);
const targetConnection = ephemeral ? ephemeralConnection.current! : provider.current.connection;
const layerLabel = ephemeral ? "ER" : "Base";
// Hoist signature so the catch handler can include the explorer link for
// failed-on-chain txs (much easier to debug with a clickable link).
let signature: string | null = null;
try {
const { value: { blockhash, lastValidBlockHeight } } = await targetConnection.getLatestBlockhashAndContext();
if (!transaction.recentBlockhash) transaction.recentBlockhash = blockhash;
if (!transaction.feePayer) transaction.feePayer = tempKeypair.publicKey;
transaction.sign(tempKeypair);

const sendStart = performance.now();
const signature = await targetConnection.sendRawTransaction(transaction.serialize(), { skipPreflight: true });
signature = await targetConnection.sendRawTransaction(transaction.serialize(), { skipPreflight: true });
const sendMs = Math.round(performance.now() - sendStart);

const confirmStart = performance.now();
Expand Down Expand Up @@ -388,7 +391,23 @@ const App: React.FC = () => {
});
return signature;
} catch (error) {
setTransactionError(`Transaction failed: ${error}`);
// If the tx made it on-chain (signature was returned by sendRawTransaction),
// attach an explorer link so the user can inspect the failure logs.
let explorerUrl: string | undefined;
if (signature) {
if (ephemeral) {
const token = await ensureAuthToken();
if (token) {
explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=custom&customUrl=${encodeURIComponent(PRIVATE_ER_ENDPOINT + '?token=' + token)}`;
}
} else {
explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=devnet`;
}
}
setTransactionError({
message: `Transaction failed: ${error}`,
explorerUrl,
});
Comment on lines +394 to +410
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap ensureAuthToken() in try-catch to prevent masking the original error.

If ensureAuthToken() throws in the catch block, the original transaction error will be masked and the user won't see the failure message.

🛡️ Proposed fix to handle potential ensureAuthToken failure
         let explorerUrl: string | undefined;
         if (signature) {
             if (ephemeral) {
-                const token = await ensureAuthToken();
-                if (token) {
-                    explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=custom&customUrl=${encodeURIComponent(PRIVATE_ER_ENDPOINT + '?token=' + token)}`;
+                try {
+                    const token = await ensureAuthToken();
+                    if (token) {
+                        explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=custom&customUrl=${encodeURIComponent(PRIVATE_ER_ENDPOINT + '?token=' + token)}`;
+                    }
+                } catch (tokenErr) {
+                    console.error("Failed to get auth token for explorer URL:", tokenErr);
                 }
             } else {
                 explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=devnet`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If the tx made it on-chain (signature was returned by sendRawTransaction),
// attach an explorer link so the user can inspect the failure logs.
let explorerUrl: string | undefined;
if (signature) {
if (ephemeral) {
const token = await ensureAuthToken();
if (token) {
explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=custom&customUrl=${encodeURIComponent(PRIVATE_ER_ENDPOINT + '?token=' + token)}`;
}
} else {
explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=devnet`;
}
}
setTransactionError({
message: `Transaction failed: ${error}`,
explorerUrl,
});
// If the tx made it on-chain (signature was returned by sendRawTransaction),
// attach an explorer link so the user can inspect the failure logs.
let explorerUrl: string | undefined;
if (signature) {
if (ephemeral) {
try {
const token = await ensureAuthToken();
if (token) {
explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=custom&customUrl=${encodeURIComponent(PRIVATE_ER_ENDPOINT + '?token=' + token)}`;
}
} catch (tokenErr) {
console.error("Failed to get auth token for explorer URL:", tokenErr);
}
} else {
explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=devnet`;
}
}
setTransactionError({
message: `Transaction failed: ${error}`,
explorerUrl,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@00-LEGACY_EXAMPLES/private-counter/app/src/App.tsx` around lines 394 - 410,
The code that builds explorerUrl inside the error handler can mask the original
transaction error if ensureAuthToken() throws; wrap the call to
ensureAuthToken() in a try-catch inside the block that checks ephemeral and
signature (the same place where explorerUrl is assigned), so any error from
ensureAuthToken() is caught and does not overwrite or change the original
transaction error; if token retrieval fails, proceed without the token (i.e.,
leave explorerUrl undefined or build the URL without the token) and optionally
log the token retrieval error, then call setTransactionError({ message:
`Transaction failed: ${error}`, explorerUrl }) as before.

} finally {
setIsSubmitting(false);
}
Expand Down Expand Up @@ -485,7 +504,7 @@ const App: React.FC = () => {
const togglePrivacyTx = useCallback(async () => {
if (!tempKeypair || !counterPda || !permissionPda || !counterProgramClient.current) return;
if (!ephemeralConnection.current) {
setTransactionError("ER connection not ready");
setTransactionError({ message: "ER connection not ready" });
return;
}
const next = !isPrivate;
Expand Down Expand Up @@ -545,7 +564,7 @@ const App: React.FC = () => {
if (!token) {
const fresh = await ensureWalletAuthToken();
if (!fresh) {
setTransactionError("Wallet declined the auth token");
setTransactionError({ message: "Wallet declined the auth token" });
return;
}
token = fresh;
Expand Down Expand Up @@ -660,7 +679,12 @@ const App: React.FC = () => {
)}

{transactionError &&
<Alert type="error" message={transactionError} onClose={() => setTransactionError(null)}/>}
<Alert
type="error"
message={transactionError.message}
href={transactionError.explorerUrl}
onClose={() => setTransactionError(null)}
/>}

{transactionSuccess &&
<Alert
Expand Down
Loading
Loading