PLAYER, WALLET, AND CONTROL · STEP 12

Session Funding and Expiry: A Target Is Not a Balance, and Browser Time Is Not Chain Time

NiceChunk's repository client uses four separate things: a refill amount saved by the browser, the connected owner's wallet, a gameplay-signing address, and a public PlayerSession record whose storage rent is separate. It also highlights five client timing policies—8 hours, 15 minutes, 5 minutes, 60 seconds, and 30 seconds—while Solana programs use chain time to decide setup and use. This player decision guide separates those objects and clocks, points permission details back to Step 2.11, and leaves the full fee, rent, close, and refund inventory to Step 2.13.

54 min read
Canonical NiceChunk villager boy turns an empty wooden target peg beside a closed dark owner chest while the canonical villager girl opens a smaller copper fee box; a stone record plinth, local hourglass, and public sundial remain physically separate in the same coastal waystation.
In the generated real-Chunk.js coastal scene, baked game materials keep the empty target frame, dark owner chest, copper authority box, stone record foundation, local hourglass, and public sundial apart. Their sizes, contents, sand level, and shadows are unlabeled teaching props—not evidence of a balance, transfer, PDA state, timestamp, expiry, or recovery result.
PLAYER QUESTION If the modular Play HUD says “Session: 0.1 SOL” or shows an expiry time, is that SOL number a real balance, which clock decides access, and what can an ordinary player safely check before approving or retrying?
Learning path
Player, wallet, and control · Step 12 of 15
Audience
Players with no programming or blockchain background
Decision-guide boundary
Step 2.11 owns the permission mask, consumer scope, stored action counter, and key-sandbox explanation. This page answers funding, timing, retry, and key-lifecycle decisions. Step 2.13 will inventory exact fee payers, rent payers, close instructions, refunds, and recoverable balances across account types
Repository source route reviewed
The repository's production source route is /play/index.html → /play/main.js → play/* → play/play-chain-adapter.js → a resolved chain module. Repository files describe current source behavior but do not alone prove which asset a live browser loaded
Retained live-byte boundary
The 2026-07-19 UTC static production snapshot retained by the Player Sessions audit byte-pins one versioned chain candidate and records equivalent 28,800-second, 900-second, 100,000,000-lamport, and funding-prefix literals. It was a fixed HTTPS GET audit, not a wallet session, RPC balance read, dynamic-import observation, or transaction
Network evidence boundary
The retained confirmed Devnet snapshot records PlayerSession PDA rent and expiry classification, but it did not query session-authority balances, owner balances, browser targets, secrets, or Mainnet. This page therefore never turns its ten captured Session records into a claim about a player's current funds
Reviewed
19 July 2026 · literal repository excerpts plus the retained offline, Devnet, and static-production Player Sessions evidence
Funding target
A browser-stored number used to calculate a possible owner-to-session-authority top-up. It is not an account, escrow, spending cap, payment receipt, or promise that a transfer happened.
Public address and private key
A public address identifies an account that anyone may look up. A private key is the secret signing material that can authorize transactions for that address and must never be pasted into an unknown site.
Keypair
The code's name for one public address plus its private signing bytes. A separate plugin Session Keypair is still an ordinary Solana signer, not a key restricted to NiceChunk.
Owner balance
Spendable lamports at the connected owner's address. In plugin mode this address funds setup and a low session authority; in Local Game Wallet mode it is also the session-authority address.
Session-authority balance
Spendable lamports at the ordinary signer used for gameplay calls. Plugin mode normally uses a separate address; Local Game Wallet uses the owner address itself.
PlayerSession rent
Lamports held by the separate 184-byte PlayerSession PDA so its data allocation can remain. Those lamports are not the session authority's spendable fee balance.
Lamport
The smallest SOL unit. One SOL equals 1,000,000,000 lamports, so 0.1 SOL equals 100,000,000 lamports.
Top-up
A transfer of only the difference between a selected target and a lower current balance. In the reviewed client, the automatic branch is entered only when the authority balance is below 0.1 SOL.
Plugin Session
The arrangement where a long-lived external wallet remains owner and a separate browser-held Keypair normally becomes sessionAuthority.
Local Game Wallet
A persistent browser wallet whose one key fills both owner and sessionAuthority. Its full owner balance is therefore also the authority balance.
Browser time
Time calculated from Date.now() on one device. It drives client refresh and cache decisions but does not decide whether a contract accepts a Session.
Chain time
Clock.unix_timestamp read by a Solana program while an instruction executes. Setup and audited consumers compare expiresAt with this value.
Expiry timestamp
The Unix-seconds value called expiresAt in code. The browser proposes and may copy it, while a program compares the public record's value with chain time when an instruction executes.
RPC
The network service a browser or explorer asks for balances, accounts, transaction status, and chain context. A failed or stale RPC answer is not proof that the underlying account changed.
Commitment, confirmed, and finalized
Commitment says how much cluster agreement an RPC read requires. Confirmed is stronger than processed, but confirmed and finalized are different status labels; the reviewed helper waits for confirmed, not specifically finalized.
Fee payer
The account whose SOL pays a transaction fee. In reviewed plugin setup the owner is fee payer; supported later gameplay calls can instead use the session authority.
Refresh skew
The 15-minute client safety window. A copied expiry inside that window is treated as needing setup or refresh before it reaches zero.
Ready cache
A five-minute in-memory shortcut for the Local Game Wallet path. It avoids an RPC read; it does not extend the on-chain record.
Status cache
A separate 60-second in-memory snapshot returned by getGameplaySessionStatus unless a force read is requested.
Balance cache
The Play wallet HUD's 30-second cache for the connected wallet address. If refresh fails after a known value, the UI can keep that value and mark it stale.
Cache lifetime or TTL
How long the browser may reuse one saved in-memory result before asking again. A TTL schedules another read; it does not extend a Session on chain.
Browser storage and Base58/Base64
Browser storage keeps site data on the device. Base58 and Base64 turn bytes into text but do not encrypt or protect a private key.
Commit
A durable state change in one system. The setup transaction and the later browser secret write belong to different systems and are not one atomic commit.
Atomic transaction
A Solana transaction whose instructions either take effect together or roll back together. A later browser storage write is outside that atomic transaction.
Unknown result
A situation where a client did not obtain a reliable final answer after submission. It does not prove either failure or success, so addresses and signature status must be checked before retrying.
Rotation
Using a different session-authority key, which derives a different PlayerSession PDA. It does not close, revoke, or refund the old record.
Recovery
Regaining control or establishing a safe replacement after expiry, deletion, loss, or compromise. The correct response depends on which private key is still controlled.
Trusted explorer
A reputable read-only service used with the correct Solana network to inspect a public address or transaction signature. It never needs a private key or secret recovery phrase.

Key points

Four money-shaped values are not one balance

The funding target is browser policy, the owner and session authority are spendable addresses, and the PlayerSession PDA holds separate rent. Always name the address or account before interpreting a SOL number.

A higher target is not continuously enforced

The reviewed top-up helper returns immediately at or above 0.1 SOL. An authority holding 0.11 SOL is not automatically raised to a stored 0.50 SOL target by that branch.

The two wallet modes have different exposure

Plugin mode normally keeps the configured gameplay funds at a separate authority address. That separation is not a cap or key sandbox. Local Game Wallet makes owner and sessionAuthority the same long-lived browser key, so there is no separate session purse to refill.

Browser timers schedule checks; chain time decides acceptance

Eight hours, fifteen minutes, five minutes, sixty seconds, and thirty seconds belong to different client decisions. Contracts accept only when expiresAt remains greater than their Clock timestamp.

Retry only after identifying the old authority and transaction result

In plugin-wallet mode, a stored valid key is normally reused; a missing or invalid plugin record causes generation of a new authority and PDA. Local Game Wallet does not generate a replacement session authority here. An uncertain result can already have funded the first address, so blind retry can strand another balance and rent allocation.

Expiry is not deletion, refund, or wallet recovery

Expiry blocks audited Session consumers for that record. It does not erase a key, drain SOL, close the PDA, refund rent, or contain a compromised owner or Local Game Wallet key.

Funding does not grant gameplay permission

