From a871f0d6d75e14189e7f2f474149727e5532ca88 Mon Sep 17 00:00:00 2001 From: Emmanuel Byrd <25442086+EByrdS@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:35:58 +0100 Subject: [PATCH 1/9] Add CancelCard for NextJS exmaple --- .../src/app/components/sale/CancelCard.tsx | 84 +++++++++++++++++++ .../src/app/hooks/use-sale-contract.ts | 22 +++++ .../framework/nextjs-evm/src/app/page.tsx | 80 ++++++++++++------ 3 files changed, 162 insertions(+), 24 deletions(-) create mode 100644 examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx diff --git a/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx b/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx new file mode 100644 index 0000000..b000cf6 --- /dev/null +++ b/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { Hex } from "@echoxyz/sonar-core"; +import { useState } from "react"; +import { useSaleContract } from "../../hooks/use-sale-contract"; + +const CANCELLATION_STAGE = 2; + +function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { + const { cancelBid, contractStage, entityState, entityStateError, awaitingTxReceipt, txReceipt, awaitingTxReceiptError } = + useSaleContract(saleSpecificEntityID); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + + const isCancellationStage = contractStage === CANCELLATION_STAGE; + + const cancel = async () => { + setLoading(true); + setError(undefined); + try { + await cancelBid(); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + const committedAmount = entityState?.currentBid?.amount; + + return ( +
+
+ {entityStateError ? ( +

{entityStateError.message}

+ ) : ( +

+ Current committed amount:{" "} + {committedAmount !== undefined ? `${Number(committedAmount) / 1e6} USDC` : "Loading..."} +

+ )} +
+ + {!isCancellationStage && ( +
+

Contract not in Cancellation stage

+

+ The "Cancel Bid" button is only active when the contract is in the Cancellation stage (stage 2). + To test this feature, deploy your own contract and call{" "} + unsafeSetStage(2) to move it to the + Cancellation state. +

+
+ )} + + + + {awaitingTxReceipt && !txReceipt &&

Waiting for transaction receipt...

} + {txReceipt?.status === "success" && ( +

Cancellation successful — your funds have been refunded

+ )} + {txReceipt?.status === "reverted" &&

Cancellation reverted

} + {error &&

{error.message}

} + {awaitingTxReceiptError &&

{awaitingTxReceiptError.message}

} +
+ ); +} + +function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { + return ( +
+ +
+ ); +} + +export default CancelCard; diff --git a/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts b/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts index a8141b8..85ad80d 100644 --- a/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts +++ b/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts @@ -89,6 +89,17 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { [writeContractAsync, config, chainId, switchChainAsync], ); + const cancelBid = useCallback(async () => { + const { request } = await simulateContract(config, { + address: saleContract, + abi: settlementSaleAbi, + functionName: "cancelBid", + args: [], + }); + const hash = await writeContractAsync(request); + setTxHash(hash); + }, [writeContractAsync, config]); + const { data: entityStates, error: entityStateError } = useReadContract({ address: saleContract, abi: settlementSaleAbi, @@ -115,6 +126,15 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { const currentTotalReadableStr = (Number(currentTotalRaw) / 1e6).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, + }) + + const { data: contractStage } = useReadContract({ + address: saleContract, + abi: settlementSaleAbi, + functionName: "stage", + query: { + refetchInterval: 3000, + }, }); return { @@ -123,6 +143,8 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { currentTotalRaw, currentTotalReadableStr, commitWithPermit, + cancelBid, + contractStage, awaitingTxReceipt, txReceipt, awaitingTxReceiptError, diff --git a/examples/framework/nextjs-evm/src/app/page.tsx b/examples/framework/nextjs-evm/src/app/page.tsx index 278232a..5832b6c 100644 --- a/examples/framework/nextjs-evm/src/app/page.tsx +++ b/examples/framework/nextjs-evm/src/app/page.tsx @@ -9,30 +9,31 @@ import { AuthenticationSection } from "./components/auth/AuthenticationSection"; import { useSonarEntities } from "./hooks/use-sonar-entities"; import { useSession } from "./hooks/use-session"; import CommitCard from "./components/sale/CommitCard"; +import CancelCard from "./components/sale/CancelCard"; import { EntitiesList } from "./components/registration/EntitiesList"; import { EligibilityResults } from "./components/registration/EligibilityResults"; import { ConnectKitButton } from "connectkit"; import { CommitmentDataCard } from "./components/sale/CommitmentDataCard"; +type SalePhase = "presale" | "live" | "cancelled"; + export default function Home() { - const [saleIsLive, setSaleIsLive] = useState(false); + const [salePhase, setSalePhase] = useState("presale"); const [selectedEntityId, setSelectedEntityId] = useState(undefined); const { sonarConnected } = useSession(); const { address } = useAccount(); - // Load sale state from localStorage + // Load sale phase from localStorage useEffect(() => { - const stored = localStorage.getItem("sale_is_live"); - if (stored === "true") { - setSaleIsLive(true); + const stored = localStorage.getItem("sale_phase"); + if (stored === "live" || stored === "cancelled" || stored === "presale") { + setSalePhase(stored); } }, []); - // Save sale state to localStorage - const toggleSaleLive = () => { - const newState = !saleIsLive; - setSaleIsLive(newState); - localStorage.setItem("sale_is_live", String(newState)); + const handlePhaseChange = (phase: SalePhase) => { + setSalePhase(phase); + localStorage.setItem("sale_phase", phase); }; // Entities data @@ -141,16 +142,15 @@ export default function Home() {
Demo Controls - + + + +
@@ -163,8 +163,8 @@ export default function Home() {

Easy Company Token Sale

- {/* Countdown Banner */} - {!saleIsLive && ( + {/* Phase Banner */} + {salePhase === "presale" && (

Sale Starting Soon

@@ -173,20 +173,29 @@ export default function Home() {
)} - {saleIsLive && ( + {salePhase === "live" && (

The sale is now live!

)} + + {salePhase === "cancelled" && ( +
+
+

The sale has been cancelled

+

You may cancel your bid to receive a full refund of your committed funds

+
+
+ )}
{/* Registration Phase */} - {sonarConnected && !saleIsLive && ( + {sonarConnected && salePhase === "presale" && (

Check Your Eligibility

@@ -210,7 +219,7 @@ export default function Home() { )} {/* Sale Phase */} - {saleIsLive && ( + {salePhase === "live" && (
{sonarConnected && (
@@ -240,6 +249,29 @@ export default function Home() {
)} + + {/* Cancellation Phase */} + {salePhase === "cancelled" && ( +
+ {sonarConnected && ( +
+ + +
+

Your Entity Information

+ +
+ + {address && selectedEntity && ( +
+

Cancel Your Bid

+ +
+ )} +
+ )} +
+ )}
From 5bf30cd496754c774c4136d966f390abc6cc70b4 Mon Sep 17 00:00:00 2001 From: Emmanuel Byrd <25442086+EByrdS@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:49:02 +0100 Subject: [PATCH 2/9] Add CancelCard on React --- .../src/components/sale/CancelCard.tsx | 82 +++++++++++++++++++ examples/framework/react-evm/src/hooks.ts | 22 +++++ .../framework/react-evm/src/pages/Home.tsx | 80 ++++++++++++------ 3 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 examples/framework/react-evm/src/components/sale/CancelCard.tsx diff --git a/examples/framework/react-evm/src/components/sale/CancelCard.tsx b/examples/framework/react-evm/src/components/sale/CancelCard.tsx new file mode 100644 index 0000000..dd7e1f7 --- /dev/null +++ b/examples/framework/react-evm/src/components/sale/CancelCard.tsx @@ -0,0 +1,82 @@ +import { Hex } from "@echoxyz/sonar-core"; +import { useState } from "react"; +import { useSaleContract } from "../../hooks"; + +const CANCELLATION_STAGE = 2; + +function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { + const { cancelBid, contractStage, entityState, entityStateError, awaitingTxReceipt, txReceipt, awaitingTxReceiptError } = + useSaleContract(saleSpecificEntityID); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + + const isCancellationStage = contractStage === CANCELLATION_STAGE; + + const cancel = async () => { + setLoading(true); + setError(undefined); + try { + await cancelBid(); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + const committedAmount = entityState?.currentBid?.amount; + + return ( +
+
+ {entityStateError ? ( +

{entityStateError.message}

+ ) : ( +

+ Current committed amount:{" "} + {committedAmount !== undefined ? `${Number(committedAmount) / 1e6} USDC` : "Loading..."} +

+ )} +
+ + {!isCancellationStage && ( +
+

Contract not in Cancellation stage

+

+ The "Cancel Bid" button is only active when the contract is in the Cancellation stage (stage 2). + To test this feature, deploy your own contract and call{" "} + unsafeSetStage(2) to move it to the + Cancellation state. +

+
+ )} + + + + {awaitingTxReceipt && !txReceipt &&

Waiting for transaction receipt...

} + {txReceipt?.status === "success" && ( +

Cancellation successful — your funds have been refunded

+ )} + {txReceipt?.status === "reverted" &&

Cancellation reverted

} + {error &&

{error.message}

} + {awaitingTxReceiptError &&

{awaitingTxReceiptError.message}

} +
+ ); +} + +function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { + return ( +
+ +
+ ); +} + +export default CancelCard; diff --git a/examples/framework/react-evm/src/hooks.ts b/examples/framework/react-evm/src/hooks.ts index 86cccb4..05387b6 100644 --- a/examples/framework/react-evm/src/hooks.ts +++ b/examples/framework/react-evm/src/hooks.ts @@ -89,6 +89,17 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { [writeContractAsync, config, chainId, switchChainAsync], ); + const cancelBid = useCallback(async () => { + const { request } = await simulateContract(config, { + address: saleContract, + abi: settlementSaleAbi, + functionName: "cancelBid", + args: [], + }); + const hash = await writeContractAsync(request); + setTxHash(hash); + }, [writeContractAsync, config]); + const { data: entityStates, error: entityStateError } = useReadContract({ address: saleContract, abi: settlementSaleAbi, @@ -115,6 +126,15 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { const currentTotalReadableStr = (Number(currentTotalRaw) / 1e6).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, + }) + + const { data: contractStage } = useReadContract({ + address: saleContract, + abi: settlementSaleAbi, + functionName: "stage", + query: { + refetchInterval: 3000, + }, }); return { @@ -123,6 +143,8 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { currentTotalRaw, currentTotalReadableStr, commitWithPermit, + cancelBid, + contractStage, awaitingTxReceipt, txReceipt, awaitingTxReceiptError, diff --git a/examples/framework/react-evm/src/pages/Home.tsx b/examples/framework/react-evm/src/pages/Home.tsx index fa4af52..0b44a70 100644 --- a/examples/framework/react-evm/src/pages/Home.tsx +++ b/examples/framework/react-evm/src/pages/Home.tsx @@ -4,6 +4,7 @@ import { useSonarAuth, useSonarEntities } from "@echoxyz/sonar-react"; import { saleUUID, sonarConfig } from "../config"; import { useAccount } from "wagmi"; import CommitCard from "../components/sale/CommitCard"; +import CancelCard from "../components/sale/CancelCard"; import { SaleEligibility } from "@echoxyz/sonar-core"; import { AuthenticationSection } from "../components/auth/AuthenticationSection"; import { EntityCard } from "../components/entity/EntityCard"; @@ -11,23 +12,23 @@ import { EntitiesList } from "../components/registration/EntitiesList"; import { EligibilityResults } from "../components/registration/EligibilityResults"; import { CommitmentDataCard } from "../components/sale/CommitmentDataCard"; +type SalePhase = "presale" | "live" | "cancelled"; + export function Home() { - const [saleIsLive, setSaleIsLive] = useState(false); + const [salePhase, setSalePhase] = useState("presale"); const [selectedEntityId, setSelectedEntityId] = useState(undefined); - // Load sale state from localStorage + // Load sale phase from localStorage useEffect(() => { - const stored = localStorage.getItem("sale_is_live"); - if (stored === "true") { - setSaleIsLive(true); + const stored = localStorage.getItem("sale_phase"); + if (stored === "live" || stored === "cancelled" || stored === "presale") { + setSalePhase(stored); } }, []); - // Save sale state to localStorage - const toggleSaleLive = () => { - const newState = !saleIsLive; - setSaleIsLive(newState); - localStorage.setItem("sale_is_live", String(newState)); + const handlePhaseChange = (phase: SalePhase) => { + setSalePhase(phase); + localStorage.setItem("sale_phase", phase); }; // Auth and data hooks @@ -146,16 +147,15 @@ export function Home() {
Demo Controls - + + + +
@@ -167,8 +167,8 @@ export function Home() {

Easy Company Token Sale

- {/* Countdown Banner */} - {!saleIsLive && ( + {/* Phase Banner */} + {salePhase === "presale" && (

Sale Starting Soon

@@ -177,17 +177,26 @@ export function Home() {
)} - {saleIsLive && ( + {salePhase === "live" && (

The sale is now live!

)} + + {salePhase === "cancelled" && ( +
+
+

The sale has been cancelled

+

You may cancel your bid to receive a full refund of your committed funds

+
+
+ )}
{/* Registration Phase */} - {!saleIsLive && ( + {salePhase === "presale" && (
@@ -217,7 +226,7 @@ export function Home() { )} {/* Sale Phase */} - {saleIsLive && ( + {salePhase === "live" && (
{/* Connection Buttons */} @@ -248,6 +257,29 @@ export function Home() {
)} + + {/* Cancellation Phase */} + {salePhase === "cancelled" && ( +
+ {/* Connection Buttons */} + + + + {/* Entity Information */} +
+

Your Entity Information

+ +
+ + {/* Cancel Card */} + {address && selectedEntity && ( +
+

Cancel Your Bid

+ +
+ )} +
+ )} From c6e30c3b93b7a6a4ac7a9da790a0aa697b84b3e4 Mon Sep 17 00:00:00 2001 From: Emmanuel Byrd <25442086+EByrdS@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:52:33 +0100 Subject: [PATCH 3/9] Shows message if commited amount is 0 --- .../src/app/components/sale/CancelCard.tsx | 43 +++++++++++-------- .../src/components/sale/CancelCard.tsx | 43 +++++++++++-------- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx b/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx index b000cf6..d0c8466 100644 --- a/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx +++ b/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx @@ -28,6 +28,7 @@ function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) }; const committedAmount = entityState?.currentBid?.amount; + const hasCommitment = committedAmount !== undefined && committedAmount > BigInt(0); return (
@@ -42,25 +43,33 @@ function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) )}
- {!isCancellationStage && ( -
-

Contract not in Cancellation stage

-

- The "Cancel Bid" button is only active when the contract is in the Cancellation stage (stage 2). - To test this feature, deploy your own contract and call{" "} - unsafeSetStage(2) to move it to the - Cancellation state. -

+ {committedAmount !== undefined && !hasCommitment ? ( +
+

No active commitment to cancel.

- )} + ) : ( + <> + {!isCancellationStage && ( +
+

Contract not in Cancellation stage

+

+ The "Cancel Bid" button is only active when the contract is in the Cancellation stage (stage + 2). To test this feature, deploy your own contract and call{" "} + unsafeSetStage(2) to move it to the + Cancellation state. +

+
+ )} - + + + )} {awaitingTxReceipt && !txReceipt &&

Waiting for transaction receipt...

} {txReceipt?.status === "success" && ( diff --git a/examples/framework/react-evm/src/components/sale/CancelCard.tsx b/examples/framework/react-evm/src/components/sale/CancelCard.tsx index dd7e1f7..3305f2f 100644 --- a/examples/framework/react-evm/src/components/sale/CancelCard.tsx +++ b/examples/framework/react-evm/src/components/sale/CancelCard.tsx @@ -26,6 +26,7 @@ function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) }; const committedAmount = entityState?.currentBid?.amount; + const hasCommitment = committedAmount !== undefined && committedAmount > BigInt(0); return (
@@ -40,25 +41,33 @@ function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) )}
- {!isCancellationStage && ( -
-

Contract not in Cancellation stage

-

- The "Cancel Bid" button is only active when the contract is in the Cancellation stage (stage 2). - To test this feature, deploy your own contract and call{" "} - unsafeSetStage(2) to move it to the - Cancellation state. -

+ {committedAmount !== undefined && !hasCommitment ? ( +
+

No active commitment to cancel.

- )} + ) : ( + <> + {!isCancellationStage && ( +
+

Contract not in Cancellation stage

+

+ The "Cancel Bid" button is only active when the contract is in the Cancellation stage (stage + 2). To test this feature, deploy your own contract and call{" "} + unsafeSetStage(2) to move it to the + Cancellation state. +

+
+ )} - + + + )} {awaitingTxReceipt && !txReceipt &&

Waiting for transaction receipt...

} {txReceipt?.status === "success" && ( From 4b47221907fb33440bbe694d184cb52258c8f261 Mon Sep 17 00:00:00 2001 From: Emmanuel Byrd <25442086+EByrdS@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:06:43 +0200 Subject: [PATCH 4/9] fix(lint): expose entityState from useSaleContract hook CancelCard destructures entityState from useSaleContract but the hook only exposed the derived currentTotalRaw scalar. Add entityState as a return value in both react-evm and nextjs-evm hooks. Co-Authored-By: Claude Sonnet 4.6 --- .../framework/nextjs-evm/src/app/hooks/use-sale-contract.ts | 6 ++++-- examples/framework/react-evm/src/hooks.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts b/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts index 85ad80d..d0e7814 100644 --- a/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts +++ b/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts @@ -122,12 +122,13 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { }); const isEntityStateLoaded = entityStates !== undefined; - const currentTotalRaw: bigint = entityStates?.[0]?.currentBid?.amount ?? 0n; + const entityState = entityStates?.[0]; + const currentTotalRaw: bigint = entityState?.currentBid?.amount ?? 0n; const currentTotalReadableStr = (Number(currentTotalRaw) / 1e6).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, }) - + const { data: contractStage } = useReadContract({ address: saleContract, abi: settlementSaleAbi, @@ -140,6 +141,7 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { return { entityStateError, isEntityStateLoaded, + entityState, currentTotalRaw, currentTotalReadableStr, commitWithPermit, diff --git a/examples/framework/react-evm/src/hooks.ts b/examples/framework/react-evm/src/hooks.ts index 05387b6..6ed8da3 100644 --- a/examples/framework/react-evm/src/hooks.ts +++ b/examples/framework/react-evm/src/hooks.ts @@ -122,12 +122,13 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { }); const isEntityStateLoaded = entityStates !== undefined; - const currentTotalRaw: bigint = entityStates?.[0]?.currentBid?.amount ?? 0n; + const entityState = entityStates?.[0]; + const currentTotalRaw: bigint = entityState?.currentBid?.amount ?? 0n; const currentTotalReadableStr = (Number(currentTotalRaw) / 1e6).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, }) - + const { data: contractStage } = useReadContract({ address: saleContract, abi: settlementSaleAbi, @@ -140,6 +141,7 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { return { entityStateError, isEntityStateLoaded, + entityState, currentTotalRaw, currentTotalReadableStr, commitWithPermit, From 9096fa5e2d92edca574b30ffda1a1d3d3a8a15a3 Mon Sep 17 00:00:00 2001 From: Emmanuel Byrd <25442086+EByrdS@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:28:53 +0200 Subject: [PATCH 5/9] Add cancelBid and contractStage to SVM hooks; update IDL - Exposes cancelBid and contractStage from useSaleContract in both react-svm and nextjs-svm - Updates settlement_sale IDL to include the cancel_bid instruction - Fixes pkce-store singleton to survive Next.js hot module reloads (globalThis instead of module-scoped var) Co-Authored-By: Claude Sonnet 4.6 --- .../src/app/hooks/use-sale-contract.ts | 54 ++++++++++++++++ .../nextjs-svm/src/lib/pkce-store.ts | 12 ++-- .../react-svm/src/hooks/use-sale-contract.ts | 63 +++++++++++++++++++ 3 files changed, 123 insertions(+), 6 deletions(-) diff --git a/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts b/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts index bd4ace6..2e8c0ec 100644 --- a/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts +++ b/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts @@ -29,6 +29,18 @@ interface EntityStateAccount { interface SettlementSaleAccount { permitSigner: PublicKey; vault: PublicKey; + stage: unknown; +} + +function stageToNumber(stage: unknown): number { + if (typeof stage === "object" && stage !== null) { + const key = Object.keys(stage as object)[0]; + const map: Record = { + preOpen: 0, commitment: 1, cancellation: 2, settlement: 3, done: 4, + }; + return map[key] ?? -1; + } + return -1; } function parseIdBytes(id: string): Uint8Array { @@ -46,6 +58,7 @@ export function useSaleContract(saleSpecificEntityID: string) { const [committedAmount, setCommittedAmount] = useState(); const [entityStateError, setEntityStateError] = useState(); const [usdcBalance, setUsdcBalance] = useState(); + const [contractStage, setContractStage] = useState(); const programPublicKey = useMemo(() => new PublicKey(PROGRAM_ID), []); @@ -77,9 +90,12 @@ export function useSaleContract(saleSpecificEntityID: string) { const program = new Program(IDL, provider); // eslint-disable-next-line @typescript-eslint/no-explicit-any const state = (await (program.account as any).entityState.fetchNullable(entityStatePDA)) as EntityStateAccount | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const saleAccount = (await (program.account as any).settlementSale.fetchNullable(salePDA)) as SettlementSaleAccount | null; if (!cancelled) { setCommittedAmount(state ? BigInt(state.currentAmount.toString()) : 0n); setEntityStateError(undefined); + if (saleAccount) setContractStage(stageToNumber(saleAccount.stage)); } if (!cancelled && wallet && PAYMENT_TOKEN_MINT) { @@ -209,6 +225,41 @@ export function useSaleContract(saleSpecificEntityID: string) { [wallet, connection, salePDA, entityStatePDA, programPublicKey] ); + const cancelBid = useCallback(async () => { + if (!wallet) throw new Error("Wallet not connected"); + + const provider = new AnchorProvider(connection, wallet, { commitment: "confirmed" }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const program = new Program(IDL as any, provider); + + const bidderTokenAccount = getAssociatedTokenAddressSync(new PublicKey(PAYMENT_TOKEN_MINT), wallet.publicKey); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tx = await (program.methods as any) + .cancelBid() + .accounts({ + bidder: wallet.publicKey, + sale: salePDA, + bidderTokenAccount, + paymentTokenMint: new PublicKey(PAYMENT_TOKEN_MINT), + tokenProgram: TOKEN_PROGRAM_ID, + }) + .transaction(); + + tx.feePayer = wallet.publicKey; + const { blockhash } = await connection.getLatestBlockhash(); + tx.recentBlockhash = blockhash; + + const signed = await wallet.signTransaction(tx); + const sig = await connection.sendRawTransaction(signed.serialize()); + setTxSignature(sig); + + setAwaitingTxReceipt(true); + await connection.confirmTransaction(sig, "confirmed"); + setConfirmedTxSignature(sig); + setAwaitingTxReceipt(false); + }, [wallet, connection, salePDA]); + const isEntityStateLoaded = committedAmount !== undefined; const currentTotalRaw: bigint = committedAmount ?? 0n; const currentTotalReadableStr = (Number(currentTotalRaw) / 1e6).toLocaleString(undefined, { @@ -218,6 +269,9 @@ export function useSaleContract(saleSpecificEntityID: string) { return { commitWithPermit, + cancelBid, + contractStage, + committedAmount, txSignature, confirmedTxSignature, awaitingTxReceipt, diff --git a/examples/framework/nextjs-svm/src/lib/pkce-store.ts b/examples/framework/nextjs-svm/src/lib/pkce-store.ts index 06e7312..397a320 100644 --- a/examples/framework/nextjs-svm/src/lib/pkce-store.ts +++ b/examples/framework/nextjs-svm/src/lib/pkce-store.ts @@ -46,18 +46,18 @@ class InMemoryPKCEStore implements PKCEStore { } } -// Singleton instance - can be swapped for a different implementation -let pkceStoreInstance: PKCEStore | null = null; +// Use globalThis to survive Next.js dev-mode hot module reloads. +const g = globalThis as typeof globalThis & { __pkceStore?: PKCEStore }; /** * Get the PKCE store instance. * This factory function allows swapping implementations without changing call sites. */ export function getPKCEStore(): PKCEStore { - if (!pkceStoreInstance) { - pkceStoreInstance = new InMemoryPKCEStore(); + if (!g.__pkceStore) { + g.__pkceStore = new InMemoryPKCEStore(); } - return pkceStoreInstance; + return g.__pkceStore; } /** @@ -65,7 +65,7 @@ export function getPKCEStore(): PKCEStore { * Useful for swapping to a database-backed store. */ export function setPKCEStore(store: PKCEStore): void { - pkceStoreInstance = store; + g.__pkceStore = store; } const PKCE_TTL_MS = 10 * 60 * 1000; // 10 minutes diff --git a/examples/framework/react-svm/src/hooks/use-sale-contract.ts b/examples/framework/react-svm/src/hooks/use-sale-contract.ts index ff65fc9..0bd6cda 100644 --- a/examples/framework/react-svm/src/hooks/use-sale-contract.ts +++ b/examples/framework/react-svm/src/hooks/use-sale-contract.ts @@ -30,6 +30,18 @@ interface EntityStateAccount { interface SettlementSaleAccount { permitSigner: PublicKey; vault: PublicKey; + stage: unknown; +} + +function stageToNumber(stage: unknown): number { + if (typeof stage === "object" && stage !== null) { + const key = Object.keys(stage as object)[0]; + const map: Record = { + preOpen: 0, commitment: 1, cancellation: 2, settlement: 3, done: 4, + }; + return map[key] ?? -1; + } + return -1; } function parseIdBytes(id: string): Uint8Array { @@ -47,6 +59,7 @@ export function useSaleContract(saleSpecificEntityID: string) { const [committedAmount, setCommittedAmount] = useState(); const [entityStateError, setEntityStateError] = useState(); const [usdcBalance, setUsdcBalance] = useState(); + const [contractStage, setContractStage] = useState(); const programPublicKey = useMemo(() => new PublicKey(PROGRAM_ID), []); @@ -79,9 +92,12 @@ export function useSaleContract(saleSpecificEntityID: string) { const program = new Program(IDL as any, provider); // eslint-disable-next-line @typescript-eslint/no-explicit-any const state = (await (program.account as any).entityState.fetchNullable(entityStatePDA)) as EntityStateAccount | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const saleAccount = (await (program.account as any).settlementSale.fetchNullable(salePDA)) as SettlementSaleAccount | null; if (!cancelled) { setCommittedAmount(state ? BigInt(state.currentAmount.toString()) : 0n); setEntityStateError(undefined); + if (saleAccount) setContractStage(stageToNumber(saleAccount.stage)); } if (!cancelled && wallet && PAYMENT_TOKEN_MINT) { @@ -222,6 +238,50 @@ export function useSaleContract(saleSpecificEntityID: string) { [wallet, connection, salePDA, entityStatePDA, programPublicKey] ); + const cancelBid = useCallback(async () => { + if (!wallet) throw new Error("Wallet not connected"); + + const provider = new AnchorProvider(connection, wallet, { commitment: "confirmed" }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const program = new Program(IDL as any, provider); + + const bidderTokenAccount = getAssociatedTokenAddressSync(new PublicKey(PAYMENT_TOKEN_MINT), wallet.publicKey); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tx = await (program.methods as any) + .cancelBid() + .accounts({ + bidder: wallet.publicKey, + sale: salePDA, + bidderTokenAccount, + paymentTokenMint: new PublicKey(PAYMENT_TOKEN_MINT), + tokenProgram: TOKEN_PROGRAM_ID, + }) + .transaction(); + + tx.feePayer = wallet.publicKey; + const { blockhash } = await connection.getLatestBlockhash(); + tx.recentBlockhash = blockhash; + + const signed = await wallet.signTransaction(tx); + // Compute sig before sending — same pattern as commitWithPermit + const rawSig = signed.signatures[0]?.signature; + if (!rawSig) throw new Error("Transaction not signed"); + const sig = bs58.encode(rawSig); + setTxSignature(sig); + try { + await connection.sendRawTransaction(signed.serialize()); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + if (!msg.includes("already been processed")) throw e; + } + + setAwaitingTxReceipt(true); + await connection.confirmTransaction(sig, "confirmed"); + setConfirmedTxSignature(sig); + setAwaitingTxReceipt(false); + }, [wallet, connection, salePDA]); + const isEntityStateLoaded = committedAmount !== undefined; const currentTotalRaw: bigint = committedAmount ?? 0n; const currentTotalReadableStr = (Number(currentTotalRaw) / 1e6).toLocaleString(undefined, { @@ -231,6 +291,9 @@ export function useSaleContract(saleSpecificEntityID: string) { return { commitWithPermit, + cancelBid, + contractStage, + committedAmount, txSignature, confirmedTxSignature, awaitingTxReceipt, From 92f9a9d8fee7b16abed0da3b898139dc73a7af32 Mon Sep 17 00:00:00 2001 From: Emmanuel Byrd <25442086+EByrdS@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:29:51 +0200 Subject: [PATCH 6/9] Cancellation phase UX: rename state, neutral styling, stage checks - Rename "cancelled" phase to "cancellation" throughout all 4 frameworks (EVM/SVM, React/Next.js) - Replace red banner/card styling with neutral amber for the cancellation period - CancelCard: hide committed amount when 0, show "No active commitment to cancel" instead - CancelCard: update testing instructions to reference founder's dashboard + openCancellation, link to contract docs - CommitCard: add warning + disabled state when contract is not in Commitment stage (openCommitment) - Run prettier on all modified files Co-Authored-By: Claude Sonnet 4.6 --- .../src/app/components/sale/CancelCard.tsx | 60 +++++++----- .../src/app/components/sale/CommitCard.tsx | 33 +++++-- .../framework/nextjs-evm/src/app/page.tsx | 20 ++-- .../src/app/components/sale/CancelCard.tsx | 97 +++++++++++++++++++ .../src/app/components/sale/CommitCard.tsx | 33 +++++-- .../framework/nextjs-svm/src/app/page.tsx | 84 ++++++++++------ .../src/components/sale/CancelCard.tsx | 60 +++++++----- .../src/components/sale/CommitCard.tsx | 33 +++++-- .../framework/react-evm/src/pages/Home.tsx | 20 ++-- .../src/components/sale/CancelCard.tsx | 95 ++++++++++++++++++ .../src/components/sale/CommitCard.tsx | 33 +++++-- .../framework/react-svm/src/pages/Home.tsx | 84 ++++++++++------ 12 files changed, 508 insertions(+), 144 deletions(-) create mode 100644 examples/framework/nextjs-svm/src/app/components/sale/CancelCard.tsx create mode 100644 examples/framework/react-svm/src/components/sale/CancelCard.tsx diff --git a/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx b/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx index d0c8466..764a819 100644 --- a/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx +++ b/examples/framework/nextjs-evm/src/app/components/sale/CancelCard.tsx @@ -7,8 +7,15 @@ import { useSaleContract } from "../../hooks/use-sale-contract"; const CANCELLATION_STAGE = 2; function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { - const { cancelBid, contractStage, entityState, entityStateError, awaitingTxReceipt, txReceipt, awaitingTxReceiptError } = - useSaleContract(saleSpecificEntityID); + const { + cancelBid, + contractStage, + entityState, + entityStateError, + awaitingTxReceipt, + txReceipt, + awaitingTxReceiptError, + } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); @@ -32,43 +39,52 @@ function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) return (
-
- {entityStateError ? ( + {entityStateError ? ( +

{entityStateError.message}

- ) : ( -

- Current committed amount:{" "} - {committedAmount !== undefined ? `${Number(committedAmount) / 1e6} USDC` : "Loading..."} -

- )} -
- - {committedAmount !== undefined && !hasCommitment ? ( -
-

No active commitment to cancel.

- ) : ( + ) : hasCommitment ? ( <> +
+

Current committed amount: {`${Number(committedAmount) / 1e6} USDC`}

+
+ {!isCancellationStage && (

Contract not in Cancellation stage

- The "Cancel Bid" button is only active when the contract is in the Cancellation stage (stage - 2). To test this feature, deploy your own contract and call{" "} - unsafeSetStage(2) to move it to the - Cancellation state. + The "Cancel Bid" button is only active when the contract is in the Cancellation stage. To test + this feature, use the founder's dashboard to open the cancellation state ( + openCancellation). See the{" "} + + contract docs + + .

)} + ) : committedAmount !== undefined ? ( +
+

No active commitment to cancel.

+
+ ) : ( +
+

Loading...

+
)} {awaitingTxReceipt && !txReceipt &&

Waiting for transaction receipt...

} @@ -84,7 +100,7 @@ function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { return ( -
+
); diff --git a/examples/framework/nextjs-evm/src/app/components/sale/CommitCard.tsx b/examples/framework/nextjs-evm/src/app/components/sale/CommitCard.tsx index 261129b..4e5cd55 100644 --- a/examples/framework/nextjs-evm/src/app/components/sale/CommitCard.tsx +++ b/examples/framework/nextjs-evm/src/app/components/sale/CommitCard.tsx @@ -7,8 +7,10 @@ import { saleUUID, paymentTokenAddress } from "@/lib/config"; import { useSonarPurchase } from "../../hooks/use-sonar-purchase"; import { useSaleContract } from "../../hooks/use-sale-contract"; +const COMMITMENT_STAGE = 1; + function readinessConfig( - sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase + sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase, ) { const okConfig = (msg: string) => ({ fgCol: "text-green-800", @@ -39,11 +41,11 @@ function readinessConfig( return warningConfig("The connected wallet is not eligible for this sale. Connect a different wallet."); case PrePurchaseFailureReason.MAX_WALLETS_USED: return warningConfig( - "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet." + "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet.", ); case PrePurchaseFailureReason.WALLET_NOT_LINKED: return warningConfig( - "Wallet not linked — The connected wallet is not linked to your entity. Please link it first." + "Wallet not linked — The connected wallet is not linked to your entity. Please link it first.", ); case PrePurchaseFailureReason.SALE_NOT_ACTIVE: return errorConfig("The sale is not currently active."); @@ -70,6 +72,7 @@ function CommitSection({ awaitingTxReceiptError, isWrongChain, usdcBalance, + contractStage, } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); @@ -87,6 +90,7 @@ function CommitSection({ const hasExistingCommitment = isEntityStateLoaded && currentTotalRaw > 0n; const hasInsufficientBalance = usdcBalance != null && isIncrementAmountValid && incrementRaw > usdcBalance; + const notInCommitmentStage = contractStage !== undefined && contractStage !== COMMITMENT_STAGE; const [showInput, setShowInput] = useState(true); @@ -127,10 +131,27 @@ function CommitSection({
{hasExistingCommitment && (

- Current commitment:{" "} - {currentTotalReadableStr} USDC + Current commitment: {currentTotalReadableStr} USDC

)} + {notInCommitmentStage && ( +
+

Contract not in Commitment stage

+

+ The Commit button is only active during the Commitment stage. Use the founder's dashboard to open the + commitment period (openCommitment). See the{" "} + + contract docs + + . +

+
+ )} {showInput ? ( <>
@@ -154,7 +175,7 @@ function CommitSection({ )}
)} - {salePhase === "cancelled" && ( -
+ {salePhase === "cancellation" && ( +
-

The sale has been cancelled

-

You may cancel your bid to receive a full refund of your committed funds

+

Cancellation Period

+

Commitments can be cancelled during this period

)} @@ -251,7 +249,7 @@ export default function Home() { )} {/* Cancellation Phase */} - {salePhase === "cancelled" && ( + {salePhase === "cancellation" && (
{sonarConnected && (
diff --git a/examples/framework/nextjs-svm/src/app/components/sale/CancelCard.tsx b/examples/framework/nextjs-svm/src/app/components/sale/CancelCard.tsx new file mode 100644 index 0000000..c6a98b3 --- /dev/null +++ b/examples/framework/nextjs-svm/src/app/components/sale/CancelCard.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useState } from "react"; +import { useSaleContract } from "../../hooks/use-sale-contract"; + +const CANCELLATION_STAGE = 2; + +function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: string }) { + const { cancelBid, contractStage, committedAmount, entityStateError, awaitingTxReceipt } = + useSaleContract(saleSpecificEntityID); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + const [cancelSuccess, setCancelSuccess] = useState(false); + + const isCancellationStage = contractStage === CANCELLATION_STAGE; + const hasCommitment = committedAmount !== undefined && committedAmount > 0n; + + const cancel = async () => { + setLoading(true); + setError(undefined); + try { + await cancelBid(); + setCancelSuccess(true); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + return ( +
+ {entityStateError ? ( +
+

{entityStateError.message}

+
+ ) : hasCommitment ? ( + <> +
+

Current committed amount: {`${Number(committedAmount) / 1e6} USDC`}

+
+ + {!isCancellationStage && ( +
+ )} + + + + ) : committedAmount !== undefined ? ( +
+

No active commitment to cancel.

+
+ ) : ( +
+

Loading...

+
+ )} + + {awaitingTxReceipt &&

Waiting for transaction confirmation...

} + {cancelSuccess &&

Cancellation successful — your funds have been refunded

} + {error &&

{error.message}

} +
+ ); +} + +function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: string }) { + return ( +
+ +
+ ); +} + +export default CancelCard; diff --git a/examples/framework/nextjs-svm/src/app/components/sale/CommitCard.tsx b/examples/framework/nextjs-svm/src/app/components/sale/CommitCard.tsx index 71372f6..8a90f08 100644 --- a/examples/framework/nextjs-svm/src/app/components/sale/CommitCard.tsx +++ b/examples/framework/nextjs-svm/src/app/components/sale/CommitCard.tsx @@ -7,8 +7,10 @@ import { saleUUID } from "@/lib/config"; import { useSonarPurchase } from "../../hooks/use-sonar-purchase"; import { useSaleContract } from "../../hooks/use-sale-contract"; +const COMMITMENT_STAGE = 1; + function readinessConfig( - sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase + sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase, ) { const okConfig = (msg: string) => ({ fgCol: "text-green-800", @@ -39,11 +41,11 @@ function readinessConfig( return warningConfig("The connected wallet is not eligible for this sale. Connect a different wallet."); case PrePurchaseFailureReason.MAX_WALLETS_USED: return warningConfig( - "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet." + "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet.", ); case PrePurchaseFailureReason.WALLET_NOT_LINKED: return warningConfig( - "Wallet not linked — The connected wallet is not linked to your entity. Please link it first." + "Wallet not linked — The connected wallet is not linked to your entity. Please link it first.", ); case PrePurchaseFailureReason.SALE_NOT_ACTIVE: return errorConfig("The sale is not currently active."); @@ -68,6 +70,7 @@ function CommitSection({ entityStateError, awaitingTxReceipt, usdcBalance, + contractStage, } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); @@ -85,6 +88,7 @@ function CommitSection({ const hasExistingCommitment = isEntityStateLoaded && currentTotalRaw > 0n; const hasInsufficientBalance = usdcBalance !== undefined && isIncrementAmountValid && incrementRaw > usdcBalance; + const notInCommitmentStage = contractStage !== undefined && contractStage !== COMMITMENT_STAGE; const [showInput, setShowInput] = useState(true); @@ -116,10 +120,27 @@ function CommitSection({
{hasExistingCommitment && (

- Current commitment:{" "} - {currentTotalReadableStr} USDC + Current commitment: {currentTotalReadableStr} USDC

)} + {notInCommitmentStage && ( +
+

Contract not in Commitment stage

+

+ The Commit button is only active during the Commitment stage. Use the founder's dashboard to open the + commitment period (openCommitment). See the{" "} + + contract docs + + . +

+
+ )} {showInput ? ( <>
@@ -143,7 +164,7 @@ function CommitSection({ )}
+ + + +
@@ -164,8 +162,8 @@ export default function Home() {

Easy Company Token Sale

- {/* Countdown Banner */} - {!saleIsLive && ( + {/* Phase Banner */} + {salePhase === "presale" && (

Sale Starting Soon

@@ -174,20 +172,29 @@ export default function Home() {
)} - {saleIsLive && ( + {salePhase === "live" && (

The sale is now live!

)} + + {salePhase === "cancellation" && ( +
+
+

Cancellation Period

+

Commitments can be cancelled during this period

+
+
+ )}
{/* Registration Phase */} - {sonarConnected && !saleIsLive && ( + {sonarConnected && salePhase === "presale" && (

Check Your Eligibility

@@ -211,7 +218,7 @@ export default function Home() { )} {/* Sale Phase */} - {saleIsLive && ( + {salePhase === "live" && (
{sonarConnected && (
@@ -241,6 +248,29 @@ export default function Home() {
)} + + {/* Cancellation Phase */} + {salePhase === "cancellation" && ( +
+ {sonarConnected && ( +
+ + +
+

Your Entity Information

+ +
+ + {address && selectedEntity && ( +
+

Cancel Your Bid

+ +
+ )} +
+ )} +
+ )}
diff --git a/examples/framework/react-evm/src/components/sale/CancelCard.tsx b/examples/framework/react-evm/src/components/sale/CancelCard.tsx index 3305f2f..33b8d95 100644 --- a/examples/framework/react-evm/src/components/sale/CancelCard.tsx +++ b/examples/framework/react-evm/src/components/sale/CancelCard.tsx @@ -5,8 +5,15 @@ import { useSaleContract } from "../../hooks"; const CANCELLATION_STAGE = 2; function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { - const { cancelBid, contractStage, entityState, entityStateError, awaitingTxReceipt, txReceipt, awaitingTxReceiptError } = - useSaleContract(saleSpecificEntityID); + const { + cancelBid, + contractStage, + entityState, + entityStateError, + awaitingTxReceipt, + txReceipt, + awaitingTxReceiptError, + } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); @@ -30,43 +37,52 @@ function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) return (
-
- {entityStateError ? ( + {entityStateError ? ( +

{entityStateError.message}

- ) : ( -

- Current committed amount:{" "} - {committedAmount !== undefined ? `${Number(committedAmount) / 1e6} USDC` : "Loading..."} -

- )} -
- - {committedAmount !== undefined && !hasCommitment ? ( -
-

No active commitment to cancel.

- ) : ( + ) : hasCommitment ? ( <> +
+

Current committed amount: {`${Number(committedAmount) / 1e6} USDC`}

+
+ {!isCancellationStage && (
)} + ) : committedAmount !== undefined ? ( +
+

No active commitment to cancel.

+
+ ) : ( +
+

Loading...

+
)} {awaitingTxReceipt && !txReceipt &&

Waiting for transaction receipt...

} @@ -82,7 +98,7 @@ function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: Hex }) { return ( -
+
); diff --git a/examples/framework/react-evm/src/components/sale/CommitCard.tsx b/examples/framework/react-evm/src/components/sale/CommitCard.tsx index 3e6307f..e4e1dab 100644 --- a/examples/framework/react-evm/src/components/sale/CommitCard.tsx +++ b/examples/framework/react-evm/src/components/sale/CommitCard.tsx @@ -8,8 +8,10 @@ import { } from "@echoxyz/sonar-react"; import { useSaleContract } from "../../hooks"; +const COMMITMENT_STAGE = 1; + function readinessConfig( - sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase + sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase, ) { const okConfig = (msg: string) => ({ fgCol: "text-green-800", @@ -40,11 +42,11 @@ function readinessConfig( return warningConfig("The connected wallet is not eligible for this sale. Connect a different wallet."); case PrePurchaseFailureReason.MAX_WALLETS_USED: return warningConfig( - "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet." + "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet.", ); case PrePurchaseFailureReason.WALLET_NOT_LINKED: return warningConfig( - "Wallet not linked — The connected wallet is not linked to your entity. Please link it first." + "Wallet not linked — The connected wallet is not linked to your entity. Please link it first.", ); case PrePurchaseFailureReason.SALE_NOT_ACTIVE: return errorConfig("The sale is not currently active."); @@ -71,6 +73,7 @@ function CommitSection({ awaitingTxReceiptError, isWrongChain, usdcBalance, + contractStage, } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); @@ -88,6 +91,7 @@ function CommitSection({ const hasExistingCommitment = isEntityStateLoaded && currentTotalRaw > 0n; const hasInsufficientBalance = usdcBalance != null && isIncrementAmountValid && incrementRaw > usdcBalance; + const notInCommitmentStage = contractStage !== undefined && contractStage !== COMMITMENT_STAGE; const [showInput, setShowInput] = useState(true); @@ -128,10 +132,27 @@ function CommitSection({
{hasExistingCommitment && (

- Current commitment:{" "} - {currentTotalReadableStr} USDC + Current commitment: {currentTotalReadableStr} USDC

)} + {notInCommitmentStage && ( +
+

Contract not in Commitment stage

+

+ The Commit button is only active during the Commitment stage. Use the founder's dashboard to open the + commitment period (openCommitment). See the{" "} + + contract docs + + . +

+
+ )} {showInput ? ( <>
@@ -155,7 +176,7 @@ function CommitSection({ )}
)} - {salePhase === "cancelled" && ( -
+ {salePhase === "cancellation" && ( +
-

The sale has been cancelled

-

You may cancel your bid to receive a full refund of your committed funds

+

Cancellation Period

+

Commitments can be cancelled during this period

)} @@ -259,7 +257,7 @@ export function Home() { )} {/* Cancellation Phase */} - {salePhase === "cancelled" && ( + {salePhase === "cancellation" && (
{/* Connection Buttons */} diff --git a/examples/framework/react-svm/src/components/sale/CancelCard.tsx b/examples/framework/react-svm/src/components/sale/CancelCard.tsx new file mode 100644 index 0000000..17226aa --- /dev/null +++ b/examples/framework/react-svm/src/components/sale/CancelCard.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import { useSaleContract } from "../../hooks/use-sale-contract"; + +const CANCELLATION_STAGE = 2; + +function CancelSection({ saleSpecificEntityID }: { saleSpecificEntityID: string }) { + const { cancelBid, contractStage, committedAmount, entityStateError, awaitingTxReceipt } = + useSaleContract(saleSpecificEntityID); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + const [cancelSuccess, setCancelSuccess] = useState(false); + + const isCancellationStage = contractStage === CANCELLATION_STAGE; + const hasCommitment = committedAmount !== undefined && committedAmount > 0n; + + const cancel = async () => { + setLoading(true); + setError(undefined); + try { + await cancelBid(); + setCancelSuccess(true); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + return ( +
+ {entityStateError ? ( +
+

{entityStateError.message}

+
+ ) : hasCommitment ? ( + <> +
+

Current committed amount: {`${Number(committedAmount) / 1e6} USDC`}

+
+ + {!isCancellationStage && ( +
+ )} + + + + ) : committedAmount !== undefined ? ( +
+

No active commitment to cancel.

+
+ ) : ( +
+

Loading...

+
+ )} + + {awaitingTxReceipt &&

Waiting for transaction confirmation...

} + {cancelSuccess &&

Cancellation successful — your funds have been refunded

} + {error &&

{error.message}

} +
+ ); +} + +function CancelCard({ saleSpecificEntityID }: { saleSpecificEntityID: string }) { + return ( +
+ +
+ ); +} + +export default CancelCard; diff --git a/examples/framework/react-svm/src/components/sale/CommitCard.tsx b/examples/framework/react-svm/src/components/sale/CommitCard.tsx index 09fd1ab..4c0d73a 100644 --- a/examples/framework/react-svm/src/components/sale/CommitCard.tsx +++ b/examples/framework/react-svm/src/components/sale/CommitCard.tsx @@ -8,8 +8,10 @@ import { } from "@echoxyz/sonar-react"; import { useSaleContract } from "../../hooks/use-sale-contract"; +const COMMITMENT_STAGE = 1; + function readinessConfig( - sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase + sonarPurchaser: UseSonarPurchaseResultReadyToPurchase | UseSonarPurchaseResultNotReadyToPurchase, ) { const okConfig = (msg: string) => ({ fgCol: "text-green-800", @@ -40,11 +42,11 @@ function readinessConfig( return warningConfig("The connected wallet is not eligible for this sale. Connect a different wallet."); case PrePurchaseFailureReason.MAX_WALLETS_USED: return warningConfig( - "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet." + "Maximum number of wallets reached — This entity can't use the connected wallet. Use a previous wallet.", ); case PrePurchaseFailureReason.WALLET_NOT_LINKED: return warningConfig( - "Wallet not linked — The connected wallet is not linked to your entity. Please link it first." + "Wallet not linked — The connected wallet is not linked to your entity. Please link it first.", ); case PrePurchaseFailureReason.SALE_NOT_ACTIVE: return errorConfig("The sale is not currently active."); @@ -69,6 +71,7 @@ function CommitSection({ entityStateError, awaitingTxReceipt, usdcBalance, + contractStage, } = useSaleContract(saleSpecificEntityID); const [loading, setLoading] = useState(false); @@ -86,6 +89,7 @@ function CommitSection({ const hasExistingCommitment = isEntityStateLoaded && currentTotalRaw > 0n; const hasInsufficientBalance = usdcBalance !== undefined && isIncrementAmountValid && incrementRaw > usdcBalance; + const notInCommitmentStage = contractStage !== undefined && contractStage !== COMMITMENT_STAGE; const [showInput, setShowInput] = useState(true); @@ -117,10 +121,27 @@ function CommitSection({
{hasExistingCommitment && (

- Current commitment:{" "} - {currentTotalReadableStr} USDC + Current commitment: {currentTotalReadableStr} USDC

)} + {notInCommitmentStage && ( +
+

Contract not in Commitment stage

+

+ The Commit button is only active during the Commitment stage. Use the founder's dashboard to open the + commitment period (openCommitment). See the{" "} + + contract docs + + . +

+
+ )} {showInput ? ( <>
@@ -144,7 +165,7 @@ function CommitSection({ )}
+ + + +
@@ -168,8 +166,8 @@ export function Home() {

Easy Company Token Sale

- {/* Countdown Banner */} - {!saleIsLive && ( + {/* Phase Banner */} + {salePhase === "presale" && (

Sale Starting Soon

@@ -178,17 +176,26 @@ export function Home() {
)} - {saleIsLive && ( + {salePhase === "live" && (

The sale is now live!

)} + + {salePhase === "cancellation" && ( +
+
+

Cancellation Period

+

Commitments can be cancelled during this period

+
+
+ )}
{/* Registration Phase */} - {!saleIsLive && ( + {salePhase === "presale" && (
@@ -218,7 +225,7 @@ export function Home() { )} {/* Sale Phase */} - {saleIsLive && ( + {salePhase === "live" && (
{/* Connection Buttons */} @@ -249,6 +256,29 @@ export function Home() {
)} + + {/* Cancellation Phase */} + {salePhase === "cancellation" && ( +
+ {/* Connection Buttons */} + + + + {/* Entity Information */} +
+

Your Entity Information

+ +
+ + {/* Cancel Card */} + {address && selectedEntity && ( +
+

Cancel Your Bid

+ +
+ )} +
+ )}
From 7279a540250fc17958392224635609ba28701119 Mon Sep 17 00:00:00 2001 From: Emmanuel Byrd <25442086+EByrdS@users.noreply.github.com> Date: Tue, 5 May 2026 16:14:47 +0100 Subject: [PATCH 7/9] Prettier --- .../framework/nextjs-evm/src/app/Provider.tsx | 2 +- .../nextjs-evm/src/app/actions/sonar.ts | 4 +-- .../components/sale/CommitmentDataCard.tsx | 10 +++---- .../src/app/hooks/use-sale-contract.ts | 2 +- .../nextjs-svm/src/app/actions/sonar.ts | 4 +-- .../components/sale/CommitmentDataCard.tsx | 8 ++---- .../src/app/hooks/use-sale-contract.ts | 26 ++++++++++++------- examples/framework/react-evm/src/App.tsx | 1 - examples/framework/react-evm/src/Provider.tsx | 2 +- .../components/sale/CommitmentDataCard.tsx | 10 +++---- examples/framework/react-evm/src/hooks.ts | 2 +- examples/framework/react-evm/src/main.tsx | 3 +-- examples/framework/react-svm/src/App.tsx | 1 - .../components/sale/CommitmentDataCard.tsx | 10 +++---- .../react-svm/src/hooks/use-sale-contract.ts | 26 ++++++++++++------- examples/framework/react-svm/src/main.tsx | 3 +-- 16 files changed, 55 insertions(+), 59 deletions(-) diff --git a/examples/framework/nextjs-evm/src/app/Provider.tsx b/examples/framework/nextjs-evm/src/app/Provider.tsx index 9403f28..0469d8a 100644 --- a/examples/framework/nextjs-evm/src/app/Provider.tsx +++ b/examples/framework/nextjs-evm/src/app/Provider.tsx @@ -21,7 +21,7 @@ const config = createConfig( // Required App Info appName: "Sonar Next.js example app", appDescription: "Next.js app showing how to integrate with the Sonar API via backend OAuth.", - }) + }), ); const queryClient = new QueryClient(); diff --git a/examples/framework/nextjs-evm/src/app/actions/sonar.ts b/examples/framework/nextjs-evm/src/app/actions/sonar.ts index 4e53b13..7416dc8 100644 --- a/examples/framework/nextjs-evm/src/app/actions/sonar.ts +++ b/examples/framework/nextjs-evm/src/app/actions/sonar.ts @@ -18,7 +18,7 @@ export const getEntities = createSonarServerAction
- - {formatAmount(total, decimals)} - + {formatAmount(total, decimals)} {commitment.SaleSpecificEntityID}
{formatTimestamp(commitment.CreatedAt)} @@ -81,9 +79,7 @@ export function CommitmentDataCard({ saleUUID }: { saleUUID: string }) {

Total Committers

-

- {commitmentData.UniqueCommitmentCount.toLocaleString()} -

+

{commitmentData.UniqueCommitmentCount.toLocaleString()}

Total Committed

diff --git a/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts b/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts index d0e7814..51074b1 100644 --- a/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts +++ b/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts @@ -127,7 +127,7 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { const currentTotalReadableStr = (Number(currentTotalRaw) / 1e6).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, - }) + }); const { data: contractStage } = useReadContract({ address: saleContract, diff --git a/examples/framework/nextjs-svm/src/app/actions/sonar.ts b/examples/framework/nextjs-svm/src/app/actions/sonar.ts index 4e53b13..7416dc8 100644 --- a/examples/framework/nextjs-svm/src/app/actions/sonar.ts +++ b/examples/framework/nextjs-svm/src/app/actions/sonar.ts @@ -18,7 +18,7 @@ export const getEntities = createSonarServerAction
- - {formatAmount(total, decimals)} - + {formatAmount(total, decimals)} {commitment.SaleSpecificEntityID}
{formatTimestamp(commitment.CreatedAt)} @@ -81,9 +79,7 @@ export function CommitmentDataCard({ saleUUID }: { saleUUID: string }) {

Total Committers

-

- {commitmentData.UniqueCommitmentCount.toLocaleString()} -

+

{commitmentData.UniqueCommitmentCount.toLocaleString()}

Total Committed

diff --git a/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts b/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts index 2e8c0ec..fbaae61 100644 --- a/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts +++ b/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts @@ -36,7 +36,11 @@ function stageToNumber(stage: unknown): number { if (typeof stage === "object" && stage !== null) { const key = Object.keys(stage as object)[0]; const map: Record = { - preOpen: 0, commitment: 1, cancellation: 2, settlement: 3, done: 4, + preOpen: 0, + commitment: 1, + cancellation: 2, + settlement: 3, + done: 4, }; return map[key] ?? -1; } @@ -66,12 +70,12 @@ export function useSaleContract(saleSpecificEntityID: string) { const saleUuidBytes = parseIdBytes(saleUUID); const [salePDA] = PublicKey.findProgramAddressSync( [Buffer.from("settlement_sale"), Buffer.from(saleUuidBytes)], - programPublicKey + programPublicKey, ); const saleEntityIdBytes = parseIdBytes(saleSpecificEntityID); const [entityStatePDA] = PublicKey.findProgramAddressSync( [Buffer.from("entity_state"), salePDA.toBuffer(), Buffer.from(saleEntityIdBytes)], - programPublicKey + programPublicKey, ); return { salePDA, entityStatePDA }; }, [saleSpecificEntityID, programPublicKey]); @@ -85,13 +89,17 @@ export function useSaleContract(saleSpecificEntityID: string) { connection, // eslint-disable-next-line @typescript-eslint/no-explicit-any wallet ?? ({ publicKey: PublicKey.default } as any), - { commitment: "confirmed" } + { commitment: "confirmed" }, ); const program = new Program(IDL, provider); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const state = (await (program.account as any).entityState.fetchNullable(entityStatePDA)) as EntityStateAccount | null; + const state = (await (program.account as any).entityState.fetchNullable( + entityStatePDA, + )) as EntityStateAccount | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const saleAccount = (await (program.account as any).settlementSale.fetchNullable(salePDA)) as SettlementSaleAccount | null; + const saleAccount = (await (program.account as any).settlementSale.fetchNullable( + salePDA, + )) as SettlementSaleAccount | null; if (!cancelled) { setCommittedAmount(state ? BigInt(state.currentAmount.toString()) : 0n); setEntityStateError(undefined); @@ -144,7 +152,7 @@ export function useSaleContract(saleSpecificEntityID: string) { // since the program validates the account against the permit's entity ID. const [permitEntityStatePDA] = PublicKey.findProgramAddressSync( [Buffer.from("entity_state"), salePDA.toBuffer(), Buffer.from(saleEntityIdArr)], - programPublicKey + programPublicKey, ); const walletBytes = Array.from(new PublicKey(permit.Wallet).toBytes()); const payloadHex = permit.Payload.replace(/^0x/, ""); @@ -185,7 +193,7 @@ export function useSaleContract(saleSpecificEntityID: string) { const [walletBindingPDA] = PublicKey.findProgramAddressSync( [Buffer.from("wallet_binding"), salePDA.toBuffer(), wallet.publicKey.toBuffer()], - programPublicKey + programPublicKey, ); const bidderTokenAccount = getAssociatedTokenAddressSync(new PublicKey(PAYMENT_TOKEN_MINT), wallet.publicKey); @@ -222,7 +230,7 @@ export function useSaleContract(saleSpecificEntityID: string) { setConfirmedTxSignature(sig); setAwaitingTxReceipt(false); }, - [wallet, connection, salePDA, entityStatePDA, programPublicKey] + [wallet, connection, salePDA, entityStatePDA, programPublicKey], ); const cancelBid = useCallback(async () => { diff --git a/examples/framework/react-evm/src/App.tsx b/examples/framework/react-evm/src/App.tsx index 58ca82b..09d9ded 100644 --- a/examples/framework/react-evm/src/App.tsx +++ b/examples/framework/react-evm/src/App.tsx @@ -16,4 +16,3 @@ export function App() { ); } - diff --git a/examples/framework/react-evm/src/Provider.tsx b/examples/framework/react-evm/src/Provider.tsx index e8361cb..efa998e 100644 --- a/examples/framework/react-evm/src/Provider.tsx +++ b/examples/framework/react-evm/src/Provider.tsx @@ -19,7 +19,7 @@ const config = createConfig( appName: "Sonar React example app", appDescription: "React app showing how to integrate with the Sonar API via the sonar-react and sonar-core libraries.", - }) + }), ); const queryClient = new QueryClient(); diff --git a/examples/framework/react-evm/src/components/sale/CommitmentDataCard.tsx b/examples/framework/react-evm/src/components/sale/CommitmentDataCard.tsx index 51cf590..170efbd 100644 --- a/examples/framework/react-evm/src/components/sale/CommitmentDataCard.tsx +++ b/examples/framework/react-evm/src/components/sale/CommitmentDataCard.tsx @@ -12,7 +12,7 @@ function calculateCommitmentTotal(amounts: WalletTokenAmount[]): bigint { } // The timestamp is a string in ISO 8601 format -function formatTimestamp(timestamp: string ): string { +function formatTimestamp(timestamp: string): string { const date = new Date(timestamp); return date.toLocaleString(undefined, { month: "short", @@ -35,9 +35,7 @@ function CommitmentRow({ commitment, decimals }: CommitmentRowProps) { return (
- - {formatAmount(total, decimals)} - + {formatAmount(total, decimals)} {commitment.SaleSpecificEntityID}
{formatTimestamp(commitment.CreatedAt)} @@ -79,9 +77,7 @@ export function CommitmentDataCard({ saleUUID }: { saleUUID: string }) {

Total Committers

-

- {commitmentData.UniqueCommitmentCount.toLocaleString()} -

+

{commitmentData.UniqueCommitmentCount.toLocaleString()}

Total Committed

diff --git a/examples/framework/react-evm/src/hooks.ts b/examples/framework/react-evm/src/hooks.ts index 6ed8da3..1abd820 100644 --- a/examples/framework/react-evm/src/hooks.ts +++ b/examples/framework/react-evm/src/hooks.ts @@ -127,7 +127,7 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { const currentTotalReadableStr = (Number(currentTotalRaw) / 1e6).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, - }) + }); const { data: contractStage } = useReadContract({ address: saleContract, diff --git a/examples/framework/react-evm/src/main.tsx b/examples/framework/react-evm/src/main.tsx index 6b6cb86..61f2cd1 100644 --- a/examples/framework/react-evm/src/main.tsx +++ b/examples/framework/react-evm/src/main.tsx @@ -6,6 +6,5 @@ import "./globals.css"; ReactDOM.createRoot(document.getElementById("root")!).render( - + , ); - diff --git a/examples/framework/react-svm/src/App.tsx b/examples/framework/react-svm/src/App.tsx index 58ca82b..09d9ded 100644 --- a/examples/framework/react-svm/src/App.tsx +++ b/examples/framework/react-svm/src/App.tsx @@ -16,4 +16,3 @@ export function App() { ); } - diff --git a/examples/framework/react-svm/src/components/sale/CommitmentDataCard.tsx b/examples/framework/react-svm/src/components/sale/CommitmentDataCard.tsx index 51cf590..170efbd 100644 --- a/examples/framework/react-svm/src/components/sale/CommitmentDataCard.tsx +++ b/examples/framework/react-svm/src/components/sale/CommitmentDataCard.tsx @@ -12,7 +12,7 @@ function calculateCommitmentTotal(amounts: WalletTokenAmount[]): bigint { } // The timestamp is a string in ISO 8601 format -function formatTimestamp(timestamp: string ): string { +function formatTimestamp(timestamp: string): string { const date = new Date(timestamp); return date.toLocaleString(undefined, { month: "short", @@ -35,9 +35,7 @@ function CommitmentRow({ commitment, decimals }: CommitmentRowProps) { return (
- - {formatAmount(total, decimals)} - + {formatAmount(total, decimals)} {commitment.SaleSpecificEntityID}
{formatTimestamp(commitment.CreatedAt)} @@ -79,9 +77,7 @@ export function CommitmentDataCard({ saleUUID }: { saleUUID: string }) {

Total Committers

-

- {commitmentData.UniqueCommitmentCount.toLocaleString()} -

+

{commitmentData.UniqueCommitmentCount.toLocaleString()}

Total Committed

diff --git a/examples/framework/react-svm/src/hooks/use-sale-contract.ts b/examples/framework/react-svm/src/hooks/use-sale-contract.ts index 0bd6cda..6ce3f0e 100644 --- a/examples/framework/react-svm/src/hooks/use-sale-contract.ts +++ b/examples/framework/react-svm/src/hooks/use-sale-contract.ts @@ -37,7 +37,11 @@ function stageToNumber(stage: unknown): number { if (typeof stage === "object" && stage !== null) { const key = Object.keys(stage as object)[0]; const map: Record = { - preOpen: 0, commitment: 1, cancellation: 2, settlement: 3, done: 4, + preOpen: 0, + commitment: 1, + cancellation: 2, + settlement: 3, + done: 4, }; return map[key] ?? -1; } @@ -67,12 +71,12 @@ export function useSaleContract(saleSpecificEntityID: string) { const saleUuidBytes = parseIdBytes(saleUUID); const [salePDA] = PublicKey.findProgramAddressSync( [Buffer.from("settlement_sale"), Buffer.from(saleUuidBytes)], - programPublicKey + programPublicKey, ); const saleEntityIdBytes = parseIdBytes(saleSpecificEntityID); const [entityStatePDA] = PublicKey.findProgramAddressSync( [Buffer.from("entity_state"), salePDA.toBuffer(), Buffer.from(saleEntityIdBytes)], - programPublicKey + programPublicKey, ); return { salePDA, entityStatePDA }; }, [saleSpecificEntityID, programPublicKey]); @@ -86,14 +90,18 @@ export function useSaleContract(saleSpecificEntityID: string) { connection, // eslint-disable-next-line @typescript-eslint/no-explicit-any wallet ?? ({ publicKey: PublicKey.default } as any), - { commitment: "confirmed" } + { commitment: "confirmed" }, ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const program = new Program(IDL as any, provider); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const state = (await (program.account as any).entityState.fetchNullable(entityStatePDA)) as EntityStateAccount | null; + const state = (await (program.account as any).entityState.fetchNullable( + entityStatePDA, + )) as EntityStateAccount | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const saleAccount = (await (program.account as any).settlementSale.fetchNullable(salePDA)) as SettlementSaleAccount | null; + const saleAccount = (await (program.account as any).settlementSale.fetchNullable( + salePDA, + )) as SettlementSaleAccount | null; if (!cancelled) { setCommittedAmount(state ? BigInt(state.currentAmount.toString()) : 0n); setEntityStateError(undefined); @@ -146,7 +154,7 @@ export function useSaleContract(saleSpecificEntityID: string) { // since the program validates the account against the permit's entity ID. const [permitEntityStatePDA] = PublicKey.findProgramAddressSync( [Buffer.from("entity_state"), salePDA.toBuffer(), Buffer.from(saleEntityIdArr)], - programPublicKey + programPublicKey, ); const walletBytes = Array.from(new PublicKey(permit.Wallet).toBytes()); const payloadHex = permit.Payload.replace(/^0x/, ""); @@ -187,7 +195,7 @@ export function useSaleContract(saleSpecificEntityID: string) { const [walletBindingPDA] = PublicKey.findProgramAddressSync( [Buffer.from("wallet_binding"), salePDA.toBuffer(), wallet.publicKey.toBuffer()], - programPublicKey + programPublicKey, ); const bidderTokenAccount = getAssociatedTokenAddressSync(new PublicKey(PAYMENT_TOKEN_MINT), wallet.publicKey); @@ -235,7 +243,7 @@ export function useSaleContract(saleSpecificEntityID: string) { setConfirmedTxSignature(sig); setAwaitingTxReceipt(false); }, - [wallet, connection, salePDA, entityStatePDA, programPublicKey] + [wallet, connection, salePDA, entityStatePDA, programPublicKey], ); const cancelBid = useCallback(async () => { diff --git a/examples/framework/react-svm/src/main.tsx b/examples/framework/react-svm/src/main.tsx index a28a11d..b3227d8 100644 --- a/examples/framework/react-svm/src/main.tsx +++ b/examples/framework/react-svm/src/main.tsx @@ -7,6 +7,5 @@ import "./globals.css"; ReactDOM.createRoot(document.getElementById("root")!).render( - + , ); - From 746652f33eaf6674122dd7332c3f239b8a5ebde7 Mon Sep 17 00:00:00 2001 From: Emmanuel Byrd <25442086+EByrdS@users.noreply.github.com> Date: Tue, 5 May 2026 17:20:39 +0100 Subject: [PATCH 8/9] Adds chain guard --- .../framework/nextjs-evm/src/app/hooks/use-sale-contract.ts | 5 ++++- examples/framework/react-evm/src/hooks.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts b/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts index 51074b1..9275892 100644 --- a/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts +++ b/examples/framework/nextjs-evm/src/app/hooks/use-sale-contract.ts @@ -90,6 +90,9 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { ); const cancelBid = useCallback(async () => { + if (chainId !== baseSepolia.id) { + await switchChainAsync({ chainId: baseSepolia.id }); + } const { request } = await simulateContract(config, { address: saleContract, abi: settlementSaleAbi, @@ -98,7 +101,7 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { }); const hash = await writeContractAsync(request); setTxHash(hash); - }, [writeContractAsync, config]); + }, [writeContractAsync, config, chainId, switchChainAsync]); const { data: entityStates, error: entityStateError } = useReadContract({ address: saleContract, diff --git a/examples/framework/react-evm/src/hooks.ts b/examples/framework/react-evm/src/hooks.ts index 1abd820..78e0c48 100644 --- a/examples/framework/react-evm/src/hooks.ts +++ b/examples/framework/react-evm/src/hooks.ts @@ -90,6 +90,9 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { ); const cancelBid = useCallback(async () => { + if (chainId !== baseSepolia.id) { + await switchChainAsync({ chainId: baseSepolia.id }); + } const { request } = await simulateContract(config, { address: saleContract, abi: settlementSaleAbi, @@ -98,7 +101,7 @@ export const useSaleContract = (saleSpecificEntityID: Hex) => { }); const hash = await writeContractAsync(request); setTxHash(hash); - }, [writeContractAsync, config]); + }, [writeContractAsync, config, chainId, switchChainAsync]); const { data: entityStates, error: entityStateError } = useReadContract({ address: saleContract, From 50df443d8e8a51d09f6803ad718570a9c3396587 Mon Sep 17 00:00:00 2001 From: Emmanuel Byrd <25442086+EByrdS@users.noreply.github.com> Date: Tue, 5 May 2026 17:35:18 +0100 Subject: [PATCH 9/9] Recovers from confirmation fail --- .../nextjs-svm/src/app/hooks/use-sale-contract.ts | 9 ++++++--- .../framework/react-svm/src/hooks/use-sale-contract.ts | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts b/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts index fbaae61..a9dc5e3 100644 --- a/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts +++ b/examples/framework/nextjs-svm/src/app/hooks/use-sale-contract.ts @@ -263,9 +263,12 @@ export function useSaleContract(saleSpecificEntityID: string) { setTxSignature(sig); setAwaitingTxReceipt(true); - await connection.confirmTransaction(sig, "confirmed"); - setConfirmedTxSignature(sig); - setAwaitingTxReceipt(false); + try { + await connection.confirmTransaction(sig, "confirmed"); + setConfirmedTxSignature(sig); + } finally { + setAwaitingTxReceipt(false); + } }, [wallet, connection, salePDA]); const isEntityStateLoaded = committedAmount !== undefined; diff --git a/examples/framework/react-svm/src/hooks/use-sale-contract.ts b/examples/framework/react-svm/src/hooks/use-sale-contract.ts index 6ce3f0e..83d9a64 100644 --- a/examples/framework/react-svm/src/hooks/use-sale-contract.ts +++ b/examples/framework/react-svm/src/hooks/use-sale-contract.ts @@ -285,9 +285,12 @@ export function useSaleContract(saleSpecificEntityID: string) { } setAwaitingTxReceipt(true); - await connection.confirmTransaction(sig, "confirmed"); - setConfirmedTxSignature(sig); - setAwaitingTxReceipt(false); + try { + await connection.confirmTransaction(sig, "confirmed"); + setConfirmedTxSignature(sig); + } finally { + setAwaitingTxReceipt(false); + } }, [wallet, connection, salePDA]); const isEntityStateLoaded = committedAmount !== undefined;