diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml
index a63e141..c113fd0 100644
--- a/.github/workflows/frontend.yml
+++ b/.github/workflows/frontend.yml
@@ -15,8 +15,14 @@ on:
jobs:
check:
- name: Next.js checks
+ name: Next.js checks (${{ matrix.deployment }})
runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ deployment:
+ - riseTestnet
+ - somniaShannon
permissions:
contents: read
defaults:
@@ -43,5 +49,7 @@ jobs:
- name: Build frontend
env:
+ NEXT_PUBLIC_DEPLOYMENT: ${{ matrix.deployment }}
NEXT_PUBLIC_RISE_TESTNET_DUNGEON_ADDRESS: "0x0000000000000000000000000000000000000001"
+ NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS: "0x07c5D071132ae95C3708031790b3feC740F4c292"
run: npm run build
diff --git a/README.md b/README.md
index b09db49..e1b3ca1 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ Practice Mode is not onchain, does not persist authoritative state and does not
Onchain Mode connects a wallet to a configured deployment. Player state and game actions are handled by the deployed `Delveworn` contract, and randomness-backed actions resolve through the configured provider and callback adapter.
-Onchain Mode depends on the selected network, RPC availability, wallet confirmations and the deployed contract address. The current public deployment targets RISE Testnet.
+Onchain Mode depends on the selected network, RPC availability, wallet confirmations and the deployed contract address. The current public deployment remains on RISE Testnet; Somnia Shannon is an independently selectable deployment and does not replace the grant-linked RISE experience.
## Project overview
@@ -93,7 +93,7 @@ The frontend uses `frontendSnapshotV3()`, `claimRelic(bool)` and `equipOwnedReli
| Environment | Status | Scope |
| --- | --- | --- |
| RISE Testnet | Public beta | Current wallet-connected deployment and frontend integration. |
-| Somnia Shannon Testnet | Adapter and deployment path test-covered | Native verifiable VRF requests use Somnia's coordinator-funded Reactivity/drand flow. No public Delveworn deployment is advertised until its address and live fulfillment are verified. |
+| Somnia Shannon Testnet | Verified opt-in deployment | Delveworn [`0x07c5…c292`](https://shannon-explorer.somnia.network/address/0x07c5D071132ae95C3708031790b3feC740F4c292) uses a native VRF adapter and Somnia's coordinator-funded Reactivity/drand flow. A live monster request completed with `callbackSuccess=true`; the public frontend remains on RISE unless explicitly configured otherwise. Somnia supports ERC-4337-style smart accounts and session keys, but this Delveworn configuration currently exposes only its tested MetaMask transaction path. |
| Local Anvil | Development only | Deterministic contract, relic, balance and request/callback testing through `DevRandomnessAdapter`. |
| Chainlink VRF v2.5 adapter | Implemented and test-covered | Adapter support exists, but no public deployment is presented as production-ready. |
| Other EVM networks | Architecture target | The core is designed for adapter-based deployments; these networks are not yet advertised as supported public deployments. |
@@ -241,6 +241,15 @@ At minimum, the selected public deployment needs its contract address. RISE Test
NEXT_PUBLIC_RISE_TESTNET_DUNGEON_ADDRESS
```
+Somnia Shannon is opt-in and uses:
+
+```text
+NEXT_PUBLIC_DEPLOYMENT=somniaShannon
+NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS=0x07c5D071132ae95C3708031790b3feC740F4c292
+```
+
+Do not change the production deployment selector merely by merging Somnia support; the RISE URL is referenced by existing grant applications and remains the default.
+
Changes must pass the path-filtered contract and frontend workflows before they are merged to `main`.
## Roadmap
diff --git a/frontend/.env.example b/frontend/.env.example
index 77eacd1..4f0bdf2 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -8,11 +8,9 @@ NEXT_PUBLIC_RISE_TESTNET_RPC_URL=https://testnet.riselabs.xyz
NEXT_PUBLIC_RISE_TESTNET_WS_URL=wss://testnet.riselabs.xyz/ws
NEXT_PUBLIC_RISE_TESTNET_EXPLORER_URL=https://explorer.testnet.riselabs.xyz
-# Somnia Shannon benchmark candidate.
-# This deployment is registered but intentionally not selectable yet.
-# Set the address after deploying the benchmark/game contracts; enabling it
-# still requires changing selectable=true in chain-config.ts after validation.
-# NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS=0x0000000000000000000000000000000000000000
+# Somnia Shannon deployment (opt in with NEXT_PUBLIC_DEPLOYMENT=somniaShannon).
+# The public RISE deployment remains the default.
+NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS=0x07c5D071132ae95C3708031790b3feC740F4c292
NEXT_PUBLIC_SOMNIA_SHANNON_RPC_URL=https://dream-rpc.somnia.network/
NEXT_PUBLIC_SOMNIA_SHANNON_EXPLORER_URL=https://shannon-explorer.somnia.network/
diff --git a/frontend/README.md b/frontend/README.md
index 2d1726d..c1ed216 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -1,6 +1,6 @@
# Delveworn
-Delveworn is a fully onchain dungeon crawler. The current public frontend deployment targets RISE Testnet and supports RISE Wallet session keys for fast, popup-free gameplay, plus MetaMask standard transactions.
+Delveworn is a fully onchain dungeon crawler. The public frontend remains on RISE Testnet and supports RISE Wallet session keys for fast, popup-free gameplay, plus MetaMask standard transactions. A separately selectable Somnia Shannon configuration uses MetaMask and Somnia Native VRF without changing the public RISE experience.
## Status
@@ -31,6 +31,13 @@ Create `.env.local` and point the frontend at the deployed Delveworn contract:
NEXT_PUBLIC_RISE_TESTNET_DUNGEON_ADDRESS=0x...
```
+To run the separately verified Somnia Shannon deployment instead:
+
+```bash
+NEXT_PUBLIC_DEPLOYMENT=somniaShannon
+NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS=0x07c5D071132ae95C3708031790b3feC740F4c292
+```
+
`NEXT_PUBLIC_DUNGEON_ADDRESS` remains supported as a legacy fallback.
Start the development server:
@@ -72,4 +79,4 @@ The repository includes `scripts/vrf-latency-monitor.mjs` for measuring request/
## Network
-The current frontend targets RISE Testnet. Product naming is chain-independent; contract addresses and network configuration should be selected per deployment before any mainnet release.
+The public frontend targets RISE Testnet. Somnia Shannon is available as an opt-in build configuration using chain ID `50312`, the official Dream RPC and Somnia Native VRF. Somnia supports ERC-4337-style smart accounts and session keys at the network/tooling layer, but Delveworn's Somnia configuration currently uses MetaMask standard transactions until a specific account-abstraction provider and smart-account flow are integrated and tested. Product naming remains chain-independent; contract addresses and network configuration must be selected per deployment before any mainnet release.
diff --git a/frontend/app/chain-config.ts b/frontend/app/chain-config.ts
index db6a29d..c5d693a 100644
--- a/frontend/app/chain-config.ts
+++ b/frontend/app/chain-config.ts
@@ -12,6 +12,7 @@ export type InstantPlayProvider =
export type RandomnessProvider =
| "rise-vrf"
+ | "somnia-native-vrf"
| "chainlink-vrf"
| "supra-dvrf"
| "other";
@@ -80,12 +81,6 @@ function validateDeploymentConfig(config: DeploymentConfig) {
throw new Error(`Deployment ${config.key} must provide an ecosystem name.`);
}
- if (config.selectable && !config.dungeonAddress) {
- throw new Error(
- `Deployment ${config.key} is selectable but has no dungeon contract address.`
- );
- }
-
if (config.realtime.websocket && !config.wsUrl) {
throw new Error(
`Deployment ${config.key} enables WebSocket realtime but has no wsUrl.`
@@ -161,8 +156,7 @@ export const deployments = {
ecosystemName: "RISE",
selectable: true,
chain: riseTestnet,
- dungeonAddress: requiredAddress(
- "NEXT_PUBLIC_RISE_TESTNET_DUNGEON_ADDRESS or NEXT_PUBLIC_DUNGEON_ADDRESS",
+ dungeonAddress: optionalAddress(
process.env.NEXT_PUBLIC_RISE_TESTNET_DUNGEON_ADDRESS ??
process.env.NEXT_PUBLIC_DUNGEON_ADDRESS
),
@@ -207,10 +201,12 @@ export const deployments = {
key: "somnia-shannon",
label: "Somnia Shannon Testnet",
ecosystemName: "Somnia",
- selectable: false,
+ selectable: true,
chain: somniaShannon,
- dungeonAddress: optionalAddress(
- process.env.NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS
+ dungeonAddress: requiredAddress(
+ "NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS",
+ process.env.NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS ??
+ "0x07c5D071132ae95C3708031790b3feC740F4c292"
),
rpcUrl:
process.env.NEXT_PUBLIC_SOMNIA_SHANNON_RPC_URL ??
@@ -230,8 +226,8 @@ export const deployments = {
shreds: false,
},
randomness: {
- provider: "chainlink-vrf",
- providerLabel: "Chainlink VRF v2.5 (direct-funding adapter)",
+ provider: "somnia-native-vrf",
+ providerLabel: "Somnia Native VRF",
retrySupported: true,
},
timing: {
@@ -256,10 +252,16 @@ if (!(configuredDeployment in deployments)) {
const selectedDeployment = deployments[configuredDeployment];
-if (!selectedDeployment.selectable || !selectedDeployment.dungeonAddress) {
+if (!selectedDeployment.selectable) {
throw new Error(
`Deployment ${selectedDeployment.key} is registered for benchmarking but is not selectable yet.`
);
}
+if (!selectedDeployment.dungeonAddress) {
+ throw new Error(
+ `Deployment ${selectedDeployment.key} is selected but has no dungeon contract address.`
+ );
+}
+
export const activeDeployment = selectedDeployment as ActiveDeploymentConfig;
diff --git a/frontend/app/game-ui.tsx b/frontend/app/game-ui.tsx
index 2e07441..67273f9 100644
--- a/frontend/app/game-ui.tsx
+++ b/frontend/app/game-ui.tsx
@@ -454,7 +454,7 @@ export function DungeonEntry({
]
: [
"Wallet or temporary session authorization.",
- "RISE Fast VRF provides verifiable randomness.",
+ "The configured VRF provider supplies verifiable randomness.",
"Game state and rewards are stored by the Delveworn contract.",
];
diff --git a/frontend/app/page-runtime-bindings.ts b/frontend/app/page-runtime-bindings.ts
index 763ff66..10378d0 100644
--- a/frontend/app/page-runtime-bindings.ts
+++ b/frontend/app/page-runtime-bindings.ts
@@ -31,6 +31,9 @@ import {
export const DUNGEON_ADDRESS =
activeDeployment.dungeonAddress;
+export const DUNGEON_EXPLORER_URL =
+ `${activeDeployment.explorerUrl.replace(/\/$/, "")}/address/${DUNGEON_ADDRESS}`;
+
export const RPC_URL =
activeDeployment.rpcUrl;
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index a7fd0d0..553c6f9 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -2,33 +2,25 @@
import { useEffect, useRef, useState } from "react";
import {
- createPublicClient,
createWalletClient,
custom,
decodeEventLog,
encodeFunctionData,
getAddress,
- http,
keccak256,
toHex,
- webSocket,
type Address,
} from "viem";
-import { riseTestnet } from "viem/chains";
-import { shredActions } from "shreds/viem";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
WagmiProvider,
- createConfig,
useChainId,
useConnect,
useConnection,
useDisconnect,
useConnectors,
} from "wagmi";
-import { metaMask } from "wagmi/connectors/metaMask";
-import { Chains, RiseWallet } from "rise-wallet";
-import { Hooks, riseWallet } from "rise-wallet/wagmi";
+import { Hooks } from "rise-wallet/wagmi";
import { P256, PublicKey, Signature } from "ox";
import {
describeRelicEquipImpact,
@@ -51,6 +43,38 @@ import {
RoomProgressLine,
SmallStat,
} from "./game-ui";
+import { createRiseShredsClient } from "./rise-shreds-client";
+import {
+ ACTION_READY_POLL_MS,
+ ACTION_READY_TIMEOUT_MS,
+ ACTIVE_CHAIN,
+ ACTIVE_CHAIN_ID,
+ ACTIVE_ECOSYSTEM_NAME,
+ ACTIVE_NETWORK_LABEL,
+ DUNGEON_ADDRESS,
+ DUNGEON_EXPLORER_URL,
+ METAMASK_CONNECTOR_ID,
+ METAMASK_CONNECTOR_NAME,
+ MIN_VRF_DISPLAY_MS,
+ RISE_WALLET_CONNECTOR_ID,
+ SESSION_DURATION_SECONDS,
+ TESTNET_POLLING_MS,
+ VRF_CANONICAL_FALLBACK_MS,
+ VRF_DELAYED_NOTICE_MS,
+ activeRandomnessProviderLabel,
+ connectedNetworkLabel,
+ delayedRandomnessText,
+ delayedRandomnessTitle,
+ ensureActiveChain,
+ isMetaMaskConnector,
+ isRiseWalletConnector,
+ publicClient,
+ runtimeFooterLabel,
+ sessionStorageKey,
+ supportsInstantPlay,
+ supportsRandomnessRetry,
+ wagmiConfig,
+} from "./page-runtime-bindings";
/*
============================================================
@@ -59,90 +83,11 @@ import {
============================================================
*/
-const CONFIGURED_DUNGEON_ADDRESS =
- process.env.NEXT_PUBLIC_RISE_TESTNET_DUNGEON_ADDRESS ??
- process.env.NEXT_PUBLIC_DUNGEON_ADDRESS;
-
-if (!CONFIGURED_DUNGEON_ADDRESS) {
- throw new Error(
- "NEXT_PUBLIC_RISE_TESTNET_DUNGEON_ADDRESS or NEXT_PUBLIC_DUNGEON_ADDRESS is not set. Add the deployed Delveworn address to frontend/.env.local and restart Next.js."
- );
-}
-
-const DUNGEON_ADDRESS =
- getAddress(
- CONFIGURED_DUNGEON_ADDRESS
- );
-
-const DUNGEON_EXPLORER_URL =
- `https://explorer.testnet.riselabs.xyz/address/${DUNGEON_ADDRESS}`;
-
-const RPC_URL =
- "https://testnet.riselabs.xyz";
-
-const WS_URL =
- "wss://testnet.riselabs.xyz/ws";
-
-const TESTNET_POLLING_MS = 50;
-const MIN_VRF_DISPLAY_MS = 300;
-const VRF_DELAYED_NOTICE_MS = 5_000;
-const VRF_CANONICAL_FALLBACK_MS = 20_000;
-const ACTION_READY_TIMEOUT_MS = 45_000;
-const ACTION_READY_POLL_MS = 250;
const FRONTEND_SNAPSHOT_V3_RETRY_MS = 5_000;
-const SESSION_DURATION_SECONDS = 8 * 60 * 60;
const SESSION_STATUS_TIMEOUT_MS = 12_000;
const SESSION_STATUS_POLL_MS = 40;
-const RISE_WALLET_CONNECTOR_ID =
- "com.risechain.wallet";
-
-const METAMASK_CONNECTOR_ID =
- "metaMaskSDK";
-
-const METAMASK_CONNECTOR_NAME =
- "MetaMask";
-
-const riseWalletConnector =
- riseWallet(
- RiseWallet.defaultConfig
- );
-
-const metaMaskConnector =
- metaMask({
- dapp: {
- name:
- "RISE Dungeon",
- url:
- typeof window ===
- "undefined"
- ? "https://rise-dungeon-frontend.vercel.app"
- : window.location.origin,
- },
- });
-
-const wagmiConfig =
- createConfig({
- ssr: true,
-
- chains: [
- Chains.riseTestnet,
- ],
-
- connectors: [
- riseWalletConnector,
- metaMaskConnector,
- ],
-
- transports: {
- [Chains.riseTestnet.id]:
- http(
- RPC_URL
- ),
- },
- });
-
/*
============================================================
BALANCE CONSTANTS
@@ -205,90 +150,6 @@ function asInjectedProvider(
InjectedProvider;
}
-function getErrorCode(
- error: unknown
-) {
- if (
- typeof error ===
- "object" &&
- error !== null &&
- "code" in error
- ) {
- return Number(
- (
- error as {
- code: unknown;
- }
- ).code
- );
- }
-
- return undefined;
-}
-
-async function ensureRiseTestnet(
- provider:
- InjectedProvider
-) {
- const chainId =
- `0x${riseTestnet.id.toString(16)}`;
-
- try {
- await provider.request({
- method:
- "wallet_switchEthereumChain",
-
- params: [
- {
- chainId,
- },
- ],
- });
- } catch (
- error
- ) {
- if (
- getErrorCode(error) !==
- 4902
- ) {
- throw error;
- }
-
- await provider.request({
- method:
- "wallet_addEthereumChain",
-
- params: [
- {
- chainId,
-
- chainName:
- "RISE Testnet",
-
- nativeCurrency: {
- name:
- "Ether",
-
- symbol:
- "ETH",
-
- decimals:
- 18,
- },
-
- rpcUrls: [
- RPC_URL,
- ],
-
- blockExplorerUrls: [
- "https://explorer.testnet.riselabs.xyz",
- ],
- },
- ],
- });
- }
-}
-
async function createConnectedWalletClient(
account:
Address,
@@ -296,7 +157,7 @@ async function createConnectedWalletClient(
provider:
InjectedProvider
) {
- await ensureRiseTestnet(
+ await ensureActiveChain(
provider
);
@@ -304,7 +165,7 @@ async function createConnectedWalletClient(
account,
chain:
- riseTestnet,
+ ACTIVE_CHAIN,
transport:
custom(
@@ -313,49 +174,8 @@ async function createConnectedWalletClient(
});
}
-const publicClient =
- createPublicClient({
- chain:
- riseTestnet,
-
- pollingInterval:
- TESTNET_POLLING_MS,
-
- cacheTime: 0,
-
- transport: http(
- RPC_URL
- ),
- });
-
const realtimeClient =
- createPublicClient({
- chain:
- riseTestnet,
-
- cacheTime: 0,
-
- transport:
- webSocket(
- WS_URL,
- {
- keepAlive: {
- interval: 5_000,
- },
-
- reconnect: {
- attempts: 100,
- delay: 500,
- },
-
- retryCount: 5,
- retryDelay: 100,
- timeout: 15_000,
- }
- ),
- }).extend(
- shredActions
- );
+ createRiseShredsClient();
/*
============================================================
@@ -962,9 +782,6 @@ type SessionAction =
| PendingAction
| RecoveryAction;
-const SESSION_PERMISSION_VERSION =
- "4";
-
const SESSION_FUNCTION_SIGNATURES = [
"startGame()",
"attack()",
@@ -996,14 +813,6 @@ function selectorFor(
) as `0x${string}`;
}
-function sessionStorageKey(
- address: Address
-) {
- return (
- `rise_dungeon_${DUNGEON_ADDRESS.toLowerCase()}_${address.toLowerCase()}_session_v${SESSION_PERMISSION_VERSION}_key`
- );
-}
-
/*
============================================================
MONSTER PERSONALITIES
@@ -2099,7 +1908,7 @@ async function fetchPlayerState(
const reader =
source ===
"realtime"
- ? realtimeClient
+ ? realtimeClient ?? publicClient
: publicClient;
let snapshot: unknown;
@@ -2976,7 +2785,7 @@ function getLootMessage(
============================================================
*/
-function RiseProviders({
+function ChainProviders({
children,
}: {
children:
@@ -3010,9 +2819,9 @@ function RiseProviders({
export default function Home() {
return (
-
MetaMask confirms each action. RISE Wallet enables popup-free play after one session approval.
++ {supportsInstantPlay() + ? "MetaMask confirms each action. RISE Wallet enables popup-free play after one session approval." + : "MetaMask confirms each onchain action."} +
- LIVE ON RISE TESTNET + LIVE ON {ACTIVE_NETWORK_LABEL.toUpperCase()}
- RISE Testnet · {connectedAddress.slice(0, 6)}…{connectedAddress.slice(-4)} + {connectedNetworkLabel(connectedAddress)}