Skip to content
Open
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
48 changes: 48 additions & 0 deletions examples/harkan-abs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
\# Harkan ABS — Abstract Testnet Game



Cyberpunk mini‑game inspired by Harkan, built for Abstract Testnet.



\## Demo

https://abs-game001.vercel.app/



\## Contract (Abstract Testnet)

0x51F2C923a5307E2701F228DcC4cB3D72B1aAb804



\## Features

\- AGW login + sponsored tx option

\- On‑chain score submission + leaderboard

\- Static frontend (no build step)



\## Run locally

python -m http.server 5173

\# then open http://localhost:5173



\## Usage

\- Switch to Abstract Testnet

\- Connect wallet

\- Play a run

\- Submit on‑chain score

213 changes: 213 additions & 0 deletions examples/harkan-abs/agw-panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import React, { useEffect, useMemo, useState } from "https://esm.sh/react@18.2.0";
import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
import {
AbstractWalletProvider,
useLoginWithAbstract,
useWriteContractSponsored,
} from "https://esm.sh/@abstract-foundation/agw-react@0.6.0";
import { useAccount, useWriteContract } from "https://esm.sh/wagmi@2.12.5";
import {
isAddress,
parseAbi,
} from "https://esm.sh/viem@2.21.25";
import { getGeneralPaymasterInput } from "https://esm.sh/viem@2.21.25/zksync";
import { abstractTestnet } from "https://esm.sh/viem@2.21.25/chains";

const STORAGE_KEYS = {
contract: "harkanContractAddress",
paymaster: "harkanPaymasterAddress",
};

const CONTRACT_ABI = parseAbi([
"function submitScore(uint256 score,uint256 duration,uint256 hits,uint256 misses)",
]);

function getStoredValue(key) {
if (typeof localStorage === "undefined") return "";
return localStorage.getItem(key) || "";
}

function setStoredValue(key, value) {
if (typeof localStorage === "undefined") return;
if (!value) {
localStorage.removeItem(key);
return;
}
localStorage.setItem(key, value);
}

function useLatestScore() {
const [latestScore, setLatestScore] = useState(
typeof window !== "undefined" ? window.harkanLatestScore || null : null
);

useEffect(() => {
const handler = (event) => {
setLatestScore(event.detail);
};
window.addEventListener("harkan:runComplete", handler);
return () => window.removeEventListener("harkan:runComplete", handler);
}, []);

return latestScore;
}

function AgwPanel() {
const { login, logout } = useLoginWithAbstract();
const { address, status } = useAccount();
const { writeContractAsync, isPending: isPendingDirect } = useWriteContract();
const { writeContractSponsoredAsync, isPending: isPendingSponsored } =
useWriteContractSponsored();
const latestScore = useLatestScore();

const [contractAddress, setContractAddress] = useState(
getStoredValue(STORAGE_KEYS.contract)
);
const [paymasterAddress, setPaymasterAddress] = useState(
getStoredValue(STORAGE_KEYS.paymaster)
);
const [statusMsg, setStatusMsg] = useState("Waiting for run...");
const [lastHash, setLastHash] = useState("");

const isConnected = status === "connected";
const scoreSummary = useMemo(() => {
if (!latestScore) return "No run data yet";
return `${latestScore.score} pts • ${latestScore.hits} hits • ${latestScore.accuracy}%`;
}, [latestScore]);

const handleContractChange = (event) => {
const value = event.target.value.trim();
setContractAddress(value);
setStoredValue(STORAGE_KEYS.contract, value);
};

const handlePaymasterChange = (event) => {
const value = event.target.value.trim();
setPaymasterAddress(value);
setStoredValue(STORAGE_KEYS.paymaster, value);
};

async function submitScore() {
if (!latestScore) {
setStatusMsg("Play a run first");
return;
}
if (!isAddress(contractAddress)) {
setStatusMsg("Set a valid contract address");
return;
}
if (!isConnected) {
setStatusMsg("Connect AGW first");
return;
}
try {
setStatusMsg("Submitting...");
const args = [
BigInt(latestScore.score),
BigInt(latestScore.duration),
BigInt(latestScore.hits),
BigInt(latestScore.misses),
];

let hash = "";
if (paymasterAddress) {
if (!isAddress(paymasterAddress)) {
setStatusMsg("Invalid paymaster address");
return;
}
hash = await writeContractSponsoredAsync({
abi: CONTRACT_ABI,
address: contractAddress,
functionName: "submitScore",
args,
paymaster: paymasterAddress,
paymasterInput: getGeneralPaymasterInput({ innerInput: "0x" }),
});
} else {
hash = await writeContractAsync({
abi: CONTRACT_ABI,
address: contractAddress,
functionName: "submitScore",
args,
});
}

setLastHash(hash);
setStatusMsg("Score submitted");
} catch (error) {
setStatusMsg("Submission failed");
}
}

const isSubmitting = isPendingDirect || isPendingSponsored;

return (
<div className="agw-card">
<div className="agw-status">
Status: {isConnected ? "Connected" : "Disconnected"}
</div>
<div className="agw-status">
Address: {address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "—"}
</div>
<div className="agw-status">Latest run: {scoreSummary}</div>
<label className="agw-field">
Contract Address
<input
className="input"
value={contractAddress}
onChange={handleContractChange}
placeholder="0x..."
/>
</label>
<label className="agw-field">
Paymaster (optional)
<input
className="input"
value={paymasterAddress}
onChange={handlePaymasterChange}
placeholder="0x..."
/>
</label>
<div className="agw-actions">
{!isConnected ? (
<button className="btn" onClick={login}>
Connect AGW
</button>
) : (
<button className="btn ghost" onClick={logout}>
Disconnect
</button>
)}
<button className="btn" onClick={submitScore} disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit Score"}
</button>
</div>
<div className="agw-status">{statusMsg}</div>
{lastHash ? (
<div className="agw-status">
Tx:{" "}
<a
href={`https://sepolia.abscan.org/tx/${lastHash}`}
target="_blank"
rel="noopener"
>
{lastHash.slice(0, 10)}...
</a>
</div>
) : null}
</div>
);
}

function Root() {
return (
<AbstractWalletProvider chain={abstractTestnet}>
<AgwPanel />
</AbstractWalletProvider>
);
}

const mountNode = document.getElementById("agwMount");
if (mountNode) {
createRoot(mountNode).render(<Root />);
}
Loading