SOL lets an address pay fees or required account funding; it does not set allowedActions or make a consumer accept the key. Step 2.11 explains the permission mask, consumer checks, and currently unenforced stored action counter.

TARGET, TWO ADDRESSES, ONE PDA

Start by naming four different money objects

A funding target is only a number saved in this browser. In the current modular Play source, the HUD text “Session: X SOL” is built from state.sessionLamports, which is that locally saved target; it is not an RPC balance for either the owner or the session authority. The funding panel clamps the selected value to at least 0.1 SOL, converts it to lamports, stores it under an owner-scoped key, and later labels a known value “Current session funding target.” Saving or displaying this number creates no transaction and proves no address received anything.

The owner balance and session-authority balance are ordinary address balances. In plugin mode they normally belong to different public keys: the owner approves setup and a possible transfer, while the session authority later pays fees for supported gameplay transactions. A wallet HUD reading the connected owner cannot be silently reused as evidence for the separate authority, and an authority status read cannot prove what remains in the owner wallet.

The PlayerSession PDA is a fourth account. The Player Program asks Solana Rent for the minimum needed by a 184-byte data account and uses the owner payer to create or top up that PDA allocation. The retained Devnet snapshot observed 2,171,520 lamports per captured PDA at its dated context, but did not query either spendable address. Treat that number as dated Devnet rent evidence, not a universal current balance.

Browser policy configured target in lamports

No public address owns this number and saving it sends no SOL.

Modular Play HUD Session: X SOL = state.sessionLamports target

That visible X is local presentation state, not an owner or session-authority getBalance result.

Connected wallet owner address balance

The Play HUD requests getBalance for walletAddress at confirmed commitment.

Gameplay signer sessionAuthority address balance

Plugin status reads the stored Keypair's public address; it can differ from owner.

Authorization record PlayerSession PDA rent lamports

A separate program-owned data account rather than spendable authority funds.

Canonical NiceChunk villager boy adjusts an unmarked wooden target frame beside a large closed dark chest while the canonical villager girl opens a smaller copper box and points toward a detached gray stone record plinth with its own foundation recess.
The generated scene uses the real Chunk.js coast and baked game materials to keep four metaphors apart: policy frame, owner chest, authority box, and record foundation. The empty frame changes no supply, and the containers reveal no numeric amount, so the image does not establish SOL balances, rent, ownership, an RPC read, or a completed transfer.

BELOW THE FLOOR ONLY

The reviewed helper tops up toward a target only after balance falls below 0.1 SOL

The configured target is clamped to at least 100,000,000 lamports. The chain module first looks for an owner-specific browser value, then the default value, and finally falls back to the minimum when the selected number is missing, invalid, or too small. This selection still describes policy, not funds.

For a stored plugin Session, fundGameplaySessionIfNeeded immediately returns when the authority has at least the 0.1 SOL minimum. Only a lower balance reaches target selection. If that lower balance is also below the selected target, the owner is asked to transfer target minus balance. The result is a refill trigger, not continuous target maintenance.

For example, with target 0.50 SOL: an authority balance read at confirmed commitment of 0.06 SOL produces a 0.44 SOL requested difference, while a balance of 0.11 SOL produces no automatic top-up in this helper. In reviewed plugin setup, the owner is transaction fee payer and funds a newly allocated PlayerSession PDA. Supported later gameplay calls can instead use the session authority as fee payer and can spend its balance on fees or action-created accounts. Step 2.13 will inventory those fee and rent paths precisely.

Funding still grants no gameplay permission. A funded authority must also present a PlayerSession that passes the relevant consumer's owner, relationship, expiry, and allowed-action checks. Step 2.11 owns that permission mask and explains why the stored maxActions and actionCount fields are not a currently enforced spending or action ceiling.

Minimum trigger 100,000,000 lamports = 0.1 SOL

Repository client policy and retained static-production literal, not a contract field.

Low balance transfer = target − current balance

Only when current balance is below both the minimum-entry branch and selected target.

At least minimum automatic transfer = 0

A higher stored target is not filled by the reviewed top-up helper.

After funding later fee-payer or account-funding duties may reduce authority balance

The owner pays reviewed plugin setup fees and Session-PDA allocation; Step 2.13 separates every later payer and recovery path.

Permission not granted by SOL

Funding does not set allowedActions or bypass the consumer checks explained in Step 2.11.

Canonical NiceChunk villager boy lifts a timber sluice above water entering one copper-lined basin while the canonical villager girl examines a separate fuller basin beneath an uphill dark reservoir and a second open trough in the same coastal refill station.
This generated real-Chunk.js coast scene uses baked game materials and two water basins as an approximate threshold metaphor. Both water levels are illustrative, and the right-hand trough also appears wet; therefore the picture cannot prove which branch opened, any 0.1 SOL comparison, target difference, balance query, wallet approval, transfer, or confirmation.

TWO FUNDING SHAPES

Plugin Session separates the small signer purse; Local Game Wallet does not

In plugin mode, owner and sessionAuthority normally differ. The client can transfer SOL from the owner to the separate authority and applies both signatures to setup. Later supported calls can use that authority as transaction fee payer. This arrangement keeps the configured gameplay funds at a separate address, but it is separation rather than isolation: PlayerSession does not cap the balance or technically restrict what the ordinary authority key can sign.

Local Game Wallet uses one persistent browser Keypair as owner and sessionAuthority. The chain module's Local-mode status object reports configuredFundingLamports and minimumFundingLamports as zero to mean that a separate-session top-up policy is not applicable. Those zeros do not mean the address balance is zero, do not overwrite a browser target displayed elsewhere, and do not turn that target into a balance. The same address balance fills both owner and authority roles, while PlayerSession PDA rent remains separate.

The Local Game Wallet private key is stored as Base58 text in browser storage. Plugin Session stores a different Base64-encoded private key under an owner-scoped Session key. Neither encoding is encryption. In plain language, begin every funding decision by asking whether the game is using two addresses or the same address twice; do not rely on a generic label such as “session wallet.”

Plugin mode owner usually differs from sessionAuthority

A distinct authority can receive a modest top-up and sign supported calls.

Local Game Wallet owner equals sessionAuthority

One persistent key controls the full address balance and fills both setup roles.

Local mode status configured minimum and target reported as 0

This means separate-session funding is not applicable in that status object, not that the wallet balance or every browser target is zero.

PDA rent separate in both modes

Equal owner and authority keys do not merge the program-owned Session PDA into the wallet.

Canonical NiceChunk villager boy stands beside a large dark chest while holding a separate copper key above its own small copper box; the canonical villager girl holds one dark key bar against two recesses on a connected stone-and-timber fixture above a larger chest.
The generated real-Chunk.js workshop and baked game materials contrast a separated plugin-style key and fee box with one combined Local Game Wallet fixture. Prop count and placement explain roles only: they do not prove key ownership, encrypted storage, wallet balance, funding exposure, signature scope, setup success, or any live mode selection.

SIX DIFFERENT TIME QUESTIONS

Five highlighted browser windows schedule work; Solana Clock decides contract acceptance

The client requests expiresAt as browser now plus 28,800 seconds, or eight hours. It treats a copied plugin expiry as reusable only when more than 900 seconds remain. Those are browser calculations and can be wrong when the device clock, stored value, or account state is wrong. Setup independently rejects expiresAt at or before Clock.unix_timestamp.

Three highlighted caches answer narrower questions. The Local Game Wallet ready cache can bypass a network account read for five minutes and return a newly calculated now-plus-eight-hours value; it does not rewrite the record. The Session-status source API can return a cached snapshot for sixty seconds unless its force option is used, and the Play owner-balance HUD reuses a balance fetched at confirmed commitment for thirty seconds. None extends authorization.

These are the five Session and funding windows highlighted in this guide, not every timer in the client. The same source also contains a 12-second balance-request abort, a separate 60-second confirmation fallback for a branch without finite block-height information, and shorter polling intervals. A shared number of seconds does not give two timers the same job.

At actual use, audited Chunk, Building, and Backpack branches obtain Clock and reject when expiresAt is less than or equal to chain time. Therefore an optimistic browser countdown can coexist with a contract rejection, and an expired-looking browser copy can be stale while a refreshed chain record exists. The deciding test is the record read plus the consumer's chain-time gate, not one displayed countdown.

