PLAYER, WALLET, AND CONTROL 01 · EXTERNAL WALLET SAFETY

Connecting shares an address, not the secret behind it

Connecting Phantom, Solflare, Backpack, or another injected Solana wallet lets NiceChunk request its public address and later ask for separate approvals. It does not give the site your private key, approve a message, approve a transaction, create a character, or make a chain write by itself.

14 min read
In a teaching scene generated from the canonical NiceChunk villagers and a real Chunk.js hillside reference, the girl greets the boy with an empty open hand while a closed wooden chest stays beside her.
The greeting represents sharing a public identity; the closed chest represents a signing secret that never changes hands. Both are teaching metaphors, not wallet objects or connection indicators in the game, and this image proves no approval or transaction.
PLAYER QUESTION What does connecting a wallet allow, and what should never be shared?
Learning path
Player, wallet, and control · Step 1 of 15
Audience
Players with no programming or blockchain background
Current network
Solana Devnet · client target with best-effort wallet detection
Reviewed
15 July 2026 · current working tree and local wallet-flow evidence
External wallet
A separate wallet app or browser extension such as Phantom, Solflare, or Backpack. It keeps its signing key and decides whether to approve each supported request.
Wallet provider
The interface an installed wallet exposes to a web page. NiceChunk asks this interface to connect, sign a message, or sign a transaction; those are different calls.
Public address
The public Solana identifier returned after connection. It is not a spending secret, but anyone who knows it can inspect and link its public activity.
Private key
Secret bytes that can create valid signatures for a wallet. Whoever obtains them may be able to control that wallet, so they must never be sent to another person or ordinary connection form.
Recovery phrase
A seed phrase that can restore wallet control. Treat it like the wallet itself: never share, photograph, upload, or type it into a site.
Connection request
A wallet prompt that lets a site learn the selected public address. It is not a message signature or a Solana transaction.
Message signature
A signature over exact bytes, usually readable text. It can prove control of a key for that message, but it does not submit a Solana transaction by itself.
Transaction signature
Approval of exact transaction bytes containing instructions and accounts. The signed request still has to be sent and accepted before shared state changes.
Origin and domain
The site address that requested an approval. Check it before connecting or signing so a look-alike site does not borrow your trust.
Network or cluster
The Solana environment a wallet or RPC is using. This client targets Devnet, but its wallet detection is not a cryptographic cluster proof.
Built-in Game Wallet
A separate browser-held key path offered on the login page. It is not the same security model as connecting an external wallet; the next guide explains its storage, signing, backup, deletion, and recovery limits.

Key points

A public address, not a private key

The normal external-wallet connection path receives the selected public address and records a local binding. The wallet keeps its signing secret.

Three requests, three decisions

Connecting, signing one login message, and signing one transaction use separate wallet operations. One approval does not silently approve the others.

Read first; never reveal secrets

Check the real site, request type, message or transaction details, network, amounts, and fees. Never send a recovery phrase, private key, secret-key JSON, or wallet password.

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.

What the page receives A public address and wallet label

It stores those values plus a local binding timestamp so later pages can select the same public identity.

What stays inside the wallet The external wallet's private key

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.

What is not involved Guardian and world reconstruction

Guardian does not authenticate this login, and rebuilding terrain says nothing about who controls a wallet.

In a teaching scene generated from the canonical villagers and real Chunk.js hillside reference, the girl and boy recognize each other with separated empty hands while the same closed chest remains behind the girl.
The two empty greetings represent recognition of a public address without an object transfer. The chest is only a private-key metaphor and is not a game item; connection alone does not sign a message, send a transaction, or prove continuing wallet control.

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.

Connect Reveal the selected public address

No message or transaction signature is implied by this step.

Sign message Sign exact off-chain bytes

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.

Sign transaction Approve exact instructions and accounts

The request must still be sent and accepted; the current client waits for confirmed, not finalized.

In a teaching scene generated from the canonical villagers and real Chunk.js hillside reference, the girl stands only on the first of three separated natural stone blocks while the boy waits beyond the third and the closed chest remains behind her.
The three unmarked stones are a teaching metaphor for three separate choices, not buttons or actual wallet states. Standing on the first does not automatically cross the next two, and the closed chest still represents a secret that was never handed over.

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.

Connect request No SOL spend by itself

It asks for the selected public address and connection permission.

Message request No chain write by itself

It still deserves careful reading because a signature can be reused as evidence wherever a verifier accepts that exact message.

Transaction request Can change state or move value

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.

Network label Best-effort detection

The current code has no genesis-hash check and can explicitly mark an unknown provider as unverified.

  1. 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.

  2. Name the request type

    Connect should expose an address; signMessage signs exact bytes; a transaction can change accounts or spend funds.

  3. 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.

  4. 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.

  5. 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.

In a teaching scene generated from the canonical villagers and real Chunk.js hillside reference, the girl studies one intact stone voxel while a pickaxe rests unused on the grass and the boy waits at a distance.
The intact stone and grounded pickaxe represent pausing before one exact action; they are not a wallet prompt or transaction preview. The real details must be read in the wallet, and nothing in this calm generated scene signals approval, Devnet verification, or success.

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.

Never disclose Recovery phrase, private key, secret JSON, or password

Anyone with a usable signing secret may be able to act as that wallet.

Disconnect changes Local binding and selected runtime caches

The exact cleanup differs between the login page and Play logout, and provider disconnect can fail.

Disconnect does not change Completed chain state and separately stored secrets

History, PDAs, existing Session authority until its chain rules stop it, and the built-in Game Wallet record remain separate concerns.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

In a teaching scene generated from the canonical villagers and real Chunk.js hillside reference, the girl walks away carrying the same closed chest while the boy, intact stone, and unused pickaxe remain behind.
The girl keeping the closed chest represents retaining a secret; the chest is not a game wallet item. The unchanged stone and landscape show the separate lesson that leaving or disconnecting does not erase completed shared history, revoke every authority, or delete separately stored keys.

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

JavaScript login/login.js
async 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. 1-3

    Remember which wallet button was chosen and tell the player that the provider connection is starting.

  2. 5-8

    Ask the wallet to connect, then accept only a result that exposes a public key.

  3. 10-13

    Run the network check, convert the public key to an address string, store the local binding, and refresh the login state.

  4. 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

JavaScript login/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. 1-4

    Connect inside the wallet browser, obtain the public address, and run the same best-effort network check.

  2. 6-9

    Turn the exact challenge text into bytes, ask for a message signature, and reject an empty result.

  3. 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

JavaScript src/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. 1

    Apply any additional signatures the exact instruction requires; this does not replace the wallet owner's signature.

  2. 3-5

    If the wallet supports transaction signing, ask it to sign the prepared bytes and then send the serialized signed transaction.

  3. 6-10

    Poll the returned signature with the prepared blockhash limits until the client's confirmed condition is satisfied or the routine fails.

  4. 11-12

    Only after that wait does this branch return the signature to its caller.

An unknown wallet network can remain explicitly unverified

JavaScript src/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. 1-4

    Tell the interface a switch is being attempted, then call one of the provider-specific switch methods.

  2. 5-7

    Enter the compatibility branch only when no cluster was detected and the failure was not an explicit player rejection.

  3. 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

JavaScript play/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. 1-10

    Find the current provider and ask it to disconnect when possible, while treating rejection or an already-disconnected error as non-blocking.

  2. 12-16

    Clear the browser wallet session and wallet-scoped display state, then notify other local components that no wallet is selected.

  3. 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.