CONNECTION BOUNDARY
Pressing Connect reveals one public address
When a supported external wallet is installed, NiceChunk calls that wallet provider's connect method. If the player approves, the provider returns a public key. The page then tries to check or switch the wallet toward the client's required Devnet cluster and stores the address, wallet name, and a binding time in this browser. Those local fields let the site remember which public player records to look up.
That path does not request a private key, sign a message, sign a transaction, create a chain account, create a character, or prove that the same person still controls the wallet tomorrow. The current desktop mock audit supplies a public key, simple connect and Devnet network stubs, and a deterministic null read for the public Appearance account, but no private key, message signer, or transaction signer; the interface still advances to character creation. NiceChunk's normal external-wallet buttons therefore represent a local connection state, not a server-authenticated login session.
It stores those values plus a local binding timestamp so later pages can select the same public identity.
The reviewed NiceChunk code requests provider operations and a public key; it does not request an export of the external wallet's private key. This does not certify the wallet extension itself.
Guardian does not authenticate this login, and rebuilding terrain says nothing about who controls a wallet.
APPROVAL BOUNDARY
Connect, sign a message, and sign a transaction are separate choices
The current Phantom mobile helper creates readable challenge text containing the site domain, origin, return path, a random nonce, and an issue time. Inside the wallet browser, it asks Phantom to sign those exact message bytes. Back on the page, TweetNaCl verifies that signature against the public key and then stores the same browser binding. No transaction is built, no fee is paid, and no Solana account changes. This message proof is not used by every desktop, Solflare, or Backpack entry path.
A later on-chain action is another decision. For a supported transaction, the client prepares a transaction and asks the provider for a separate approval. NiceChunk either sends the signed bytes itself or uses the provider's sign-and-send method, then polls its configured RPC for confirmed. A player can reject that request even after connecting. A returned signature is not yet execution success, and confirmed is not renamed finalized. A Gameplay Session can reduce repeated prompts only after its own owner-approved chain transaction; that narrower temporary-authority system has a separate guide.
No message or transaction signature is implied by this step.
The current helper verifies the signed message bytes and separately checks the proof's domain and origin fields in this browser. It does not submit a transaction, but the current verifier does not enforce one-time use or fully reconstruct the challenge text from those fields.
The request must still be sent and accepted; the current client waits for confirmed, not finalized.
PLAYER CHECKLIST
Pause and inspect the exact request before approving
First check the real site origin. Then identify the request type: connect, sign a readable message, or sign a transaction. For a message, read every line and cancel if the domain, purpose, nonce context, or return path is unexpected. For a transaction, review every detail the wallet displays, including the network, requested action, program, accounts, amounts, transfers, and estimated fee when available. If the wallet does not show enough to understand the request, reject it and use a trusted transaction decoder or explorer before trying again.
NiceChunk currently targets Solana Devnet, but its wallet-network check is best effort. It infers a cluster from provider properties or RPC-looking values and tries several wallet switch methods. When the provider reports no detectable network and automatic switching is unsupported, the code can continue with unknown and unverified flags. The project also supports a local HTTPS RPC override and does not currently compare a genesis hash. A network-ready label is therefore useful interface guidance, not cryptographic proof that every wallet and RPC request reached the independently verified cluster you intended.
It asks for the selected public address and connection permission.
It still deserves careful reading because a signature can be reused as evidence wherever a verifier accepts that exact message.
Review every detail the wallet displays. If the action, accounts, transfers, fee payer, amounts, or network are not understandable, reject and inspect the request with a trusted decoder.
The current code has no genesis-hash check and can explicitly mark an unknown provider as unverified.
- Check where the request came from
Use the official site and read the origin shown by the wallet. A copied design or familiar logo is not enough.
- Name the request type
Connect should expose an address; signMessage signs exact bytes; a transaction can change accounts or spend funds.
- Read the payload and consequences
Review every detail the wallet displays. If the action, program, accounts, transfers, amounts, or fees are hidden or unclear, reject the request and inspect it with a trusted decoder.
- Verify the network independently when it matters
The client targets Devnet, but an unknown provider can pass an unverified branch. Confirm the wallet network and inspect the resulting signature with a trusted Devnet explorer.
- Reject anything unexpected
Rejecting an unexpected request avoids approving that request. Reopen the action from the official game only after you understand why the request appeared.
SECRET AND LOGOUT BOUNDARY
Never share signing secrets, and know what Disconnect cannot undo
Never send a recovery phrase, private key, secret-key JSON array, wallet password, or copied authentication secret to a person, chat, support account, form, or ordinary external-wallet connection flow. A public address is designed to be visible, but it links public activity and balances, so sharing it still has privacy consequences. Do not forward an unexpected signed login proof either; cancel the flow and start again from the official site.
The login page also offers a separate built-in NiceChunk Game Wallet that can create or import a browser-held private key. That is not an external-wallet connection. Never import a main wallet there; use only a dedicated low-balance game wallet if you choose that path. The next guide explains its Base58 encoding, browser storage, backup, loss, and same-origin script risks instead of pretending the two wallet models are interchangeable.
Disconnect is mostly local cleanup. On the login page it tries to disconnect providers and clears the connection fields. Play logout also clears the username and wallet-scoped runtime caches before returning to login. Provider disconnect is best effort. Neither path reverses completed transactions, erases public history, closes PDAs, immediately revokes an existing on-chain Gameplay Session, returns that Session key's remaining SOL, or deletes the separately stored built-in Game Wallet secret.
Anyone with a usable signing secret may be able to act as that wallet.
The exact cleanup differs between the login page and Play logout, and provider disconnect can fail.
History, PDAs, existing Session authority until its chain rules stop it, and the built-in Game Wallet record remain separate concerns.
- If a prompt asks for a recovery phrase or external-wallet private key
Stop. Do not paste it. Close the page and reopen the official site from a trusted bookmark or typed address.
- If you approved only an unfamiliar connection
Disconnect in the site and review the wallet's connected-app list. Remember that this does not undo a transaction you separately signed.
- If you signed something you do not understand
Record the exact message or transaction signature, inspect it with a trusted tool, and move valuable assets if the request granted dangerous authority.
- If a recovery phrase or private key was exposed
Treat that wallet as compromised. From a trusted device and wallet app, create a fresh wallet and move remaining assets rather than relying on Disconnect.
READ THE REQUEST
Five excerpts keep connection, proof, network guidance, transaction approval, and logout separate
The formula is a player safety rule, not a cryptographic protocol. The excerpts show the exact external connect path, the mobile message signature, a later transaction signature and confirmed wait, the unverified network branch, and the local logout behavior.
One wallet does not turn three requests into one permission
Connect(address) ≠ SignMessage(exact bytes) ≠ SignTransaction(exact instructions, accounts, fee payer)The symbol ≠ means 'is not the same operation as.' Connect lets the page learn a public address. SignMessage proves that the wallet signed one exact byte sequence but does not submit a Solana transaction. SignTransaction approves one prepared transaction, which must still be sent and accepted. None of the three asks an external wallet to reveal its private key, and none silently proves the success of a later stage.
- Connect(address)
- The provider returns the selected public address after the player accepts a connection request.
- SignMessage(exact bytes)
- The provider signs one exact off-chain message. Verification proves only that key and message relationship under the verifier's checks.
- SignTransaction(...)
- The provider signs exact transaction bytes with instructions, accounts, a fee payer, and a recent blockhash; submission and confirmation remain later observations.
- ≠
- Approving one operation does not pre-approve either of the other operations.
The ordinary external-wallet path receives a public key
JavaScriptlogin/login.jsasync function connectWallet(wallet) {
selectedWallet = wallet;
statusLine.textContent = t("login.status.connecting", { wallet: wallet.name });
try {
const result = await wallet.provider.connect();
const publicKey = result?.publicKey || wallet.provider.publicKey;
if (!publicKey) throw new Error(t("login.status.publicKeyMissing"));
await ensureLoginWalletNetwork(wallet.provider);
walletAddress = publicKey.toString();
persistConnectedWallet({ walletAddress, walletName: wallet.name });
await initialize();
} catch (error) {
statusLine.textContent = loginNetworkErrorMessage(error) || error?.message || t("login.status.connectFailed");
}
}
The page asks the installed provider to connect, requires a returned public key, runs its separate network check, and stores a browser binding before continuing. There is no signMessage or transaction call in this function. The catch path displays a failure rather than inventing a connected address.
- 1-3
Remember which wallet button was chosen and tell the player that the provider connection is starting.
- 5-8
Ask the wallet to connect, then accept only a result that exposes a public key.
- 10-13
Run the network check, convert the public key to an address string, store the local binding, and refresh the login state.
- 14-17
If connection or the network step fails, show the error and do not advance this function as a successful connection.
The Phantom mobile helper asks for a separate message signature
JavaScriptlogin/login.js const connectResult = await provider.connect();
const publicKey = publicKeyToString(connectResult?.publicKey || provider.publicKey);
if (!publicKey) throw new Error("Missing wallet public key.");
await ensureLoginWalletNetwork(provider);
const encodedMessage = new TextEncoder().encode(challenge.message);
const signResult = await provider.signMessage(encodedMessage, "utf8");
const signature = signatureToBytes(signResult);
if (!signature?.length) throw new Error("Missing wallet signature.");
const bs58 = await loadBs58();
const proof = {
version: 1,
walletName: "Phantom",
publicKey,
signature: bs58.encode(signature),
challenge,
};
This is a different path inside the Phantom wallet browser. It connects, checks the network, encodes the exact challenge message, and explicitly calls signMessage. The resulting local proof contains the public key, signature, and challenge; it contains no private key and creates no Solana transaction. The browser later verifies it with TweetNaCl, subject to the replay and challenge-binding limitations described above.
- 1-4
Connect inside the wallet browser, obtain the public address, and run the same best-effort network check.
- 6-9
Turn the exact challenge text into bytes, ask for a message signature, and reject an empty result.
- 11-17
Package only the proof version, wallet label, public key, encoded signature, and original challenge for local verification.
A later transaction has its own signature and confirmation wait
JavaScriptsrc/chain/nicechunkChain.js for (const signer of extraSigners) transaction.partialSign(signer);
if (typeof provider.signTransaction === "function") {
const signed = await provider.signTransaction(transaction);
const signature = await sendRawTransactionWithLogs(conn, signed.serialize(), "wallet");
await confirmTransactionByHttpPolling(conn, {
signature,
blockhash: transaction.recentBlockhash,
lastValidBlockHeight: transaction.lastValidBlockHeight,
}, "confirmed");
return signature;
}
This branch runs only after a transaction has been prepared with a fee payer and recent blockhash above the excerpt. Extra signers sign first, then the external provider gets a separate signTransaction request. NiceChunk sends those signed bytes and polls for confirmed before returning the signature. A connection approval does not execute these lines, and this function does not wait for finalized.
- 1
Apply any additional signatures the exact instruction requires; this does not replace the wallet owner's signature.
- 3-5
If the wallet supports transaction signing, ask it to sign the prepared bytes and then send the serialized signed transaction.
- 6-10
Poll the returned signature with the prepared blockhash limits until the client's confirmed condition is satisfied or the routine fails.
- 11-12
Only after that wait does this branch return the signature to its caller.
An unknown wallet network can remain explicitly unverified
JavaScriptsrc/solanaNetwork.js onStatus?.({ type: "switching", requiredCluster, detectedCluster });
let switchResult = null;
try {
switchResult = await requestWalletSolanaClusterSwitch(provider, requiredCluster);
} catch (error) {
if (!detectedCluster && !isUserRejectedError(error)) {
onStatus?.({ type: "ready", requiredCluster, detectedCluster: "" });
return {
requiredCluster,
detectedCluster: "",
switched: false,
unknown: true,
unverified: true,
switchUnsupported: error?.code === "unsupported",
};
The client first reports that it is trying to switch. If no cluster could be detected and the switch attempt fails for a reason other than player rejection, this branch can report ready while returning unknown and unverified flags. That is a compatibility choice, not a genesis-hash check or proof that the provider and RPC are aligned.
- 1-4
Tell the interface a switch is being attempted, then call one of the provider-specific switch methods.
- 5-7
Enter the compatibility branch only when no cluster was detected and the failure was not an explicit player rejection.
- 8-15
Return the required cluster as guidance while honestly marking the detected cluster empty, not switched, unknown, and unverified.
Play logout clears local state and treats provider disconnect as best effort
JavaScriptplay/play-chain-session.js function disconnectWallet() {
const provider = detectWalletProvider();
try {
if (provider?.disconnect) {
Promise.resolve(provider.disconnect()).catch(() => {
// The local session is already cleared; plugin disconnect is best effort.
});
}
} catch {
// Some wallet plugins throw when already disconnected.
}
clearWalletSession();
state.walletAddress = "";
state.sessionLamports = getStoredSessionLamports("");
resetWalletBalance();
dispatchWalletChanged("");
globalThis.location?.replace?.(buildWalletLoginUrl({
redirectPath: `${globalThis.location?.pathname || "/play/"}${globalThis.location?.search || ""}${globalThis.location?.hash || ""}`,
autoConnect: false,
}));
Play asks an available provider to disconnect, but failures are deliberately ignored so local logout can continue. It then clears the wallet session, resets local wallet and balance presentation, announces the change, and returns to login. Nothing here submits a revoke or close instruction, deletes the separately stored built-in Game Wallet record, reverses a completed transaction, or returns funds from a temporary Session key.
- 1-10
Find the current provider and ask it to disconnect when possible, while treating rejection or an already-disconnected error as non-blocking.
- 12-16
Clear the browser wallet session and wallet-scoped display state, then notify other local components that no wallet is selected.
- 17-20
Return to the login route with a safe local path back to the current game page and without automatic reconnection.
IMPLEMENTATION EVIDENCE
Where these claims come from
Each claim is intentionally scoped to a concrete implementation path. These references are for verification, not decoration.
login/login.js
Detects external providers, separates ordinary connect from the Phantom mobile message helper, checks the configured network, stores the browser binding, and implements login-page disconnect.
login/index.html
Shows the current external-wallet choices and the separately labeled built-in Game Wallet creation and import warnings.
src/walletSession.js
Defines wallet binding fields, cleanup, safe same-site redirects, and an event-watcher helper that is not currently wired into the reviewed runtime.
src/solanaNetwork.js
Targets Devnet, infers a wallet cluster from provider properties, attempts multiple switch methods, and exposes the unknown unverified compatibility branch.
src/rpcConfig.js
Defines the default Devnet RPC choices and permits a locally stored HTTPS override without a genesis-hash validation step.
src/chain/nicechunkChain.js
Separately asks external wallets to approve prepared transactions, either sends signed bytes itself or delegates sign-and-send to the provider, and waits for confirmed; it also implements the distinct Gameplay Session system.
play/main.js
Requires a local wallet binding before Play boots and redirects to login when it is missing.
play/play-chain-session.js
Manages the Play wallet panel and treats provider disconnect as best effort during local logout.
play/play-auth-session.js
Clears identity and wallet-scoped runtime caches and rejects unsafe protocol-relative login return targets.
src/localGameWallet.js
Implements the separate browser-held Game Wallet record whose private-key storage, direct signing, backup, and deletion behavior are examined in the dedicated follow-up guide.
scripts/audit-wallet-flows.mjs
Checks the built no-wallet state and an injected mock-provider connect path with a deterministic missing-Appearance account response, without using a real extension, private key, message signer, or transaction signer.
docs/wallet-flow-audit.md
States exactly what the mock browser audit covers and excludes real extensions, mobile deep links, network switching, live RPC correctness, and transaction signing.
play/tests/play-auth-session.test.mjs
Tests binding requirements, wallet-scoped local cleanup, and rejection of an unsafe protocol-relative redirect target.