8 hours requested Session duration

Client default of 28,800 seconds, not a contract maximum.

15 minutes early refresh or reuse skew

A browser safety margin of 900 seconds.

5 minutes Local Game Wallet ready-cache TTL

Can skip an RPC Session read and return a synthetic expiry.

60 seconds Session status-cache TTL

A presentation/read cache, not authorization lifetime.

30 seconds Play owner-balance cache and refresh interval

A wallet HUD policy, not Session refresh or chain time.

Instruction execution Clock.unix_timestamp

Authoritative setup and consumer comparison inside the programs.

Other client timers outside this five-window teaching set

Request aborts and transaction polling do not become Session duration, refresh skew, or chain time.

Canonical NiceChunk villager boy inspects a rack containing three differently sized hourglasses, a water-drip bowl, and a pendulum while the canonical villager girl stands apart in the public courtyard facing one large stone sundial tower.
The generated scene places five local scheduling props inside the real Chunk.js coastal waystation and one separate public sundial outside, all rendered with baked game materials. Device size, sand, water, pendulum, and shadow positions encode no duration or status and cannot prove browser time, chain time, cache age, expiry, or contract acceptance.

SAME KEY OR DIFFERENT PDA

A readable stored key is normally reused; a missing or invalid record creates a new authority

Plugin mode first tries to open the browser record saved for the connected owner. The saved private bytes must rebuild the public address recorded beside them, and the copied expiry must be a usable number. When those checks succeed, even a near-expiry path keeps the same plugin Keypair and asks setup to use the same authority. The same owner and authority select the same deterministic PlayerSession address.

When that plugin record is missing, malformed, inaccessible, contains invalid private bytes, or rebuilds a different public address, the loader returns no reusable key. Plugin setup then generates a new Keypair. That different authority selects a different PlayerSession PDA and leaves any old authority balance and old PDA untouched. The implementation cannot rediscover private bytes that the browser can no longer read.

Local Game Wallet has a different boundary: it does not generate a separate replacement session authority in this flow. It reloads the persistent owner Keypair and selects the owner-owner Session PDA. On a cache miss, an account fetched at confirmed commitment is reused only when it also passes the client's browser-time 15-minute check; this is still not a consumer's chain-time verdict. If the Local Game Wallet key is unavailable or does not match the owner, the flow reports that wallet unavailable rather than silently creating a new session key. Rotation and refresh are different operations, and neither is revocation.

Stored plugin key decodes reuse same Keypair

Near-expiry setup still uses stored.keypair, so PDA selection remains the same.

Stored plugin key cannot be loaded Keypair.generate()

Only the plugin path generates a new authority and therefore selects a different PDA.

Local Game Wallet authority remains owner

The flow has no separate session-key rotation branch and reports unavailable when the persistent key cannot be used.

Old state unchanged by new-key setup

No old Session account or balance is an input to the new PDA transaction.

Canonical NiceChunk villager boy returns an intact copper key to the recess of a standing gray plinth while the canonical villager girl hammers a differently shaped copper key beside a second plinth; a cracked empty wooden cradle and fragment sit between them.
The generated real-Chunk.js locksmith yard uses baked game materials to stage intact-key reuse beside a separate replacement location while both stone plinths remain standing. The scene cannot verify browser storage, key parsing, PDA derivation, refresh or rotation success, and the new key does not visually close, drain, refund, or recover the old record.

CHAIN FIRST, BROWSER COPY SECOND

Setup has an atomic chain transaction followed by a non-atomic browser secret write

Fresh plugin setup builds one Solana transaction. Depending on existing state, that transaction can initialize a Profile, transfer the authority's target difference, and create or refresh PlayerSession. Solana treats those instructions atomically: if one executed instruction fails, this transaction does not keep only the transfer while rolling back the later Session instruction. Step 2.13 will separate the exact fee and rent charges inside and around that transaction.

The browser lifecycle has another boundary. The wallet helper signs, broadcasts, and returns after RPC reports the transaction at confirmed commitment. Confirmed is stronger than processed, but it is not the same status label as finalized. Only after the helper observes confirmed does setup call the separate browser-storage function. That later localStorage.setItem operation is outside the Solana transaction, so unavailable storage, quota failure, an exception, or a page close can still leave funds and a PDA without a durable browser copy of the private key.

Transport or RPC failure can also make the client result uncertain after bytes were submitted. An error message alone proves neither rejection nor success. If wallet activity or the error report exposes a transaction signature, an ordinary player can inspect that public signature with a trusted explorer on the selected Solana network. A trusted explorer needs no private key.

The reviewed normal modular Play UI does not guarantee that it will show the attempted session-authority address or canonical PDA, and it provides no PDA-derivation or session-balance sweep button. If the signature or required public identifiers are unavailable, the result remains unknown. Do not approve the same setup repeatedly in the hope that one attempt works; retain non-secret wallet activity and error details and use an official support path instead.

Inside the transaction optional Profile + transfer + Session setup

These chain instructions succeed or roll back together.

After confirmed helper return storeGameplaySession

The separate browser write starts after the helper observes confirmed; this page does not relabel that observation finalized.

Commitment boundary confirmed is not the same label as finalized

The reviewed helper accepts confirmed or finalized status when asked for confirmed commitment.

Error after submission outcome may be unknown

Do not equate a missing client confirmation with a rejected transaction.

Blind retry risk another authority balance and PDA rent

Possible when the first secret was not stored and a new Keypair is generated.

Normal modular UI boundary signature and attempted authority are not guaranteed visible

No normal PDA-derivation or session SOL sweep control is exposed by the reviewed modular Play UI.

Canonical NiceChunk villager boy holds a blank pale receipt tile beside a stone-and-iron press and solid record block while the canonical villager girl pushes a copper key into a crooked half-open wooden drawer and pauses a handcart carrying a closed copper box.
The generated real-Chunk.js waystation uses baked game materials to place a public-record metaphor before a separate jammed storage drawer and held-back retry cart. Spatial order is not execution evidence: the scene cannot prove broadcast, confirmation, program success, localStorage success or failure, key loss, funding, or any real player's result.

VERIFY ADDRESS, NETWORK, FRESHNESS, PURPOSE

A stale or surprising SOL number needs a fixed verification order

First identify the number's purpose. A panel target came from browser storage; the account HUD reads the connected owner; plugin Session status reads the stored authority; PDA lamports belong to the data account. If the wallet mode is Local Game Wallet, owner and authority addresses match, but target and PDA rent still remain separate.

Second verify context: copy the full public address that is actually visible, confirm the selected cluster and RPC endpoint, and note whether the read uses confirmed commitment. When the interface offers a visible refresh, use it instead of trusting a 30-second HUD cache or 60-second Session status cache. The source API has a force option, but the reviewed normal modular Play UI does not expose a guaranteed player-facing force control. The HUD may deliberately preserve a prior known balance as stale when refresh fails, which is useful presentation but not a fresh chain fact.

Third use only the public evidence you actually have. If wallet activity or an error report exposes a signature, inspect it with a trusted explorer on the same network, then inspect a known authority address. Deriving and reading the canonical PlayerSession PDA requires the public owner and authority identifiers and tooling that normal modular Play does not guarantee. If those identifiers are missing, stop at unknown rather than filling the gap with a browser target or another approval. A balance proves lamports at one address, not a usable pass; a nonempty PDA proves an account exists, not that a consumer will accept it.

1 · Purpose target, owner, authority, or PDA

Name the object before comparing numbers.

2 · Address full Base58 public key

A shortened label can hide that owner and authority differ.

3 · Context cluster, RPC, commitment, freshness

A valid balance on the wrong network still answers the wrong question.

4 · Causal evidence use only available public identifiers

Signature, authority, PDA, and chain-time checks prove different facts; missing identifiers leave the outcome unknown.

UI boundary no guaranteed force or PDA tool

The source API can force a status read, but the reviewed normal modular UI does not expose every diagnostic operation.

