@@ -395,6 +526,7 @@ export function CommunityDeploymentPanel({
!factoryId ||
networkMismatch ||
walletNetworkUnknown ||
+ authorization !== "ready" ||
stage === "simulating"
}
className="min-h-11 rounded-lg border border-indigo-500 px-4 py-2 text-sm font-medium text-indigo-200 hover:bg-indigo-950/50 disabled:cursor-not-allowed disabled:opacity-50"
@@ -410,6 +542,7 @@ export function CommunityDeploymentPanel({
!confirmed ||
networkMismatch ||
walletNetworkUnknown ||
+ authorization !== "ready" ||
stage === "awaiting_approval"
}
className="min-h-11 rounded-lg bg-indigo-500 px-5 py-2 text-sm font-medium text-white hover:bg-indigo-400 disabled:cursor-not-allowed disabled:opacity-50"
diff --git a/apps/web/src/hooks/useNetworkGuard.ts b/apps/web/src/hooks/useNetworkGuard.ts
index cc54cb9..e090032 100644
--- a/apps/web/src/hooks/useNetworkGuard.ts
+++ b/apps/web/src/hooks/useNetworkGuard.ts
@@ -2,7 +2,11 @@
import { useMemo } from "react";
import { useWallet } from "@/context/WalletProvider";
-import { compareNetworks, type NetworkComparison } from "@/lib/network";
+import {
+ compareNetworks,
+ describeNetwork,
+ type NetworkComparison,
+} from "@/lib/network";
import { activeNetwork } from "@/lib/stellar";
/**
@@ -11,9 +15,11 @@ import { activeNetwork } from "@/lib/stellar";
* network directly, so mismatch handling stays in one place.
*/
export function useNetworkGuard(): NetworkComparison {
- const { walletNetwork } = useWallet();
- return useMemo(
- () => compareNetworks(activeNetwork, walletNetwork),
- [walletNetwork],
- );
+ const { walletNetwork, walletNetworkPassphrase } = useWallet();
+ return useMemo(() => {
+ const detected = walletNetworkPassphrase
+ ? describeNetwork(walletNetworkPassphrase, walletNetwork ?? undefined)
+ : null;
+ return compareNetworks(activeNetwork, detected);
+ }, [walletNetwork, walletNetworkPassphrase]);
}
diff --git a/apps/web/src/hooks/useTransactionLifecycle.ts b/apps/web/src/hooks/useTransactionLifecycle.ts
index 3468292..76334e5 100644
--- a/apps/web/src/hooks/useTransactionLifecycle.ts
+++ b/apps/web/src/hooks/useTransactionLifecycle.ts
@@ -1,6 +1,6 @@
"use client";
-import { useCallback, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import { mapTransactionError } from "@/lib/transactionErrors";
/**
@@ -73,7 +73,9 @@ export function useTransactionLifecycle(options?: UseTransactionLifecycleOptions
});
const inFlightRef = useRef(false);
const onConfirmedRef = useRef(options?.onConfirmed);
- onConfirmedRef.current = options?.onConfirmed;
+ useEffect(() => {
+ onConfirmedRef.current = options?.onConfirmed;
+ }, [options?.onConfirmed]);
const reset = useCallback(() => {
if (inFlightRef.current) return;
diff --git a/apps/web/src/lib/community/deployment.ts b/apps/web/src/lib/community/deployment.ts
index 4730221..75f8fda 100644
--- a/apps/web/src/lib/community/deployment.ts
+++ b/apps/web/src/lib/community/deployment.ts
@@ -74,6 +74,13 @@ export type CommunityDeploymentAdapter = {
verifyRegistry(
expected: CommunityRegistryRecord,
): Promise<"verified" | "missing" | "mismatch" | "rpc-error">;
+ /**
+ * Reads the CommunityFactory owner so the UI can gate deployment before any
+ * simulation or signature. Resolves to the owner address string on success;
+ * throws on a transient read failure so callers can retry rather than treat
+ * the result as an authorization verdict.
+ */
+ readFactoryOwner(factoryId: string, publicKey: string): Promise;
};
function bytesToHex(bytes: Uint8Array): string {
@@ -349,6 +356,29 @@ export const defaultCommunityDeploymentAdapter: CommunityDeploymentAdapter = {
return "rpc-error";
}
},
+
+ /**
+ * Reads `owner()` off the factory as a read-only simulation. Any throw is a
+ * transient read failure that the UI reports as retryable, never as
+ * "unauthorized".
+ */
+ async readFactoryOwner(factoryId, publicKey) {
+ const transaction = await AssembledTransaction.build({
+ contractId: factoryId,
+ method: "owner",
+ args: [],
+ networkPassphrase: config.networkPassphrase,
+ rpcUrl: config.rpcUrl,
+ publicKey,
+ timeoutInSeconds: COMMUNITY_DEPLOYMENT_TIMEOUT_SECONDS,
+ parseResultXdr: (value) => scValToNative(value) as string,
+ });
+ const owner = transaction.result;
+ if (typeof owner !== "string" || owner.trim() === "") {
+ throw new Error("The CommunityFactory owner read did not return an address.");
+ }
+ return owner;
+ },
};
export function metadataHashBytes(invocation: CommunityFactoryInvocation) {
diff --git a/apps/web/src/lib/communityFactory/useCommunityDeployment.ts b/apps/web/src/lib/communityFactory/useCommunityDeployment.ts
index 5e9772d..5a185e3 100644
--- a/apps/web/src/lib/communityFactory/useCommunityDeployment.ts
+++ b/apps/web/src/lib/communityFactory/useCommunityDeployment.ts
@@ -27,7 +27,7 @@ const stageLabels: Record = {
};
export function useCommunityDeployment() {
- const { address, networkPassphrase, signTransaction } = useWallet();
+ const { address, walletNetworkPassphrase, signTransaction } = useWallet();
const activeSubmissionRef = useRef(false);
const [stage, setStage] = useState("idle");
const [error, setError] = useState(null);
@@ -55,7 +55,7 @@ export function useCommunityDeployment() {
const nextOutcome = await deployCommunityFromWizard(state, {
address,
expectedNetworkPassphrase: config.networkPassphrase,
- walletNetworkPassphrase: networkPassphrase,
+ walletNetworkPassphrase,
createClient: () =>
createCommunityFactoryClient({
publicKey: address ?? "",
@@ -78,7 +78,7 @@ export function useCommunityDeployment() {
activeSubmissionRef.current = false;
}
},
- [address, isSubmitting, networkPassphrase, signTransaction],
+ [address, isSubmitting, walletNetworkPassphrase, signTransaction],
);
return {
diff --git a/apps/web/src/lib/contracts.ts b/apps/web/src/lib/contracts.ts
index 0814ee1..0b3ba71 100644
--- a/apps/web/src/lib/contracts.ts
+++ b/apps/web/src/lib/contracts.ts
@@ -42,7 +42,7 @@ export function createNftClient({
const nft = contractId ?? requireContractIds().nft;
return new NftClient({
contractId: nft,
- networkPassphrase: config.passphrase,
+ networkPassphrase: config.networkPassphrase,
rpcUrl: config.rpcUrl,
publicKey,
signTransaction,
@@ -59,7 +59,7 @@ export function createGovernorClient({
if (mocked) return mocked as unknown as GovernorClient;
return new GovernorClient({
contractId: governor,
- networkPassphrase: config.passphrase,
+ networkPassphrase: config.networkPassphrase,
rpcUrl: config.rpcUrl,
publicKey,
signTransaction,
@@ -70,7 +70,7 @@ export function createReadOnlyNftClient(contractId?: string) {
const nft = contractId ?? requireContractIds().nft;
return new NftClient({
contractId: nft,
- networkPassphrase: config.passphrase,
+ networkPassphrase: config.networkPassphrase,
rpcUrl: config.rpcUrl,
});
}
@@ -81,7 +81,7 @@ export function createReadOnlyGovernorClient(contractId?: string) {
if (mocked) return mocked as unknown as GovernorClient;
return new GovernorClient({
contractId: governor,
- networkPassphrase: config.passphrase,
+ networkPassphrase: config.networkPassphrase,
rpcUrl: config.rpcUrl,
});
}
diff --git a/package-lock.json b/package-lock.json
index 46ae777..aaa7735 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.5.2",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -2498,7 +2499,6 @@
"integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==",
"devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"playwright": "1.62.0"
},
@@ -2861,9 +2861,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2881,9 +2878,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2901,9 +2895,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2921,9 +2912,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2941,9 +2929,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2961,9 +2946,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3883,6 +3865,20 @@
}
}
},
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.6",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz",
+ "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
"node_modules/@trezor/analytics": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.5.0.tgz",
@@ -6890,9 +6886,15 @@
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "win32"
- ]
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
},
"node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
"version": "1.12.2",
@@ -7025,129 +7027,6 @@
"url": "https://opencollective.com/vitest"
}
},
- "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
- "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz",
- "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "4.1.10",
- "@vitest/utils": "4.1.10",
- "magic-string": "^0.30.21",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/spy": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
- "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
- "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@vitest/expect": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
- "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@standard-schema/spec": "^1.1.0",
- "@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.10",
- "@vitest/utils": "4.1.10",
- "chai": "^6.2.2",
- "tinyrainbow": "^3.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/mocker": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
- "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "4.1.10",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.21"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
- "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyrainbow": "^3.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
- "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "4.1.10",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
"node_modules/@vitest/snapshot": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
@@ -13554,35 +13433,6 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/playwright": {
- "version": "1.62.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz",
- "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "playwright-core": "1.62.0"
- },
- "bin": {
- "playwright": "cli.js"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/playwright-core": {
- "version": "1.62.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz",
- "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==",
- "devOptional": true,
- "license": "Apache-2.0",
- "bin": {
- "playwright-core": "cli.js"
- },
- "engines": {
- "node": ">=20"
- }
- },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -13689,6 +13539,35 @@
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
"license": "MIT"
},
+ "node_modules/playwright": {
+ "version": "1.62.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz",
+ "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.62.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.62.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz",
+ "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
@@ -14491,19 +14370,6 @@
"node": ">=v12.22.7"
}
},
- "node_modules/saxes": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
- "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "xmlchars": "^2.2.0"
- },
- "engines": {
- "node": ">=v12.22.7"
- }
- },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",