Skip to content
Closed
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
6 changes: 3 additions & 3 deletions apps/web/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ export async function installCreationFixtures(
scenario: "success" | "wallet-rejection" | "simulation-failure" = "success",
) {
await page.addInitScript(
({ scenario, passphrase, wallet, expectedRecord }) => {
({ scenario, networkPassphrase, wallet, expectedRecord }) => {
const hash = "ab".repeat(32);
window.__STOLLA_E2E__ = {
wallet: {
address: wallet,
networkPassphrase: passphrase,
networkPassphrase: networkPassphrase,
rejected: scenario === "wallet-rejection",
},
communities: [],
Expand Down Expand Up @@ -140,7 +140,7 @@ export async function installCreationFixtures(
},
{
scenario,
passphrase: TESTNET_PASSPHRASE,
networkPassphrase: TESTNET_PASSPHRASE,
wallet: WALLET_ADDRESS,
expectedRecord: community(
"cc".repeat(32),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/__tests__/CommunityPage.loading.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ vi.mock("@/lib/contracts", () => ({
}));

vi.mock("@/lib/stellar", () => ({
contractIds: { nft: "CNFT", governor: "CGOV" },
capabilities: { legacyContracts: { available: true, nft: "CNFT", governor: "CGOV" }, activeNetwork: { explorerSegment: "testnet" } },
}));

vi.mock("@/app/(app)/community/community-data.mjs", async (importOriginal) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/__tests__/api.health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("GET /api/health", () => {
expect(response.headers.get("cache-control")).toContain("no-store");
expect(body).toEqual({
status: "ok",
network: { selected: "testnet", passphraseConfigured: true },
network: { selected: "testnet", networkPassphraseConfigured: true },
rpc: { configured: true },
contracts: {
nftConfigured: true,
Expand All @@ -70,7 +70,7 @@ describe("GET /api/health", () => {
expect(body.status).toBe("degraded");
expect(body.contracts).toEqual({
nftConfigured: false,
governorConfigured: true,
governorConfigured: false,
allConfigured: false,
});
});
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/(app)/communities/[id]/proposals/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function ScopedProposalHistory({ community }: { community: CommunityView }) {
try {
const client = createReadOnlyGovernorClient(governorContract);
const transaction = await client.proposal_state({
proposal_id: Uint8Array.from(Buffer.from(proposalId, "hex")),
proposal_id: Buffer.from(proposalId, "hex"),
});
setStates((current) => ({
...current,
Expand Down
10 changes: 4 additions & 6 deletions apps/web/src/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { config, contractIds, stellarConfig } from "@/lib/stellar";
import { config, contractIds } from "@/lib/stellar";

export const dynamic = "force-dynamic";

Expand All @@ -9,7 +9,7 @@ type HealthResponse = {
status: HealthStatus;
network: {
selected: "testnet" | "mainnet";
passphraseConfigured: boolean;
networkPassphraseConfigured: boolean;
};
rpc: {
configured: boolean;
Expand Down Expand Up @@ -40,17 +40,15 @@ function buildResponse(): { response: HealthResponse; statusCode: number } {
const passphraseConfigured = Boolean(config.networkPassphrase);

const rpcOk =
selected === "mainnet"
? rpcConfigured
: rpcConfigured || Boolean(stellarConfig.testnet.rpcUrl);
rpcConfigured;

const isReady = rpcOk && allContractsConfigured && passphraseConfigured;

const response: HealthResponse = {
status: isReady ? "ok" : "degraded",
network: {
selected,
passphraseConfigured,
networkPassphraseConfigured: passphraseConfigured,
},
rpc: {
configured: rpcConfigured,
Expand Down
10 changes: 7 additions & 3 deletions apps/web/src/components/community/CreateCommunityWizard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import { describeNetwork } from "@/lib/network";

const wallet = vi.hoisted(() => ({
address: "GADMIN" as string | null,
walletNetwork: null as ReturnType<typeof describeNetwork> | null,
walletNetwork: null as string | null,
walletNetworkPassphrase: null as string | null,
}));

vi.mock("@/context/WalletProvider", () => ({
useWallet: () => ({
address: wallet.address,
walletNetwork: wallet.walletNetwork,
walletNetworkPassphrase: wallet.walletNetworkPassphrase,
signTransaction: vi.fn(),
}),
}));
Expand All @@ -43,9 +45,10 @@ function createPort(): CommunityDeploymentPort {
};
}

function connectOn(passphrase: string) {
function connectOn(networkPassphrase: string) {
wallet.address = "GADMIN";
wallet.walletNetwork = describeNetwork(passphrase);
wallet.walletNetworkPassphrase = networkPassphrase;
wallet.walletNetwork = networkPassphrase === Networks.PUBLIC ? "mainnet" : "testnet";
}

function renderWizard(port: CommunityDeploymentPort) {
Expand Down Expand Up @@ -193,6 +196,7 @@ describe("initial mismatch", () => {

it("locks deployment while the wallet network is still unreadable", () => {
wallet.walletNetwork = null;
wallet.walletNetworkPassphrase = null;
renderWizard(createPort());

fillMetadata();
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/community/CreateCommunityWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ export function CreateCommunityWizard({
const hydratedRef = useRef(false);
const firstFieldRef = useRef<HTMLInputElement>(null);

const detectedPassphrase = comparison.detected?.passphrase ?? null;
const detectedPassphrase = comparison.detected?.networkPassphrase ?? null;

useEffect(() => {
dispatch({ type: "network-detected", passphrase: detectedPassphrase });
dispatch({ type: "network-detected", networkPassphrase: detectedPassphrase });
}, [detectedPassphrase]);

useEffect(() => {
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/hooks/useNetworkGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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";

/**
Expand All @@ -11,9 +11,9 @@ 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]);
}
22 changes: 22 additions & 0 deletions apps/web/src/lib/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Networks } from "@stellar/stellar-sdk";
import type { NetworkId, StellarNetwork } from "./network";

export type CapabilityAvailable<T> = { available: true } & T;
export type CapabilityUnavailable = { available: false; reason: string };
export type Capability<T> = CapabilityAvailable<T> | CapabilityUnavailable;

export type NetworkCapabilities = {
activeNetwork: StellarNetwork;
rpc: Capability<{ url: string; friendbotUrl: string | null }>;
explorer: Capability<{ segment: string }>;
communityFactory: Capability<{ contractId: string }>;
legacyContracts: Capability<{ nft: string; governor: string }>;
proposalDiscovery: Capability<{ startLedger: number }>;
};

export class CapabilityError extends Error {
constructor(capabilityName: string, reason: string) {
super(`Capability '${capabilityName}' is unavailable: ${reason}`);
this.name = "CapabilityError";
}
}
34 changes: 17 additions & 17 deletions apps/web/src/lib/community-creation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ const DRAFT: CommunityDraft = {
quorum: "1",
};

function simulationOn(passphrase: string): CommunitySimulation {
function simulationOn(networkPassphrase: string): CommunitySimulation {
return {
networkPassphrase: passphrase,
networkPassphrase: networkPassphrase,
factoryAddress: "CFACTORY",
transactionXdr: "AAAA",
minResourceFee: "1000",
Expand Down Expand Up @@ -57,7 +57,7 @@ beforeEach(() => {
onTestnet = apply(
INITIAL_CREATION_STATE,
{ type: "draft-changed", changes: DRAFT },
{ type: "network-detected", passphrase: Networks.TESTNET },
{ type: "network-detected", networkPassphrase: Networks.TESTNET },
{ type: "simulation-succeeded", simulation: simulationOn(Networks.TESTNET) },
);
});
Expand Down Expand Up @@ -96,7 +96,7 @@ describe("mid-flow network switch", () => {
it("discards the simulation and keeps draft values", () => {
const switched = creationReducer(onTestnet, {
type: "network-detected",
passphrase: Networks.PUBLIC,
networkPassphrase: Networks.PUBLIC,
});

expect(switched.simulation).toBeNull();
Expand All @@ -107,7 +107,7 @@ describe("mid-flow network switch", () => {
const signing = creationReducer(onTestnet, { type: "signing-started" });
const switched = creationReducer(signing, {
type: "network-detected",
passphrase: Networks.PUBLIC,
networkPassphrase: Networks.PUBLIC,
});

expect(switched.signing).toBe(false);
Expand All @@ -116,7 +116,7 @@ describe("mid-flow network switch", () => {
it("blocks deployment once the wallet has moved", () => {
const switched = creationReducer(onTestnet, {
type: "network-detected",
passphrase: Networks.PUBLIC,
networkPassphrase: Networks.PUBLIC,
});

expect(deploymentBlocker(switched, contextFor(Networks.PUBLIC))).toBe(
Expand All @@ -127,7 +127,7 @@ describe("mid-flow network switch", () => {
it("ignores a repeated report of the same network", () => {
const same = creationReducer(onTestnet, {
type: "network-detected",
passphrase: Networks.TESTNET,
networkPassphrase: Networks.TESTNET,
});

expect(same).toBe(onTestnet);
Expand All @@ -137,7 +137,7 @@ describe("mid-flow network switch", () => {
it("drops a simulation that resolves after the wallet moved", () => {
const switched = creationReducer(onTestnet, {
type: "network-detected",
passphrase: Networks.PUBLIC,
networkPassphrase: Networks.PUBLIC,
});
const late = creationReducer(switched, {
type: "simulation-succeeded",
Expand All @@ -152,8 +152,8 @@ describe("recovery to the expected network", () => {
it("requires a fresh simulation and preserves governance values", () => {
const recovered = apply(
onTestnet,
{ type: "network-detected", passphrase: Networks.PUBLIC },
{ type: "network-detected", passphrase: Networks.TESTNET },
{ type: "network-detected", networkPassphrase: Networks.PUBLIC },
{ type: "network-detected", networkPassphrase: Networks.TESTNET },
);
const context = contextFor(Networks.TESTNET);

Expand All @@ -166,8 +166,8 @@ describe("recovery to the expected network", () => {
it("allows deployment again once a new simulation exists", () => {
const resimulated = apply(
onTestnet,
{ type: "network-detected", passphrase: Networks.PUBLIC },
{ type: "network-detected", passphrase: Networks.TESTNET },
{ type: "network-detected", networkPassphrase: Networks.PUBLIC },
{ type: "network-detected", networkPassphrase: Networks.TESTNET },
{
type: "simulation-succeeded",
simulation: simulationOn(Networks.TESTNET),
Expand Down Expand Up @@ -208,7 +208,7 @@ describe("post-submission network changes", () => {
it("keeps the transaction and the network it was submitted to", () => {
const switched = creationReducer(submitted(), {
type: "network-detected",
passphrase: Networks.PUBLIC,
networkPassphrase: Networks.PUBLIC,
});

expect(switched.submission).toEqual({
Expand All @@ -222,8 +222,8 @@ describe("post-submission network changes", () => {
it("does not allow a second submission after switching back", () => {
const returned = apply(
submitted(),
{ type: "network-detected", passphrase: Networks.PUBLIC },
{ type: "network-detected", passphrase: Networks.TESTNET },
{ type: "network-detected", networkPassphrase: Networks.PUBLIC },
{ type: "network-detected", networkPassphrase: Networks.TESTNET },
);

expect(deploymentBlocker(returned, contextFor(Networks.TESTNET))).toBe(
Expand All @@ -237,7 +237,7 @@ describe("post-submission network changes", () => {
it("still records confirmation after a switch", () => {
const confirmed = apply(
submitted(),
{ type: "network-detected", passphrase: Networks.PUBLIC },
{ type: "network-detected", networkPassphrase: Networks.PUBLIC },
{ type: "submission-settled", status: "confirmed" },
);

Expand Down Expand Up @@ -309,7 +309,7 @@ describe("blocker precedence", () => {
};
const empty = apply(INITIAL_CREATION_STATE, {
type: "network-detected",
passphrase: Networks.PUBLIC,
networkPassphrase: Networks.PUBLIC,
});

expect(simulationBlocker(empty, context)).toBe("network-mismatch");
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/lib/community-creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export type CreationAction =
submission: CommunitySubmission | null;
}
| { type: "draft-discarded" }
| { type: "network-detected"; passphrase: string | null }
| { type: "network-detected"; networkPassphrase: string | null }
| { type: "simulation-succeeded"; simulation: CommunitySimulation }
| { type: "signing-started" }
| { type: "signing-ended" }
Expand Down Expand Up @@ -134,10 +134,10 @@ export function creationReducer(
* requires a fresh one.
*/
case "network-detected": {
if (state.detectedPassphrase === action.passphrase) return state;
if (state.detectedPassphrase === action.networkPassphrase) return state;
return {
...state,
detectedPassphrase: action.passphrase,
detectedPassphrase: action.networkPassphrase,
simulation: null,
signing: false,
};
Expand Down Expand Up @@ -294,7 +294,7 @@ function isSimulationStale(
): boolean {
return (
state.simulation !== null &&
state.simulation.networkPassphrase !== context.comparison.expected.passphrase
state.simulation.networkPassphrase !== context.comparison.expected.networkPassphrase
);
}

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/community-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ const DRAFT: CommunityDraft = {
quorum: "1",
};

function simulationOn(passphrase: string): CommunitySimulation {
function simulationOn(networkPassphrase: string): CommunitySimulation {
return {
networkPassphrase: passphrase,
networkPassphrase: networkPassphrase,
factoryAddress: "CFACTORY",
transactionXdr: "not-a-real-xdr",
minResourceFee: "1000",
Expand Down
Loading