Canonical NiceChunk villagers inspect an unmarked target frame, dark ring seal, blank receipt tile, dusty glass vessel, open copper box, wooden key tray, and gray key-recess plinth across one bench, with an untouched refill crate and misty signal tower beyond.
The generated real-Chunk.js inspection station and baked game materials arrange several evidence metaphors without connectors or labels. Their order encourages verification, but no ring, tile, water line, box content, key, recess, crate, or distant tower is an actual address, signature, cache value, balance, RPC response, Session field, error cause, or refill decision.

RECOVERY DEPENDS ON THE KEY

Expiry, logout, lost secrets, and compromised owners require different responses

If only an independent plugin session key is exposed, stop using that key. Chain expiry stops the old record in the audited Session-aware consumers, but anyone holding the private key can still race to move remaining SOL. Those lamports may be movable only when a safe, usable key copy still exists; movement is not guaranteed, and the reviewed normal Play UI provides no session-balance sweep button. Never paste the private key into an unknown website, explorer, form, or support chat.

If the only private-key copy is lost or successfully deleted, the public address, balance, and PDA can remain while the normal browser flow can no longer sign for them. Reconnecting in plugin mode may generate a new authority and rent-funded PDA, but cannot recover the old private key or automatically transfer old SOL. The Player Program dispatch exposes create or refresh and no reviewed Session close, revoke, deactivate, or rent-refund handler. Step 2.13 will inventory which other account types do have exact close or recovery paths.

If the owner wallet or Local Game Wallet key is compromised, old-Session expiry is not a complete stop-loss because that key can approve future setup; Local Game Wallet can satisfy both roles itself. Stop approving new requests and follow the trusted wallet provider's incident procedure. Logout only attempts local prefix deletion, can fail silently, can reach other owner suffixes on the same origin, and changes no chain state. Any attempt to move assets can race an attacker, so this page does not promise recovery.

Independent Session key exposed stop using it; old audited access ends at chain expiry

Anyone with the private key may still race to move remaining SOL, and expiry performs no sweep.

Independent key lost new key does not recover old funds

Address and PDA can remain without a usable secret.

Owner or Local Game Wallet compromised expiry is insufficient

The long-lived key can approve or satisfy future setup roles.

Logout best-effort browser cleanup only

No Session revoke, PDA close, rent refund, or authority-balance sweep.

Safe recovery boundary possible only with a safe usable key; never guaranteed

Current Play has no sweep button, and private keys must never be pasted into an unknown site or chat.

Canonical NiceChunk villager boy checks a pale receipt beside intact stone records and copper boxes while the canonical villager girl keeps a new copper key and closed backup book near a cracked cradle, swept empty cubbies, a closed gate with hourglass, and an exposed dark chest.
The generated real-Chunk.js recovery yard uses baked game materials to keep gate, cubbies, old records, old boxes, cracked cradle, replacement key, backup book, and exposed owner chest visibly separate. These props do not prove expiry, rejection, logout, secure deletion, compromise, backup validity, replacement, refund, revocation, close support, or recovery of old funds.

VERIFY EACH DECISION

Match every balance, timer, retry, and recovery claim to its actual source

Each formula is tagged to one chapter and translated into plain language. Every source excerpt is copied as one continuous literal range and includes a walkthrough. Repository source, dated static-production bytes, and retained Devnet observations remain separately labeled so a client default is never promoted into a permanent chain fact.

Four separate money objects

moneyContext = { browserTarget, ownerBalance(owner), authorityBalance(sessionAuthority), rentLamports(PlayerSessionPDA) }

The braces group questions, not funds. A value from one member cannot be used as evidence for another without the addresses and account type matching.

browserTarget
A local policy value that creates no transaction.
ownerBalance
Spendable lamports at the owner public key.
authorityBalance
Spendable lamports at the gameplay signer public key.
rentLamports
Lamports held by the separate program-owned data account.

SOL and lamport conversion

lamports = SOL × 1,000,000,000; 0.1 SOL = 100,000,000 lamports

The conversion changes units only. It does not identify which account owns the resulting lamports or prove they were transferred.

Configured target selection

selected = ownerTarget when present, otherwise defaultTarget; target = floor(selected) when finite and at least 100,000,000, otherwise 100,000,000

The browser selects one saved value rather than merging two values. It then accepts only a finite number at or above the minimum. This is browser policy, not an on-chain Session field.

Current top-up branch

topUp(balance,target) = balance < 0.1 SOL ? max(0, target − balance) : 0

This operation order explains why a balance above the minimum is not automatically raised to a larger target.

Worked low-balance example

target 0.50 SOL − balance 0.06 SOL = requested top-up 0.44 SOL

This arithmetic illustrates the source branch only. A request still needs owner approval, successful execution, and a fresh balance read before it becomes evidence of funds received.

Plugin wallet funding shape

pluginMode: ownerAddress differs from authorityAddress; each address has its own balance

The two balances belong to different addresses; this does not claim statistical independence. A transfer can move lamports between them, but a Session record does not merge ownership or cap the authority key.

Local Game Wallet funding shape

localMode: owner = sessionAuthority ⇒ ownerBalance = authorityBalance for the same address; PDA rent remains separate

Equality here is address identity, not a safety guarantee. The one browser key controls the full address balance and both setup roles.

Requested browser expiry

requestedExpiresAt = floor(Date.now() / 1000) + 28,800

This is a client request. The Player Program accepts any supplied expiry that is strictly in the future at chain execution; it does not impose an eight-hour maximum.

Plugin reuse safety window

localReuseTimeOK = storedExpiresAt > browserNow + 900 seconds

Passing this copied-time predicate does not validate the Session record's full contents or guarantee a later consumer accepts it.

Cache windows are not authorization

age(ready result) < 300s; age(Session status) < 60s; age(owner-balance result) < 30s

Each inequality decides whether the browser may reuse local data. None changes expiresAt in the public account.

Authoritative chain-time rule

setupOrUseTimeOK = expiresAt > Clock.unix_timestamp

Equality is already expired. Consumers can still reject other identity, ownership, or permission conditions even when this comparison passes.

Same-key PDA selection

P_session = PDA(PlayerProgram,["session",owner,authority]); same owner + same authority ⇒ same address

Selecting the same address does not guarantee refresh succeeds; stored relationships and current Profile checks still apply.

Replacement-key PDA selection

authority_new ≠ authority_old ⇒ P_session,new ≠ P_session,old; old state unchanged

The new setup transaction has no automatic old-account close, SOL sweep, rent refund, or revocation side effect.

Atomic chain portion

chainCommit = atomic(optional Profile init + optional authority transfer + Session create/refresh)

If that transaction executes with an instruction error, its state changes roll back together. This does not make later browser storage atomic with it.

Two-system persistence boundary

helper observes confirmed → browser attempts localStorage.setItem(private key); the chain transaction is not atomic with the browser write

Confirmed is not relabeled finalized here. A browser-storage failure after the helper's confirmed observation can leave a funded authority and public PDA without a durable browser key copy.

Unknown result is not failure

after-submit client error: possible result = confirmed, rejected, blockhash-expired, or still unknown; do not retry blindly

This is a list of possible diagnostic states, not a probability claim. A public signature can narrow the list; when the normal UI provides no signature or authority identifier, the honest result remains unknown.

A balance statement needs four coordinates

balanceClaim = (fullAddress, cluster, commitment, observedAt)

Without those coordinates, two correct values can appear contradictory because they describe different keys, networks, or cache ages.

Evidence order after setup

when public identifiers exist: inspect signature → inspect known authority → inspect derived Session PDA; missing identifiers → result stays unknown

The order does not mean one successful step proves all later steps. Normal modular Play does not guarantee every identifier or a PDA tool, and a confirmed transfer can fund an authority whose Session is missing, expired, or rejected.

Independent Session-key expiry boundary

independent authority expires on chain → audited Session access stops; key control and any remaining SOL do not move automatically

Remaining SOL may be movable only when a safe usable key copy still exists, but movement is not guaranteed and can race an attacker. Expiry does not transfer the balance anywhere.

Logout effect boundary

logoutEffect = bestEffortLocalDeletion + redirect; chainMutation = 0

The zero refers to the reviewed logout handler's chain transactions. Storage deletion can also fail and is not verified.

Owner compromise exceeds one expiry

ownerSecretCompromised ⇒ attacker can authorize future setup; oldSessionExpiry ≠ complete containment

