STORAGE AND IMPORT BOUNDARY
Creation and import both end as one browser-held Base58 secret
Create generates a Solana Keypair. Import accepts a complete 64-byte secret as Base58 text, the same text wrapped as a JSON string, or a strict JSON array of exactly 64 byte integers. An advanced compatibility path accepts a 32-byte seed only as {"type":"nicechunk-game-wallet-seed","version":1,"seed":"<Base58-encoded 32-byte seed>"}; NiceChunk backups do not emit this record. Bare 32-byte values are rejected because a Solana public address has the same decoded length. Whichever route succeeds, NiceChunk writes the derived public address, the complete 64-byte secret encoded as Base58, the current millisecond timestamp, and a created-or-imported source label into four separate localStorage entries.
Those entries are plain origin storage, not an encrypted wallet file. Base58 changes the alphabet, not who can read the value. A same-origin script can request the secret record, and clearing this site's data can remove the only browser copy. The source label is editable browser data used for interface behavior, not cryptographic provenance. For an imported wallet, createdAt means when this browser stored it, not when that key first existed.
The key has no NiceChunk-only or Devnet-only flag. Solana derives the same address from the same secret on Devnet and Mainnet. Importing a main-wallet secret into this Devnet-targeted page therefore exposes the authority that same key has over its Mainnet address, tokens, accounts, and other permissions. Use a newly created dedicated Game Wallet and Devnet SOL only; do not send Mainnet SOL to this flow.
The importer now rejects input longer than 8,192 characters before parsing and validates JSON byte arrays before Uint8Array conversion: every item must be an integer from 0 through 255 and the complete secret must contain exactly 64 bytes. It accepts a JSON-quoted Base58 key but rejects generic secretKey or privateKey objects because other NiceChunk browser records use those names for temporary session or encryption material. Recovery phrases, hexadecimal, Base64, labels, internal whitespace, zero-width characters, OCR substitutions, and other unsupported characters are rejected rather than silently repaired. A different complete and internally consistent private key remains structurally valid, so the importer cannot know it was pasted by mistake; the displayed address must be compared with the backup address.
Create and Import call the same storage routine without checking for an existing record or asking permission to replace it. Four keys are written one after another rather than atomically. A second action can silently overwrite an unbacked wallet, and a storage failure between writes can leave a partial or mismatched record. Reading the record requires an address and secret but does not derive the address from the secret and compare them. Later provider construction derives its public key from the secret, so a corrupted stored address and stored secret can describe different wallets.
An explicitly typed 32-byte seed record is expanded to a Keypair first, so storage still receives the resulting 64-byte secret. Bare 32-byte values are rejected.
They are four independent localStorage values with no schema version, atomic commit, or authenticated relationship.
The parser rejects non-integers, values outside 0 through 255, nested or generic objects, and every bare length other than 64 before Keypair construction.
There is no overwrite confirmation or automatic export of the previous secret.
The current page targets Devnet, but the same secret controls the same public address wherever that key has authority, including Mainnet.
BACKUP AND FUNDING BOUNDARY
Verify a recoverable backup before sending even a small balance
After the full login page creates a wallet, it shows the public address and complete Base58 private-key backup, clears the confirmation checkbox, and blocks Continue until the player checks it. The page does not verify a clipboard copy, test a restored key, download a file, or persist the confirmation. A creator can check the box without saving anything. When an unbound created record is shown again later, the secret can be displayed again and the checkbox is reset.
The Copy private key button writes the value to the operating system clipboard and reports success or failure. It does not read the clipboard back, verify the reconstructed address, or clear the clipboard later. Clipboard history, clipboard synchronization, and other applications can retain copied text. On a shared browser profile, another person who can use the same origin may also reach the saved record. localStorage is not a reliable cross-device backup: changing device, browser, browser profile, site origin, privacy mode, or site-data settings can make the wallet unavailable, while some profile-backup or synchronization systems may copy browser data in ways this client does not verify.
Imported records take another branch: the full login page blanks the private-key field, hides the backup controls, and skips the backup checkbox because the player supplied the key. That does not remove the imported secret from localStorage. The source label controlling this interface branch is local data rather than proof that a safe external backup still exists.
The full login Continue path then asks the configured RPC for the stored address's confirmed SOL balance. It requires at least 100,000,000 lamports, or 0.1 SOL, before it stores the wallet session and advances. This is a minimum gate, not an exact required deposit, maximum balance, escrow, spending cap, fee guarantee, or rule that constrains where later transactions can send SOL. A three-decimal formatter can even display a value just below the threshold as 0.100 SOL while the integer comparison still rejects it.
The in-game Play wallet panel is a separate path and bypasses both full-login gates. Create immediately writes the wallet and applies a wallet session while merely telling the player to save and fund it. Use local game wallet also applies a session without checking a backup box, source label, or 0.1 SOL balance. That bypass does not make an unfunded transaction succeed and does not bypass any on-chain instruction validation; it only means the login-page safeguards are not universal.
It is reset whenever the vault is shown and does not prove that a backup exists or is usable.
The secret remains stored even though the full login page does not show it in that branch.
It is a confirmed balance comparison against 100,000,000 lamports, not a cap, reserve, escrow, or protocol authorization rule.
It exposes separate Create and Use local wallet actions that do not enforce the backup checkbox or minimum-balance comparison.
The client does not clear clipboard history or guarantee storage survives privacy mode, profile changes, site-data cleanup, or device changes.
- Use only a dedicated low-balance Game Wallet
The interface warns against importing a main wallet, but code cannot recognize one. Keep valuable assets and long-lived authority elsewhere.
- Save the exact secret before funding
Store the complete Base58 value through a trusted offline backup process. Do not send it through chat, a cloud form, or support; screenshots and synchronized clipboard history can also leak or replicate it.
- Verify the backup without exposing it
Use a separate trusted recovery environment to confirm that the saved material reconstructs the same public address. Do not use Import in the currently stored wallet as a test because Import can overwrite that record.
- Transfer only the amount you choose to risk
Use Devnet SOL only. The current login gate needs at least 0.1 SOL, but it imposes no upper limit and does not limit the key's authority. Low balance is risk reduction advice, not enforcement.
- Do not press Create or Import casually
Either operation can replace the existing stored key without confirmation. Back up the current wallet and verify the intended address first.
SIGNING AUTHORITY BOUNDARY
Fewer popups means the browser-held key can sign directly
When NiceChunk needs the local provider, it reads the stored Base58 secret, reconstructs the Keypair, and returns a provider marked connected. Its connect method simply returns the derived public key. Its transaction methods call partialSign with that Keypair for one transaction or every transaction in a supplied list. There is no external-wallet confirmation prompt in those methods, and disconnect is a no-op.
Those transaction methods do not inspect whether the bytes are a NiceChunk mining action. A caller that reaches the signer can ask the key to sign any compatible transaction supplied to it, including transfers, token operations, or owner-authorized instructions, subject to the normal Solana programs and account rules in that transaction. The current UI, 0.1 SOL minimum, PlayerSession action bits, and low-balance advice do not narrow what the owner secret itself can authorize.
The current chain adapter also recognizes this provider and can send keypair-signed transactions directly. For the local Game Wallet's player-session setup, the owner public key and session-authority public key are the same key. This avoids funding and storing a second temporary signer, but it also means the Game Wallet secret remains the direct signing authority. Calling the record a session does not turn that long-lived owner key into a server-held session or a narrower extension approval.
The PlayerSession PDA can expire or limit the supported player instructions that read it, but that expiry does not rotate, delete, or neutralize the long-lived owner secret. By contrast, the external-wallet path can use a different temporary session key. Local mode returns the Game Wallet Keypair itself and marks that it is not using a separate Gameplay Session signer.
This does not mean the key can make every attempted game action succeed. The provider implements a small transaction interface based on partialSign, while the chain adapter contains additional local-key branches. Prepared instructions still face RPC availability, fees, account ownership, PDA derivation, program checks, cooldowns, capacity rules, and confirmation failure. The local key supplies a signature; it does not prove the instruction was correct or accepted.
The stored record and provider derive identity differently after corruption. Login balance and binding use the separately stored address without verifying it against the secret. Provider construction derives the public key from the secret, and the chain provider selector later requires the bound session address to equal that derived public key. A mismatch can therefore let login inspect or bind one address but later make the local signer unavailable. It is an integrity failure, not a safe address migration feature.
The secret is decoded each time a provider or direct local Keypair is requested.
The convenience depends on trusting the same-origin application code that prepares transaction bytes and asks the Keypair to sign them.
It does not prove correct program behavior, sufficient balance, RPC acceptance, confirmed status, or finalized state.
A safe load would derive the public key from the secret and require it to equal the stored address before using either.
The partial-sign method itself does not enforce a NiceChunk-only instruction list, a SOL cap, or a Devnet-only boundary.
An on-chain PlayerSession record can expire while the same browser-held owner Keypair remains able to sign owner-authorized transactions.
LIFECYCLE BOUNDARY
Disconnect clears a session; deletion removes only this browser record
The login-page Disconnect button tries to disconnect detected extension providers and then clears the selected wallet address, wallet name, and wallet-bound timestamp. The Play logout path similarly treats provider disconnect as best effort, clears identity and selected runtime caches, and returns to login. The built-in local provider's own disconnect method returns without changing anything. None of these paths deletes the four nicechunk.localGameWallet fields.
The local wallet module does export clearLocalGameWallet, which removes the address, secret, timestamp, and source fields. Current repository search finds no reviewed runtime or product control that calls it, and neither wallet panel presents a Delete or Forget Game Wallet button. A player can disconnect and later see or reuse the same saved local record. Clearing browser site data can delete it, but that broad browser action may remove other local NiceChunk state too.
Deletion is not revocation, transfer, refund, or guaranteed secure erasure. Disconnecting, clearing site data, or deleting the local record does not move out SOL, tokens, or other assets; undo completed transactions; erase public history; close program accounts; remove browser or clipboard copies elsewhere; or necessarily remove a Keypair already held in live JavaScript memory. If the deleted record was the last valid secret, the on-chain balance and assets remain at the address but may become permanently inaccessible.
If the secret was exposed, anyone with a copy may still sign. From a trusted environment, create a fresh wallet and move remaining value and authority rather than relying on Disconnect or local deletion. Removing one browser copy cannot make another person's copy private again.
Recovery is deterministic key reconstruction, not an account reset. A correct complete 64-byte secret in accepted Base58, JSON-string, or strict JSON-array form recreates the corresponding Keypair, and an explicitly typed NiceChunk seed record can expand a deliberate 32-byte seed. Bare 32-byte values are rejected so a public address cannot silently become a different wallet seed. Always verify the recovered public address before treating the wallet as restored. If every valid copy is lost after site data is cleared or a silent overwrite, the reviewed client exposes no server-backed recovery path; players should not assume NiceChunk support can restore the secret.
Exact cleanup differs between login and Play, but neither path invokes the local Game Wallet clear function.
The clear function exists in source but has no reviewed runtime or product caller and no player-facing button.
It can also remove unrelated local game state and cannot revoke a secret copied elsewhere.
Recovery depends on valid secret material that reconstructs the same public address; do not assume support can restore a lost secret.
- To stop using the wallet in the current session
Use Disconnect or logout. Expect the local binding to clear, but expect the built-in secret to remain available in this browser.
- To delete the browser copy
First verify a safe backup and the intended address, then move or account for any remaining balance. The current product has no dedicated Delete control; clearing site data is broader than wallet-only deletion and is not guaranteed secure erasure.
- To recover the same wallet
Import the trusted backup and verify that the derived public address exactly matches the address you recorded before sending or signing anything.
- To respond to exposure
Assume copied secret material remains usable. Create a fresh wallet through a trusted path and move remaining assets or authorities; Disconnect cannot rotate the key.
- To avoid accidental replacement
Do not use Create or Import as an experiment while a funded local wallet is stored. There is no current overwrite confirmation or automatic previous-key recovery.
FOLLOW THE KEY
Eleven excerpts follow the secret from storage to signing and cleanup
The formulas separate one login-page gate from the key's much wider authority. The verbatim excerpts show creation, strict import routing, clearing, direct provider signing, created-versus-imported presentation, the backup and balance gate, the Play bypass, clipboard copying, session cleanup, owner-equals-session authority, confirmed submission, and the stored-address integrity check.
Current full-login Continue gate
continueLogin = has(record.address) AND (source = imported OR backupBox = checked) AND confirmedBalance(record.address) >= 100,000,000 lamportsThis is browser-interface policy, not a Solana program rule. The checkbox does not prove a backup, imported records bypass it, and the expression does not verify that record.address matches the public key derived from the stored secret. The threshold is a minimum rather than a cap or reserved budget. The Play wallet panel applies a local session without evaluating this formula.
- record.address
- The separately stored Base58 address read from localStorage; this gate does not derive and compare it with the stored secret.
- source
- The mutable created-or-imported localStorage label used to choose the backup interface branch.
- backupBox
- The temporary created-wallet checkbox. Checked means only that the player selected it in the current page state.
- confirmedBalance
- One RPC snapshot at confirmed commitment for the separately stored address.
- 100,000,000 lamports
- Exactly 0.1 SOL, used here as the current full-login minimum and not as an upper limit.
The login minimum is not the key's authority boundary
GameWalletSecret => full signing authority for Address on every Solana cluster; 0.1 SOL gate != spending cap != permission capThe same secret derives the same address on Devnet and Mainnet. The local provider applies that Keypair to transaction bytes without checking for a NiceChunk-only instruction. Solana programs still validate the submitted transaction, but the browser gate, low-balance advice, session action bits, and current user interface do not reduce the owner key's cryptographic scope.
- GameWalletSecret
- The complete browser-held Keypair secret, stored as Base58 text and capable of producing owner signatures.
- Address
- The public key derived from the secret. Cluster choice changes which ledger is queried, not the derived address.
- full signing authority
- The signer itself does not restrict requests to NiceChunk actions; each transaction remains subject to its normal program and account checks.
- 0.1 SOL gate
- A current full-login Devnet balance minimum, not escrow, an automatic transfer, a spending maximum, or an authorization rule.
- !=
- Means 'is not the same as.' Interface policy must not be mistaken for a cryptographic permission boundary.
Create, Import, and Clear share one local record
JavaScriptsrc/localGameWallet.jsexport function createLocalGameWallet() {
const keypair = Keypair.generate();
return storeLocalGameWalletKeypair(keypair, "created");
}
export function importLocalGameWallet(value) {
const keypair = keypairFromSecretInput(value);
return storeLocalGameWalletKeypair(keypair, "imported");
}
export function clearLocalGameWallet() {
if (!hasLocalStorage()) return;
localStorage.removeItem(localGameWalletKeys.address);
localStorage.removeItem(localGameWalletKeys.secretKey);
localStorage.removeItem(localGameWalletKeys.createdAt);
localStorage.removeItem(localGameWalletKeys.source);
}
Create generates a Keypair and Import parses one, but both immediately call the same storage routine with only a source label changed. Neither checks whether a wallet already exists, so either can replace the record. Clear removes all four Game Wallet fields, but no reviewed login or Play control calls this exported function.
- 1-4
Generate a fresh Solana Keypair and send it directly to the storage routine as created.
- 6-9
Parse the supplied secret material and send the resulting Keypair to the same routine as imported.
- 11-17
The available clear operation removes address, secret, timestamp, and source from browser storage; it does not revoke copies or chain state.
The local provider signs with the reconstructed Keypair
JavaScriptsrc/localGameWallet.jsexport function getLocalGameWalletProvider() {
const keypair = loadLocalGameWalletKeypair();
if (!keypair) return null;
return {
isNiceChunkLocalGameWallet: true,
isConnected: true,
publicKey: keypair.publicKey,
async connect() {
return { publicKey: keypair.publicKey };
},
async disconnect() {
return undefined;
},
async signTransaction(transaction) {
transaction.partialSign(keypair);
return transaction;
},
async signAllTransactions(transactions) {
for (const transaction of transactions) transaction.partialSign(keypair);
return transactions;
},
};
}
Provider construction first loads the secret-derived Keypair. The returned object is always marked connected, connect just returns the public key, and disconnect changes no state. Transaction methods partial-sign directly with the Keypair without an extension prompt. This small interface does not promise compatibility with every Solana transaction or wallet operation.
- 1-3
Reconstruct the Keypair from stored secret material and refuse to create a provider if loading fails.
- 4-10
Expose a local-provider marker, a fixed connected flag, the derived public key, and a connect result without an external approval step.
- 11-13
Return from disconnect without deleting storage, clearing a binding, or revoking the key.
- 14-21
Apply this Keypair directly to one transaction or each transaction in a list and return the signed object or list.
- 22-23
Finish the provider object and function; no message-signing, event, or delete method is added.
Import routing rejects ambiguous seeds before Keypair construction
JavaScriptsrc/localGameWallet.jsfunction keypairFromSecretInput(value) {
const input = String(value || "");
if (input.length > localGameWalletImportMaxCharacters) {
throw importError(
localGameWalletImportErrorCodes.inputTooLarge,
`Private-key input exceeds ${localGameWalletImportMaxCharacters} characters.`,
);
}
const raw = input.trim();
if (!raw) throw importError(localGameWalletImportErrorCodes.missing, "Missing private key.");
const material = startsWithJsonToken(raw)
? parseJsonSecretMaterial(raw)
: parseBase58SecretMaterial(raw);
if (material.kind === "seed") return Keypair.fromSeed(material.bytes);
try {
return Keypair.fromSecretKey(material.bytes);
} catch (cause) {
throw importError(
localGameWalletImportErrorCodes.invalidSecret,
"The supplied 64-byte value is not a valid Solana secret key.",
cause,
);
}
}
The entry point rejects oversized input before parsing, trims only outer whitespace, routes JSON-looking input to strict structured parsing, and routes other text to Base58 validation. Its helpers require byte integers, reject bare 32-byte values as ambiguous with public addresses, allow a seed only through an explicitly typed NiceChunk seed record, and map malformed input to stable local error codes. A complete 64-byte value still passes through Solana's internal secret/public consistency check before storage.
- 1-10
Bound the raw character count, normalize only outer whitespace, and reject an empty value with a stable import error code.
- 12-14
Route JSON arrays or one quoted Base58 value to structured parsing and plain text to Base58 parsing. Neither branch silently removes or substitutes unsupported characters; a different valid key must be caught by comparing the derived address with the backup address.
- 16-16
Construct from a 32-byte seed only when the helper marked an explicitly typed seed record; bare 32-byte values never reach this line.
- 17-26
Construct the complete 64-byte Keypair, preserve Solana's internal consistency validation, and convert a failure into a non-secret-bearing import error.
The full login vault treats created and imported records differently
JavaScriptlogin/login.jsfunction showGameWalletVault(wallet, { source = wallet?.source || "created" } = {}) {
gameWalletVaultSource = source === "imported" ? "imported" : "created";
if (gameWalletAddress) gameWalletAddress.value = wallet.address || "";
if (gameWalletPrivateKey) gameWalletPrivateKey.value = gameWalletVaultSource === "created" ? wallet.secretKey || "" : "";
if (gameWalletBackupConfirmed) gameWalletBackupConfirmed.checked = false;
if (gameWalletBackupFields) gameWalletBackupFields.hidden = gameWalletVaultSource === "imported";
if (copyGameWalletPrivateKey) copyGameWalletPrivateKey.tabIndex = gameWalletVaultSource === "imported" ? -1 : 0;
if (continueGameWalletButton) {
const i18nKey = gameWalletVaultSource === "imported"
? "login.gameWallet.continueImportedAction"
: "login.gameWallet.continueAction";
continueGameWalletButton.dataset.i18n = i18nKey;
continueGameWalletButton.textContent = t(i18nKey);
}
if (gameWalletBalanceLine) {
gameWalletBalanceLine.classList.remove("warning");
gameWalletBalanceLine.textContent = t("login.gameWallet.balanceReady", { address: formatAddress(wallet.address) });
}
gameWalletVault?.classList.remove("hidden");
}
The stored source label selects the interface branch. Created shows the returned secret and resets an unchecked confirmation box. Imported blanks the secret field, hides the backup controls, and changes the Continue label, even though the imported secret remains in localStorage. The balance line initially gives an instruction rather than proving a balance.
- 1-4
Normalize every source other than imported to created, show the address, and show the secret only for the created branch.
- 5-7
Reset the confirmation box, hide imported backup fields, and remove the imported private-key copy button from keyboard order.
- 8-14
Choose different Continue text for imported and created interface states.
- 15-20
Reset the balance warning, display an instruction containing the formatted address, and reveal the vault.
The full login gate checks a checkbox and a confirmed minimum
JavaScriptlogin/login.jsasync function continueWithGameWallet() {
continueGameWalletButton.disabled = true;
statusLine.textContent = t("login.gameWallet.statusCheckingBalance");
try {
const [{ getLocalGameWalletRecord }, { getNicechunkConnection }, { PublicKey }] = await Promise.all([
import("../src/localGameWallet.js"),
import("../src/chain/nicechunkChain.js"),
import("@solana/web3.js"),
]);
const record = getLocalGameWalletRecord();
if (!record?.address) throw new Error("missing-game-wallet");
const source = record.source === "imported" ? "imported" : gameWalletVaultSource;
if (source !== "imported" && !gameWalletBackupConfirmed?.checked) {
statusLine.textContent = t("login.gameWallet.statusBackupRequired");
gameWalletBackupConfirmed?.focus();
return;
}
const balanceLamports = await getNicechunkConnection().getBalance(new PublicKey(record.address), "confirmed");
if (balanceLamports < requiredGameWalletLamports) {
const balanceSol = formatSol(balanceLamports);
if (gameWalletBalanceLine) {
gameWalletBalanceLine.classList.add("warning");
gameWalletBalanceLine.textContent = t("login.gameWallet.balanceRequired", { balance: balanceSol });
}
statusLine.textContent = t("login.gameWallet.statusFundingRequired");
return;
}
walletAddress = record.address;
persistConnectedWallet({ walletAddress, walletName: "NiceChunk Game Wallet" });
statusLine.textContent = t("login.gameWallet.statusReady");
await initialize();
The full login flow reads the record without its secret, trusts the stored address, skips backup confirmation for imported source, and queries that address at confirmed commitment. Only the integer balance comparison controls the 0.1 SOL gate. Passing stores a browser connection; it does not cap the wallet, reserve funds, verify an address-secret match, or prove a later chain action.
- 1-9
Disable Continue, announce a balance check, and load the record, connection, and PublicKey dependencies.
- 10-17
Require a stored address and require only created-source interface state to have the temporary checkbox selected. Imported source bypasses it.
- 18-26
Read the separately stored address balance at confirmed commitment and stop below the current lamport threshold.
- 27-31
Persist the stored address as the selected Game Wallet, report ready, and re-enter login initialization.
The Play panel applies a local session without the full login gates
JavaScriptplay/play-chain-session.js async function createLocalWalletLogin() {
try {
const module = await loadLocalWalletModule();
if (typeof module.createLocalGameWallet !== "function") throw new Error("Local wallet module does not expose createLocalGameWallet.");
const wallet = module.createLocalGameWallet();
applyWalletSession(wallet.address, "NiceChunk Game Wallet");
renderWalletPanel(`Created game wallet ${shortAddress(wallet.address)}. Save the private key before funding it.`, wallet);
setStatus(`Created NiceChunk game wallet ${shortAddress(wallet.address)}. Fund it before real chain mining.`);
appendChainEvent(`Created local game wallet ${shortAddress(wallet.address)}. Its private key is stored as unencrypted Base58 text for this browser origin.`);
return wallet;
} catch (error) {
setStatus(`Create game wallet failed: ${readableError(error)}.`);
appendChainEvent(`Create game wallet failed: ${readableError(error)}.`);
renderWalletPanel(`Create game wallet failed: ${readableError(error)}.`);
return null;
}
}
async function continueLocalWalletLogin() {
try {
const module = await loadLocalWalletModule();
const wallet = module.getLocalGameWalletRecord?.({ includeSecret: true });
if (!wallet?.address) {
renderWalletPanel("No local game wallet exists in this browser. Create one first.");
setStatus("No local game wallet exists in this browser.");
return null;
}
applyWalletSession(wallet.address, "NiceChunk Game Wallet");
renderWalletPanel(`Using game wallet ${shortAddress(wallet.address)}.`, wallet);
setStatus(`Using NiceChunk game wallet ${shortAddress(wallet.address)}.`);
appendChainEvent(`Using local game wallet ${shortAddress(wallet.address)} for chain session signing.`);
return wallet;
} catch (error) {
setStatus(`Continue game wallet failed: ${readableError(error)}.`);
appendChainEvent(`Continue game wallet failed: ${readableError(error)}.`);
renderWalletPanel(`Continue game wallet failed: ${readableError(error)}.`);
return null;
}
}
Play creation immediately applies a wallet session after storage and only displays advice to save and fund. Play continuation requests the record including its secret and applies the session when an address exists. Neither function checks the full-login backup box, imported source branch, address-secret relationship, or 0.1 SOL minimum. On-chain and RPC checks can still reject later actions.
- 1-10
Load the local module, create and store a wallet, immediately apply the session, and display backup and funding advice without enforcing either.
- 11-17
Report creation failures and return null; no previous-wallet restoration is attempted.
- 19-27
Load an existing record including its secret and require only a nonempty address before proceeding.
- 28-32
Apply the stored address as the selected session, reveal the local record to the wallet panel, and announce signing use.
- 33-39
Report continuation failures and return null; there is no backup or balance branch in this function.
Session cleanup removes binding fields, not the Game Wallet record
JavaScriptsrc/walletSession.jsexport function clearWalletConnection() {
localStorage.removeItem(walletSessionKeys.walletAddress);
localStorage.removeItem(walletSessionKeys.walletName);
localStorage.removeItem(walletSessionKeys.walletBoundAt);
}
export function clearWalletSession() {
clearWalletConnection();
localStorage.removeItem(walletSessionKeys.username);
clearWalletRuntimeCaches();
}
The selected connection has its own address, name, bound time, username, and wallet-scoped runtime caches. Neither cleanup function refers to nicechunk.localGameWallet.address or nicechunk.localGameWallet.secretKey. Disconnect can therefore end the selected session while the separately stored owner secret remains available to the same browser origin.
- 1-5
Remove the selected wallet address, wallet label, and binding timestamp, then finish connection cleanup.
- 7-11
Full session cleanup also removes the username and wallet-scoped runtime caches, but still does not call clearLocalGameWallet.
Copy writes the secret to the operating-system clipboard
JavaScriptlogin/login.jsasync function copyGameWalletField(value, kind) {
if (!value) return;
try {
await navigator.clipboard.writeText(value);
const message = kind === "privateKey"
? t("login.gameWallet.statusPrivateKeyCopied")
: t("login.gameWallet.statusAddressCopied");
statusLine.textContent = message;
showLoginToast(message, "success");
} catch {
const message = t("login.gameWallet.statusCopyFailed");
statusLine.textContent = message;
showLoginToast(message, "error");
}
}
The button asks the browser Clipboard API to write the supplied value, reports whether that call failed, and mirrors the same result into the login toast. It does not read the value back, derive an address from a copied secret, clear clipboard history, control clipboard synchronization, or prove that the player created a durable offline backup.
- 1-2
Ignore an empty field rather than writing an empty clipboard value.
- 3-9
Write the exact value to the clipboard, choose address-or-private-key success text, update the status line, and show a success toast.
- 10-14
Display the copy failure in both the status line and the toast when the Clipboard API rejects; no verification or later clipboard cleanup follows.
Local mode makes the owner its own session authority
JavaScriptsrc/chain/nicechunkChain.js const tx = new Transaction();
if (!profileAccount?.data?.length) {
tx.add(createInitializePlayerInstruction(owner, playerProfile, ""));
}
tx.add(createOrRefreshPlayerSessionInstruction({
owner,
sessionAuthority: owner,
playerProfile,
playerSession,
expiresAt,
}));
await signAndSendKeypairTransaction(keypair, tx, conn);
const balanceLamports = await conn.getBalance(owner, "confirmed").catch(() => null);
markGameplaySessionReady(owner, owner, balanceLamports);
updateGameplaySessionStatusCache(owner, createLocalGameWalletStatus(owner, balanceLamports, expiresAt));
return { keypair, expiresAt, localGameWallet: true };
The surrounding function has already set owner from the local provider and loaded the same Game Wallet Keypair. This excerpt optionally initializes a missing profile, writes a PlayerSession whose sessionAuthority is the owner itself, signs with that Keypair, and caches readiness under owner, owner. The PDA can limit supported player instructions, but it does not turn the long-lived owner secret into an independent temporary key.
- 1-4
Prepare one transaction and add player-profile initialization only when that public account does not exist.
- 5-11
Create or refresh the PlayerSession with the same public key in both owner and sessionAuthority roles.
- 13-17
Sign with the local Game Wallet Keypair, read the owner's confirmed balance, cache owner-as-authority readiness, and return the same Keypair.
The Keypair pays, signs, sends, and waits for confirmed
JavaScriptsrc/chain/nicechunkChain.jsasync function signAndSendKeypairTransaction(signer, transaction, conn = getNicechunkConnection()) {
transaction.feePayer = signer.publicKey;
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash("confirmed");
transaction.recentBlockhash = blockhash;
transaction.lastValidBlockHeight = lastValidBlockHeight;
transaction.sign(signer);
const signature = await sendRawTransactionWithLogs(conn, transaction.serialize(), "keypair");
await confirmTransactionByHttpPolling(conn, { signature, blockhash, lastValidBlockHeight }, "confirmed");
return signature;
}
The local Keypair becomes the fee payer, receives a recent confirmed blockhash, signs the prepared transaction, and sends its serialized bytes. The client then polls that signature until its confirmed condition or an error. There is no external-wallet popup in this function, and confirmed must not be renamed finalized.
- 1-5
Use the signer address as fee payer and attach a recent blockhash plus its last valid block height.
- 6-7
Apply the owner Keypair signature and send the serialized signed transaction bytes.
- 8-10
Poll the returned signature at confirmed commitment, then return it; this excerpt does not wait for finalized.
The local provider is selected only when the bound address matches
JavaScriptsrc/chain/nicechunkChain.jsasync function connectedWalletProvider({ prompt = false } = {}) {
const storedWallet = localStorage.getItem(storageWalletKey);
const localGameWallet = getLocalGameWalletProvider();
if (localGameWallet?.publicKey && storedWallet === localGameWallet.publicKey.toBase58()) {
return localGameWallet;
}
The selected wallet address is read separately from the Game Wallet record. Provider construction derives its public key from the stored secret. Only an exact match chooses the local provider. Because the earlier record reader and login balance gate do not perform that comparison, a damaged address field can appear usable during login and then fail this later signer selection.
- 1-3
Read the active browser binding and independently reconstruct a provider from the stored Game Wallet secret.
- 4-6
Return the local signer only when its secret-derived public key exactly equals the active bound address.
IMPLEMENTATION EVIDENCE
Where these claims come from
Each claim is intentionally scoped to a concrete implementation path. These references are for verification, not decoration.
src/localGameWallet.js
Defines the four localStorage keys, generated and imported Keypair paths, strict Base58 and JSON validation, ambiguous 32-byte rejection, explicitly typed seed handling, direct signing provider, no-op disconnect, and an exported clear function with no reviewed runtime or product caller.
login/login.js
Shows created secrets, hides imported secrets, resets the backup checkbox, enforces the full-login 100,000,000-lamport confirmed minimum, binds the stored address, and disconnects without clearing the Game Wallet record.
login/index.html
Provides Create and Import controls, the visible backup checkbox and copy fields, low-balance warnings, and Disconnect controls, but no Delete or Forget Game Wallet control.
public/login/locales/en.json
States the player-facing backup, low-balance, imported-wallet, 0.1 SOL, copy, and ready messages used by the full login page.
src/walletSession.js
Separates selected wallet binding keys from the local Game Wallet keys and clears address, name, bound time, username, and selected runtime-cache prefixes without deleting the built-in secret.
src/chain/nicechunkChain.js
Selects the local provider only when its secret-derived public key matches the stored session address, sends local Keypair-signed transactions, and uses the same local key as owner and player-session authority.
play/play-chain-session.js
Exposes separate in-game Create and Use local wallet paths that apply a session without the full-login backup checkbox or balance minimum and shows the returned secret in the Play wallet panel.
play/index.html
Provides Create, Use local wallet, Disconnect, address, and private-key copy controls in Play, with no wallet-only Delete control.
scripts/audit-wallet-flows.mjs
Checks a built local Game Wallet creation flow, the temporary backup checkbox, 99,999,999-versus-100,000,000-lamport boundary, session cleanup, retained four-field record, and redisplayed created secret with deterministic mocked RPC responses; it does not test import, overwrite, mismatch, live funding, transaction signing, or deletion.
scripts/audit-login-game-wallet-import.mjs
Checks the built login page with deterministic throwaway material: ambiguous-address and invalid-character rejection, successful JSON-quoted Base58 import, no secret rendering or request leakage, no expected console warning, and 390-pixel overflow.
docs/wallet-flow-audit.md
Documents that the browser audit creates only an ephemeral isolated test key, never records its secret, uses mocked RPC data, and does not prove live balances, safe backup, transaction signing, submission, confirmation, recovery, or storage isolation.
play/tests/play-auth-session.test.mjs
Tests selected identity and runtime-cache cleanup while preserving unrelated storage, but does not directly exercise the local Game Wallet module or its four keys.
play/tests/local-game-wallet.test.mjs
Directly checks generated and imported key material, Base58 normalization, quoted and strict-array forms, explicitly typed seeds, ambiguous public-address rejection, strict byte validation, invalid-input storage preservation, default secret omission, signing, disconnect retention, and exact removal of the four wallet fields.
play/tests/session-funding-boundary-copy.test.mjs
Prevents the Play funding target from being described as escrow, a transaction spending boundary, or an authorization cap, and requires local-wallet copy to name the unencrypted origin-storage boundary.
package.json
Defines the focused local Game Wallet test command and includes it plus the built browser wallet-flow audit in release validation.