Local Game Wallet is the broadest version because the same compromised key fills owner and sessionAuthority roles.

The Play panel saves a target and names its boundary

JavaScript play/play-chain-session.js
    elements.sessionFundingForm?.addEventListener("submit", (event) => {
      event.preventDefault();
      const sol = Math.max(MINIMUM_SESSION_SOL, Number(elements.sessionFundingAmount?.value) || MINIMUM_SESSION_SOL);
      state.sessionLamports = Math.trunc(sol * LAMPORTS_PER_SOL);
      saveSessionLamports(state.walletAddress, state.sessionLamports);
      saveSessionAcknowledged(state.walletAddress);
      closeSessionPanel(true);
      render();
      appendChainEvent(`Session funding target set to ${formatSol(state.sessionLamports)} SOL for ${ownerLabel()}.`);
      setStatus(`Session funding target set to ${formatSol(state.sessionLamports)} SOL. It is a top-up target, not a spending cap or authorization limit.`);
    });

Submitting this form clamps and stores a browser number, updates presentation, and explicitly calls it a target. This continuous range contains no transfer or RPC balance read.

  1. Lines 1–4

    Turn the selected SOL policy into an integer lamport target no lower than 0.1 SOL.

  2. Lines 5–8

    Save owner-scoped browser preferences and redraw the panel without constructing a transaction.

  3. Lines 9–11

    Tell the player that the value is a top-up target rather than a cap or authorization limit.

The modular Play Session HUD renders the local target

JavaScript play/play-chain-session.js
    const sessionText = state.sessionLamports > 0 ? `Session: ${formatSol(state.sessionLamports)} SOL` : "Session: not funded";
    const rpcText = state.rpcMode === "custom"
      ? ui("main.profile.rpcCustom", "Custom Devnet RPC")
      : state.rpcMode === "helius"
        ? ui("main.profile.rpcHelius", "Helius RPC")
        : ui("main.profile.rpcPublic", "Public RPC");
    if (elements.accountName) elements.accountName.textContent = identity.name || profile.name || "Local Miner";
    if (elements.accountLevel) elements.accountLevel.textContent = `Lv. ${levelFromProfile(profile)}`;
    if (elements.accountTitle) elements.accountTitle.textContent = identity.title || chainModeLabel(state.chainMode);
    renderWalletBalance();
    if (elements.accountSessionBalance) elements.accountSessionBalance.textContent = sessionText;

The visible Session text is formatted directly from state.sessionLamports, while the owner wallet balance is rendered by a separate function. This is literal evidence that “Session: X SOL” is the local target presentation rather than either RPC balance.

  1. Line 1

    Build the visible Session label directly from the in-memory sessionLamports target or show not funded when it is zero.

  2. Lines 2–7

    Select Public, Helius, or Custom Devnet RPC presentation, then prepare name, level, and title without changing the Session target value.

  3. Lines 11–12

    Render the owner wallet balance separately, then place the prebuilt target-derived Session text in its own HUD element.

The owner HUD asks getBalance for walletAddress

JavaScript play/play-chain-session.js
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: "nicechunk-wallet-balance",
        method: "getBalance",
        params: [walletAddress, { commitment: "confirmed" }],
      }),
      signal: controller?.signal,
      cache: "no-store",

The Play balance request is tied to walletAddress and confirmed commitment. In plugin mode that connected owner address can differ from the stored session authority.

  1. Lines 1–5

    Build one JSON-RPC getBalance request for the connected wallet address at confirmed commitment.

  2. Lines 6–8

    Attach the request-abort signal and disable HTTP response caching; the separate in-memory UI cache still exists.

PlayerSession allocation calculates separate rent

Rust programs/nicechunk_player/src/lib.rs
    let rent = Rent::get()?;
    let lamports = rent.minimum_balance(PlayerSession::LEN);

    if player_session.lamports() == 0 {
        let create = system_instruction::create_account(
            payer.key,
            player_session.key,
            lamports,
            PlayerSession::LEN as u64,
            program_id,
        );

The Player Program calculates rent for the fixed Session data length and creates the PDA with that lamport allocation. This is not a transfer to sessionAuthority.

  1. Lines 1–2

    Ask the chain Rent sysvar for the minimum balance of the PlayerSession allocation.

  2. Lines 4–11

    When the PDA has no lamports, build a payer-funded account creation for the program-owned data account.

The public setter clamps the target to the client minimum

JavaScript src/chain/nicechunkChain.js
export function setConfiguredGameplaySessionFundingSol(value, owner = null) {
  const parsed = Number(value);
  const lamports = Number.isFinite(parsed)
    ? Math.max(minimumSessionFundingLamports, Math.ceil(parsed * lamportsPerSol))
    : minimumSessionFundingLamports;
  if (hasLocalStorage()) localStorage.setItem(sessionFundingStorageKey(owner), String(lamports));
  return lamports / lamportsPerSol;
}

The setter normalizes a browser preference and stores it when localStorage exists. It neither reads nor changes the authority address balance.

  1. Lines 1–5

    Convert finite SOL input to rounded-up lamports and clamp it to the 0.1 SOL minimum.

  2. Lines 6–7

    Save the target under the selected suffix and return the normalized SOL display value.

Owner-specific target falls back to default then minimum

JavaScript src/chain/nicechunkChain.js
function getConfiguredGameplaySessionFundingLamports(owner = null) {
  if (!hasLocalStorage()) return minimumSessionFundingLamports;
  const ownerValue = localStorage.getItem(sessionFundingStorageKey(owner));
  const defaultValue = localStorage.getItem(sessionFundingStorageKey(null));
  const parsed = Number(ownerValue ?? defaultValue);
  return Number.isFinite(parsed) && parsed >= minimumSessionFundingLamports
    ? Math.floor(parsed)
    : minimumSessionFundingLamports;
}

The nullish operator selects the owner value when present and otherwise the default; the final branch rejects invalid or below-minimum values.

  1. Lines 1–4

    Use the minimum without storage, otherwise read owner-specific and default browser keys.

  2. Lines 5–8

    Select one value and accept it only when finite and at least the minimum.

The top-up helper checks the minimum before the target

JavaScript src/chain/nicechunkChain.js
async function fundGameplaySessionIfNeeded(provider, sessionAuthority, sessionBalance, conn) {
  if (sessionBalance >= minimumSessionFundingLamports) return;
  const targetLamports = getConfiguredGameplaySessionFundingLamports(provider.publicKey);
  if (sessionBalance >= targetLamports) return;
  const lamports = targetLamports - sessionBalance;
  if (lamports <= 0) return;
  const tx = new Transaction();
  tx.add(SystemProgram.transfer({
    fromPubkey: provider.publicKey,
    toPubkey: sessionAuthority,
    lamports,
  }));
  await signAndSendWalletTransaction(provider, tx, conn);
}

Operation order is decisive: a balance at or above 0.1 SOL returns before a higher target is considered. A lower balance may request the positive difference from owner to authority.

  1. Lines 1–4

    Exit at the minimum threshold; only a lower balance loads and compares the configured target.

  2. Lines 5–6

    Calculate and validate a strictly positive difference.

  3. Lines 7–13

    Build the owner-to-authority System transfer and ask the wallet helper to submit it.

Local Game Wallet status has one address and no separate target

JavaScript src/chain/nicechunkChain.js
function createLocalGameWalletStatus(owner, balanceLamports = null, expiresAt = null) {
  const ownerKey = owner?.toBase58?.() ?? String(owner ?? "");
  const normalizedBalance = Number.isFinite(balanceLamports) ? Math.floor(balanceLamports) : null;
  return {
    walletAvailable: true,
    owner: ownerKey,
    acknowledged: true,
    configuredFundingLamports: 0,
    minimumFundingLamports: 0,
    balanceLamports: normalizedBalance,
    balanceSol: normalizedBalance === null ? null : normalizedBalance / lamportsPerSol,
    publicKey: ownerKey,
    sessionAuthority: ownerKey,
    expiresAt,
    usesGameplaySession: false,
    walletMode: "localGameWallet",
  };
}

The status object uses ownerKey for owner, publicKey, and sessionAuthority, while reporting zero for the separate-session funding policy fields. Zero target does not mean zero wallet balance.

  1. Lines 1–3

    Normalize the one Local Game Wallet address and an optional fresh balance.

  2. Lines 4–10

    Report no separate funding target while retaining the actual one-address balance.

  3. Lines 11–17

    Put the same key in public and authority roles and label the wallet mode explicitly.

Local Game Wallet fills both setup roles with owner

JavaScript src/chain/nicechunkChain.js
  tx.add(createOrRefreshPlayerSessionInstruction({
    owner,
    sessionAuthority: owner,
    playerProfile,
    playerSession,
    expiresAt,
  }));

  await signAndSendKeypairTransaction(keypair, tx, conn);

The same public key occupies owner and sessionAuthority, and the persistent Local Game Wallet Keypair signs the transaction directly.

  1. Lines 1–7

    Construct Session setup with owner repeated in the authority role.

  2. Line 9

    Sign and submit using that same long-lived browser Keypair.

The Local Game Wallet secret is plain Base58 text

JavaScript src/localGameWallet.js
function storeLocalGameWalletKeypair(keypair, source = "created") {
  const address = keypair.publicKey.toBase58();
  const secretKey = bs58.encode(keypair.secretKey);
  const createdAt = String(Date.now());
  if (!hasLocalStorage()) throw new Error("Browser local storage is unavailable.");
  localStorage.setItem(localGameWalletKeys.address, address);
  localStorage.setItem(localGameWalletKeys.secretKey, secretKey);

Base58 changes representation but does not encrypt the full long-lived key. Its funding exposure therefore follows the one owner-and-authority address.

  1. Lines 1–4

    Derive the address, reversible Base58 secret text, and browser creation time.

  2. Lines 5–7

    Require localStorage and save both public address and private signing bytes.

Repository policy declares duration, skew, and cache windows

JavaScript src/chain/nicechunkChain.js
const sessionDurationSeconds = 8 * 60 * 60;
const sessionRefreshSkewSeconds = 15 * 60;
const lamportsPerSol = 1_000_000_000;
const minimumSessionFundingLamports = 100_000_000;
const sessionMinimumMiningLamports = 8_000_000;
const sessionAllowedActions = (1 << 1) | (1 << 2);
const sessionMaxActions = 10_000;
const miningComputeUnitLimit = 1_400_000;
const fallbackTransactionFeeLamports = 5_000;
const transactionConfirmationPollMs = 500;
const transactionBlockHeightPollMs = 1_000;
const transactionConfirmationTimeoutMs = 60_000;
const treeFellMaxChunkCount = 4;
const treeFellLeafRadius = 2;
const supportCollapseMaxOnChainBlocks = 48;
const bulkMiningModeDebug = 1;
const chunkDeltaCacheTtlMs = 60_000;
const gameplaySessionStatusCacheTtlMs = 60_000;
const gameplaySessionReadyCacheTtlMs = 5 * 60_000;

The relevant declarations set eight-hour duration, fifteen-minute skew, sixty-second status cache, and five-minute ready cache. The nearby sixty-second confirmation fallback is a different policy and must not be confused with Session lifetime.

  1. Lines 1–4

    Declare requested duration, refresh skew, SOL conversion, and funding minimum.

  2. Lines 5–16

    Declare unrelated action, fee, polling, and gameplay constants that share the same module.

  3. Lines 17–19

    Declare independent sixty-second Session status and five-minute readiness cache lifetimes.

Play declares a separate thirty-second owner-balance cache

JavaScript play/play-chain-session.js
const LAMPORTS_PER_SOL = 1_000_000_000;
const MINIMUM_SESSION_SOL = 0.1;
const BALANCE_CACHE_TTL_MS = 30_000;
const BALANCE_REFRESH_INTERVAL_MS = 30_000;
const BALANCE_REQUEST_TIMEOUT_MS = 12_000;
let localWalletModulePromise = null;
let localWalletModuleManifestPromise = null;
const walletBalanceCache = new Map();

The HUD's thirty-second cache and refresh interval are independent of the chain module's Session status and readiness caches. The twelve-second HTTP abort is yet another timer.

  1. Lines 1–2

    Declare UI unit conversion and the displayed minimum target.

  2. Lines 3–5

    Set the owner-balance cache, refresh interval, and request-abort policies.

  3. Lines 6–8

    Keep module-loading state and balance-cache data in separate in-memory values.

The Player Program compares requested expiry with chain Clock

Rust programs/nicechunk_player/src/lib.rs
    let clock = Clock::get()?;
    if expires_at <= clock.unix_timestamp {
        return Err(NicechunkPlayerError::InvalidInstruction.into());
    }

Setup accepts only a strictly future expiry at instruction execution. Browser Date.now and cache age are not used inside this contract decision.

  1. Line 1

    Read Solana's Clock sysvar during the program instruction.

  2. Lines 2–4

    Reject an expiry equal to or earlier than the chain Unix timestamp.

Stored plugin records fail closed to no reusable key

JavaScript src/chain/nicechunkChain.js
function loadStoredGameplaySession(owner) {
  try {
    if (!hasLocalStorage()) return null;
    const raw = localStorage.getItem(sessionStorageKey(owner));
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed?.secretKey || !Number.isFinite(parsed.expiresAt)) return null;
    const secretKey = base64ToBytes(parsed.secretKey);
    const keypair = Keypair.fromSecretKey(secretKey);
    if (parsed.publicKey && keypair.publicKey.toBase58() !== parsed.publicKey) return null;
    return { keypair, expiresAt: Number(parsed.expiresAt) };
  } catch {
    return null;
  }
}

Missing storage, malformed JSON, invalid fields, invalid secret bytes, or a mismatching public key all leave the caller with null rather than an accessible old Keypair.

  1. Lines 1–5

    Require readable localStorage and an owner-scoped serialized record.

  2. Lines 6–10

    Decode JSON and secret bytes, then reject an optional saved-public-key mismatch.

  3. Lines 11–14

    Return the recovered key and expiry, or null for any thrown decoding or storage error.

The setup path reuses stored keypair or generates one replacement

JavaScript src/chain/nicechunkChain.js
  const keypair = stored?.keypair ?? Keypair.generate();
  const expiresAt = nowSeconds + sessionDurationSeconds;
  const [playerProfile] = derivePlayerProfilePda(owner);
  const [playerSession] = derivePlayerSessionPda(owner, keypair.publicKey);
  const tx = new Transaction();

A decoded stored record keeps its exact authority even when the fast reuse branch was not taken. Only null storage causes Keypair.generate and therefore a different authority seed.

  1. Line 1

    Prefer the recovered Keypair and generate only when no usable stored record exists.

  2. Lines 2–5

    Request a new browser expiry and derive Profile and Session addresses from owner and selected key.

Local Game Wallet always derives Session from owner twice

JavaScript src/chain/nicechunkChain.js
async function getOrCreateLocalGameWalletGameplaySession(provider) {
  const owner = provider.publicKey;
  const keypair = getLocalGameWalletKeypair();
  if (!keypair?.publicKey?.equals?.(owner)) {
    throw new Error("Local game wallet is unavailable.");
  }

  const nowSeconds = Math.floor(Date.now() / 1000);
  const expiresAt = nowSeconds + sessionDurationSeconds;
  if (isGameplaySessionReadyCached(owner, owner)) {
    return { keypair, expiresAt, localGameWallet: true };
  }

  const conn = getNicechunkConnection();
  const [playerProfile] = derivePlayerProfilePda(owner);
  const [playerSession] = derivePlayerSessionPda(owner, owner);

This branch requires the persistent Local Game Wallet key to match owner, queries readiness with owner in both positions, and derives the same-key PDA. It has no separate authority generator.

  1. Lines 1–6

    Load the persistent key and refuse to continue if its public key differs from the provider owner.

  2. Lines 8–12

    Compute browser time and allow the five-minute same-key ready shortcut.

  3. Lines 14–16

    On a cache miss, derive the Profile and owner-owner Session addresses for RPC checks.

Fresh plugin setup places funding and Session setup in one transaction

JavaScript src/chain/nicechunkChain.js
  if (!profileAccount?.data?.length) {
    tx.add(createInitializePlayerInstruction(owner, playerProfile, ""));
  }
  const targetLamports = sessionBalance < minimumSessionFundingLamports
    ? getConfiguredGameplaySessionFundingLamports(owner)
    : sessionBalance;
  if (sessionBalance < targetLamports) {
    tx.add(SystemProgram.transfer({
      fromPubkey: owner,
      toPubkey: keypair.publicKey,
      lamports: targetLamports - sessionBalance,
    }));
  }
  tx.add(createOrRefreshPlayerSessionInstruction({
    owner,
    sessionAuthority: keypair.publicKey,
    playerProfile,
    playerSession,
    expiresAt,
  }));

The optional Profile initialization, optional target difference transfer, and create-or-refresh instruction are appended to the same transaction object before submission.

  1. Lines 1–3

    Add Profile initialization only when the fetched Profile has no data.

  2. Lines 4–13

    Select target by the minimum-entry rule and append an owner-to-new-authority transfer only for a positive shortfall.

  3. Lines 14–20

    Append Session setup for the exact selected authority and expiry to that same transaction.

Browser secret persistence happens after the wallet helper returns

JavaScript src/chain/nicechunkChain.js
  await signAndSendWalletTransaction(provider, tx, conn, [keypair]);
  const session = { keypair, expiresAt };
  storeGameplaySession(owner, session);
  markGameplaySessionReady(owner, keypair.publicKey, targetLamports);

The wallet helper first observes its requested confirmed commitment. Only then does the source call browser-storage persistence and mark the in-memory ready cache, making the private-key copy a second commit outside the transaction; confirmed is not relabeled finalized.

  1. Line 1

    Apply the extra authority signature, owner approval, broadcast, and helper observation at confirmed commitment before returning.

  2. Lines 2–3

    Assemble the local browser record and attempt to persist its private key after that confirmed observation.

  3. Line 4

    Mark readiness only after storeGameplaySession returns without throwing.

The wallet helper broadcasts before polling confirmed status

JavaScript src/chain/nicechunkChain.js
  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;
  }

In this provider branch the raw transaction is already submitted before status polling completes. The helper asks for confirmed, which can accept a confirmed or finalized status but does not require finalized specifically. A later error therefore needs reconciliation rather than an assumption that nothing landed.

  1. Lines 1–3

    Obtain the owner signature and broadcast serialized transaction bytes, receiving a signature string.

  2. Lines 4–8

    Poll that signature and blockhash lifetime while explicitly requesting confirmed commitment rather than finalized.

  3. Line 9

    Return the signature only after the helper's confirmed predicate succeeds; this wording does not upgrade it to finalized.

The normal modular error feed shows a reason, not every recovery identifier

JavaScript play/play-chain-session.js
    } catch (error) {
      state.chainMode = "adapter-error";
      const reason = readableError(error);
      logSubmissionFailure(action, pending, {
        stage: "exception",
        reason,
        error,
      });
      state.chainResults.set(pending.txId, { reason });
      const rolledBack = action === "mine" ? controls.rollbackTx?.(pending.txId) : null;
      if (!rolledBack) {
        appendSubmissionState(
          action,
          pending,
          `${pending.txId} chain adapter error: ${reason}.`,
          "error",
          { reason },
        );
      }
      render();
    }

The player-facing event text contains the local pending ID and readable reason. A separate structured console report may contain more when the error carries it, but this visible branch does not guarantee a signature, attempted authority, derived PDA, or sweep control.

  1. Lines 1–8

    Convert the thrown value into a readable reason and send richer details to the separate structured failure logger.

  2. Lines 9–18

    Store and display the reason around local rollback state without promising a transaction signature or authority address in the visible message.

  3. Lines 19–20

    Render the updated error state; this range exposes no PDA derivation or session-balance sweep action.

Session status can return a sixty-second cached snapshot

JavaScript src/chain/nicechunkChain.js
  const owner = provider.publicKey;
  const ownerKey = owner.toBase58();
  const cached = gameplaySessionStatusCache.get(ownerKey);
  if (!force && cached && Date.now() - cached.loadedAt < gameplaySessionStatusCacheTtlMs) {
    return { ...cached.status };
  }

The status cache is owner-keyed and bypassed by force. Without force, a valid cached object can be returned without a new balance or Session read.

  1. Lines 1–3

    Normalize the connected owner and retrieve its in-memory Session-status entry.

  2. Lines 4–6

    Return a copy when force is false and the entry remains within its declared TTL.

A failed HUD refresh can preserve the previous value as stale

JavaScript play/play-chain-session.js
      .catch((error) => {
        if (requestSerial === state.walletBalanceRequestSerial) {
          state.walletBalanceStatus = hadBalance ? "stale" : "error";
          renderWalletBalance();
          console.warn("NiceChunk wallet balance refresh failed", error);
        }
        return { ok: false, reason: readableError(error) };
      })

When a previous balance exists, a refresh failure marks it stale instead of clearing it. The number can remain useful on screen but is not a fresh RPC observation.

  1. Lines 1–3

    Handle only the newest request and choose stale when an earlier known balance exists.

  2. Lines 4–5

    Redraw that state and record the refresh error in the console.

  3. Lines 7–8

    Return a structured failure without changing it into a successful fresh read.

Plugin Session status reads the stored authority address

JavaScript src/chain/nicechunkChain.js
  let balanceLamports = null;
  try {
    balanceLamports = await getNicechunkConnection().getBalance(stored.keypair.publicKey, "confirmed");
  } catch (error) {
    reportRpcError(error, "session-balance");
    throw error;
  }

  const status = {
    walletAvailable: true,
    owner: ownerKey,
    acknowledged: hasAcknowledgedGameplaySessionFunding(owner),
    configuredFundingLamports,
    minimumFundingLamports: minimumSessionFundingLamports,
    balanceLamports,
    balanceSol: balanceLamports / lamportsPerSol,
    publicKey: stored.keypair.publicKey.toBase58(),
    expiresAt: stored.expiresAt,
    usesGameplaySession: true,
    walletMode: "pluginWallet",
  };

This read targets stored.keypair.publicKey, then returns owner and authority-related fields together. The object must not be interpreted as one merged wallet.

  1. Lines 1–7

    Read the stored authority's balance at confirmed commitment or surface the RPC failure.

  2. Lines 9–14

    Keep the owner identity and configured browser funding policy as separate status fields.

  3. Lines 15–21

    Attach the authority balance, copied expiry, public key, and explicit plugin mode.

The reviewed Player dispatch has create or refresh but no Session kill tag

Rust programs/nicechunk_player/src/lib.rs
    match tag {
        0 => initialize_player(program_id, accounts, payload),
        1 => update_position(program_id, accounts, payload),
        3 => set_backpack_style(program_id, accounts, payload),
        4 => create_or_refresh_player_session(program_id, accounts, payload),
        5 => set_equipped_backpack(program_id, accounts),
        6 => add_forging_xp(program_id, accounts, payload),
        7 => set_player_name(program_id, accounts, payload),
        8 => upsert_player_appearance(program_id, accounts, payload),
        9 => close_player_appearance(program_id, accounts),
        10 => initialize_invite_index_page(program_id, accounts, payload),
        11 => append_invite_registration(program_id, accounts, payload),
        13 => transfer_equipment_slot(program_id, accounts, payload),
        14 => swap_equipment_slots(program_id, accounts, payload),
        15 => consume_equipment_durability(program_id, accounts, payload),
        16 => release_equipment_to_market(program_id, accounts, payload),
        _ => Err(NicechunkPlayerError::InvalidInstruction.into()),
    }

Tag 4 creates or refreshes PlayerSession. Tags 13-15 manage equipment and durability; tag 16 releases equipment to Market. None closes a Session, while tag 9 closes Appearance only.

  1. Lines 1–6

    The first dispatch group initializes and updates Player state, with tag 4 as the sole PlayerSession create-or-refresh entry.

  2. Lines 7–12

    Name, appearance, and invitation operations occupy their own tags; tag 9 closes only PlayerAppearance.

  3. Lines 13–14

    Tags 13 and 14 transfer one equipment slot or swap two equipment slots without adding a PlayerSession lifecycle instruction.

  4. Lines 15–17

    Durability and Market release complete the current map, after which every unknown tag is rejected without a PlayerSession close path.

Logout scans every matching runtime prefix on this origin

JavaScript play/play-auth-session.js
export function clearWalletSession(storage = globalThis.localStorage) {
  for (const key of Object.values(walletSessionKeys)) remove(storage, key);
  const keys = [];
  try {
    for (let index = 0; index < storage.length; index += 1) {
      const key = storage.key(index);
      if (key) keys.push(key);
    }
  } catch {
    return;
  }
  for (const key of keys) {
    if (runtimeCachePrefixes.some((prefix) => key.startsWith(prefix))) remove(storage, key);
  }
}

Cleanup first attempts identity fields and then every matching prefix without restricting the owner suffix. Enumeration failure exits early, and this function sends no chain transaction.

  1. Lines 1–2

    Attempt removal of the fixed wallet-identity browser keys.

  2. Lines 3–11

    Enumerate storage keys, but abandon the prefix pass when enumeration throws.

  3. Lines 12–15

    Attempt removal for every recognized runtime prefix across all suffixes on the origin.

Per-key cleanup errors are swallowed

JavaScript play/play-auth-session.js
function remove(storage, key) {
  try {
    storage?.removeItem?.(key);
  } catch {
    // Logout still redirects even when browser storage is unavailable.
  }
}

The helper does not read back the key or report removal failure. Logout can continue even when a secret or target remains in browser storage.

  1. Lines 1–3

    Attempt one optional localStorage removal for the supplied key.

  2. Lines 4–7

    Catch and ignore an exception so redirect flow continues without a deletion guarantee.

IMPLEMENTATION EVIDENCE

Where these claims come from

Each claim is intentionally scoped to a concrete implementation path. These references are for verification, not decoration.

play/play-chain-session.js

Modular Play stores an owner-scoped funding target, renders `Session: X SOL` directly from state.sessionLamports, and separately requests the connected walletAddress balance at confirmed commitment.

src/chain/nicechunkChain.js

Plugin Session status requests getBalance for stored.keypair.publicKey, so its authority balance can describe a different address from the Play owner HUD.

programs/nicechunk_player/src/lib.rs

PlayerSession allocation calculates Rent::minimum_balance for PlayerSession::LEN and uses the owner payer to create or fund the separate PDA.

docs/audits/player-sessions-devnet-2026-07-18.json

The retained confirmed Devnet snapshot records 2,171,520 lamports as the observed rent minimum for its 184-byte Session accounts and explicitly does not provide authority balances.

src/chain/nicechunkChain.js

The target getter uses ownerValue ?? defaultValue, applies the 100,000,000-lamport floor, and the funding helper checks that minimum before loading a higher target.

play/play-chain-session.js

The visible form clamps its stored SOL target to at least 0.1 and tells the player that this value is not a cap or authorization limit.

programs/nicechunk_player/src/lib.rs

PlayerSession setup separately validates allowed_actions, future chain-time expiry, signer roles, and account relationships, so transferring SOL cannot by itself grant a consumer permission.

docs/audits/player-sessions-production-2026-07-18.json

The retained 2026-07-19 UTC static production byte audit records a 100,000,000-lamport minimum, owner-then-default lookup, and its fixed-GET privacy boundary.

docs/audits/player-sessions-derived-2026-07-18.json

The deterministic Player Sessions audit separates the client funding policy from ordinary signing capability and from PlayerSession record fields.

src/chain/nicechunkChain.js

Plugin setup uses a separate selected Keypair when available, while Local Game Wallet setup passes owner as sessionAuthority and reports one address in its status.

src/localGameWallet.js

Local Game Wallet persists its long-lived secret bytes as a reversible Base58 string under separate browser storage keys rather than an encrypted Session vault.

programs/nicechunk_player/src/lib.rs

The Player Program requires owner and session_authority signer roles; one matching public key can occupy both roles in the Local Game Wallet transaction.

docs/audits/player-sessions-devnet-2026-07-18.json

The dated Devnet snapshot observed five owner-equals-authority and five distinct-authority Session records without reading or possessing any corresponding secrets.

src/chain/nicechunkChain.js

Repository source declares the 28,800-second duration, 900-second skew, 60-second Session status cache, and five-minute Local Game Wallet readiness cache.

play/play-chain-session.js

Play independently declares a 30-second connected-wallet balance cache and refresh interval plus a 12-second RPC request abort.

programs/nicechunk_player/src/lib.rs

Session setup reads Clock and rejects expires_at less than or equal to clock.unix_timestamp, without using a browser cache timer.

programs/nicechunk_chunk/src/lib.rs

The audited Chunk action context reads the Solana Clock and passes Clock.unix_timestamp into PlayerSession validation at actual instruction use.

src/chain/nicechunkChain.js

loadStoredGameplaySession returns a Keypair only after storage, JSON, secret, expiry, and optional public-key checks; setup otherwise selects Keypair.generate.

sdk/nicechunk-player.ts

The SDK derives the canonical Session address from the ordered literal session seed, owner public key, and session-authority public key.

programs/nicechunk_player/src/state.rs

PlayerSession refresh validates the stored owner, authority, Profile, and configuration relationships before rewriting selected fields on the same account.

docs/audits/player-sessions-devnet-2026-07-18.json

One retained owner had three canonical Session PDAs in the dated snapshot, showing coexistence but not proving simultaneous usability or why the keys were created.

src/chain/nicechunkChain.js

Fresh plugin setup appends optional Profile initialization, optional owner-to-authority funding, and Session setup to one Transaction before calling the wallet helper.

src/chain/nicechunkChain.js

The wallet helper broadcasts and explicitly polls at confirmed commitment before getOrCreateGameplaySession calls storeGameplaySession; its predicate can accept confirmed or finalized status but does not require finalized specifically.

docs/audits/player-sessions-derived-2026-07-18.json

The retained derived audit records browser-secret storage and chain Session state as separate lifecycle surfaces rather than one atomic record.

docs/audits/player-sessions-production-2026-07-18.json

The static live-byte audit records storage field names and setup defaults but sent no transaction and read no browser storage, so it cannot prove any player's setup outcome.

play/play-chain-session.js

The reviewed modular Play exception branch renders a pending ID and readable reason while richer fields go to a separate structured logger; the visible message does not guarantee a signature, authority address, PDA derivation, or sweep control.

play/play-chain-submission-log.js

The structured failure report can extract a transaction signature only when an error, result, or pending record actually carries one, so the documentation must preserve an explicit signature-unavailable outcome.

play/play-chain-session.js

The connected-wallet HUD caches by RPC URL plus wallet address, marks a previously known balance stale after refresh failure, and uses confirmed getBalance requests.

src/chain/nicechunkChain.js

getGameplaySessionStatus supports a source-level force option and otherwise may return a 60-second cached owner status; the reviewed normal modular Play UI does not thereby guarantee a player-facing force or PDA tool.

src/rpcConfig.js

Runtime RPC configuration resolves the selected cluster and endpoint separately from any public address, making network context part of a balance claim.

scripts/capture-player-sessions-devnet.mjs

The retained Devnet capture pins shared context and block time while explicitly separating Session account observation from unqueried authority balances and private keys.

play/play-chain-session.js

The reviewed modular Play UI implements an owner-wallet balance refresh and readable failure presentation but does not expose a guaranteed player control for source-level Session force reads, PDA derivation, or authority-balance sweeping.

programs/nicechunk_player/src/lib.rs

The complete reviewed Player dispatch currently reaches tag 16: equipment transfer is tag 13, slot swap is tag 14, session-authorized durability consumption is tag 15, and equipment release to Market is tag 16. Tag 4 remains Session create-or-refresh, and no tag closes, revokes, deactivates, or refunds PlayerSession.

play/play-auth-session.js

Logout attempts identity removal and a prefix-wide runtime-key scan, can exit on enumeration failure, swallows per-key removal exceptions, and exposes no chain revoke or session-authority SOL sweep.

src/localGameWallet.js

The persistent Local Game Wallet private key uses storage keys outside the plugin Session prefix, so plugin Session cleanup is not owner-key recovery.

docs/audits/player-sessions-devnet-2026-07-18.json

All ten retained Session accounts were expired relative to their captured confirmed context time while the public accounts and their rent lamports still existed.