PLAYER, WALLET, AND CONTROL · STEP 13

Transaction Fees, Account Rent, and Recovery: Who Pays and What Can Return

A Solana transaction fee, a transfer to a gameplay signer, and lamports placed inside a program-owned data account are three different costs. This guide follows the current NiceChunk client and program source account by account: it identifies the transaction fee payer, the storage payer, the three implemented close paths, the records that currently retain their rent, and the exact private keys and account state needed before any recovery is possible.

62 min read
Canonical NiceChunk villager boy stands at a timber transaction gate with one rectangular copper piece suspended in a stone fee bowl while the canonical villager girl places separate copper blocks into an open stone account foundation; an owner chest, retained vaults, hourglass, and closed return compartment remain apart in a real Chunk.js coastal yard.
The real Chunk.js coastal scene uses canonical villagers and baked game materials to separate a small processing-cost gate, an ordinary owner chest, account-funding blocks, retained storage, elapsed time, and a possible program-controlled return compartment. No prop proves a fee, balance, payer, account owner, close instruction, refund, transaction status, or current chain state.
PLAYER QUESTION When NiceChunk asks for SOL, which address pays the network, which account keeps the storage lamports, and what can I actually recover after a cancel, logout, expired Session, browser reset, or lost key?
Learning path
Player, wallet, and control · Step 13 of 15
Audience
Players with no programming or blockchain background
Scope
Current repository client, Player, Chunk, Building, Core, Guardian, Civilization, and the unified Game namespaces for Backpack, Market, and Smelting
Configured source route
public/mainnet.json currently declares the Devnet cluster and routes Backpack, Market, and Smelting through the unified Game program. A configuration file identifies intended addresses; it does not by itself prove deployment bytes or a player's transaction
Close-path audit boundary
The reviewed Rust program tree contains direct account-lamport drains only for treasury-authorized PlayerAppearance close and pending Building upload cancellation. Market cancellation restores the item but retains the Listing account and its lamports. Dispatch maps and account creation paths were also reviewed so absence is stated only for this source revision
Rent boundary
Account byte lengths are source constants. Exact rent-exempt lamports are runtime results from the selected Solana RPC and must not be presented as permanent protocol prices
Failure boundary
Local rejection, preflight rejection, accepted execution failure, and an unknown timeout are separate outcomes. Only a signature status and transaction metadata can settle an accepted transaction's result and actual processing fee
Reviewed
20 July 2026 · literal repository source audit
Transaction fee
SOL charged for processing a submitted Solana transaction. The transaction message names one fee payer.
Fee payer
The signer whose ordinary address pays the transaction processing fee. This role does not automatically identify who funds a program-created account.
Instruction payer
The writable signer passed to a program as the source of lamports for account creation or a rent top-up. It may be the same address as the transaction fee payer, but that is a separate fact.
Rent-exempt minimum
The minimum lamport balance Solana reports for keeping a data account of a given byte length exempt from ongoing rent collection under the current runtime.
Storage deposit
Plain lamports held inside a program-owned data account at or above its current rent-exempt minimum. This guide uses the phrase to distinguish those lamports from a processing fee, not to promise a refund.
Program-derived address or PDA
A deterministic address controlled by a program through known seeds. A player's normal wallet signature cannot spend lamports from a program-owned PDA.
Close instruction
Program logic that validates authority, transfers an account's lamports to an allowed recipient, and makes the old data account unusable. A close path must exist in the program; wallet ownership of related game data is not enough.
Refund
Lamports transferred by a validated close path. It is the account's current lamport balance, not necessarily the exact amount originally paid, and the close transaction can still cost a network fee.
Retained rent
Lamports that remain in a program-owned account because the current instruction map has no applicable close path.
Ordinary signer balance
Spendable SOL at an owner or Session authority address. It is not the same balance as lamports held inside a Session, Listing, BuildSite, or other PDA.
Atomic execution
All program state changes in one accepted transaction succeed together or roll back together. The processing fee is outside the rolled-back game state.
Preflight
Client-requested simulation before broadcast. A preflight error does not demonstrate that a transaction was accepted and executed on chain.
Accepted execution failure
A transaction with a real signature whose status contains an error after cluster execution. Program writes and transfers roll back, while processing fees may remain charged.
Unknown result
A timeout, expiry, or transport failure where the browser did not establish final status. It is neither proof of success nor proof of refund.
Session authority
The ordinary Solana signer used for supported gameplay transactions. In plugin mode it is normally a separate browser-held key; in Local Game Wallet mode it is the owner key itself.
Recovery prerequisite
A fact that must still be available before recovery can work, such as the correct private key, an active Listing, an uploading manifest, or all required PDA addresses.

Key points

One transaction can have two payer roles

The outer transaction names a fee payer. A program instruction separately names the signer that funds a new or enlarged account. Always inspect both.

PDA rent is not a second wallet balance

Lamports inside a program-owned account keep its bytes allocated. The related player cannot move them with an ordinary wallet transfer.

Rent must be queried at runtime

Byte length comes from source, while the exact lamport minimum comes from the selected cluster and RPC. The onboarding value of 0.000005 SOL is only a fee approximation.

Execution failure rolls back game state, not necessarily fees

An accepted transaction with status.err keeps no partial account creation or transfer, but Solana processing fees may remain charged.

Only two reviewed NiceChunk paths drain account lamports

They are pending Building upload cancellation to a valid current Session authority and PlayerAppearance close to the configured treasury.

Sold Listings retain their account lamports

Purchase marks a Listing sold and transfers the item, but the current Market instruction does not close that Listing.

Logout and expiry perform no recovery

Browser cleanup sends no transaction. It cannot revoke a Session, close a PDA, move an old temporary-key balance, or restore a lost owner secret.

Recovery follows the remaining key and current account state

An owner key can rebuild deterministic addresses and create a new Session, but only the old temporary private key can sign for its ordinary SOL balance, and only an implemented close path can release PDA lamports.

TWO PAYER ROLES, THREE KINDS OF SOL MOVEMENT

Name the transaction fee payer before naming the account-storage payer

A Solana transaction message has one fee payer. NiceChunk's wallet helper explicitly assigns provider.publicKey, while its keypair helper assigns signer.publicKey. In plugin Session setup, the connected owner is the fee payer, the temporary Session key co-signs, and the owner may also transfer ordinary spendable SOL to that key. That transfer is neither a processing fee nor PDA rent.

Inside a program, a writable signer is separately passed as payer for create_account or a rent top-up. The Player setup instruction passes the owner as the PlayerProfile and PlayerSession storage payer. Mining and Building calls are signed and fee-paid by the Session key, and those programs also use that writable Session authority to fund new progress, ChunkBroken, BuildSite, manifest, or shard bytes. Local Game Wallet makes owner and Session authority the same key, so the two labels point to one address there without becoming one conceptual role.

Owner-wallet flows use the connected owner as fee payer when creating a Backpack, executing Smelting, creating or canceling a Market Listing, or buying. That owner is also the storage payer when the instruction creates a Backpack, Smelting progress record, Listing, or missing associated token account. A Market buyer may fund missing seller or treasury associated token accounts before an NCK purchase. Those token-account allocations are governed by the associated-token and Token programs; they are not refundable through a NiceChunk Listing close.

The client function named transactionSolSpendFromMeta measures the payer's positive balance decrease and takes the greater of that decrease and meta.fee. That total can include network fee, transfers, PDA funding, and token-account creation. The feeLamports field is the narrower processing-fee observation.

Plugin Session setup owner fee payer and storage payer; temporary authority co-signs

The same setup transaction may initialize Profile, top up the ordinary Session signer, and create or refresh PlayerSession.

Mining and Building Session authority fee payer and instruction payer

The temporary key can lose spendable SOL to both processing fees and account creation or growth.

Backpack, Smelting, and Market wallet flows connected owner fee payer

The owner is also the program payer where these reviewed instructions create a Backpack, progress record, Listing, or missing token account.

Do not merge fee + transfer + account lamports

All can lower one address in one transaction while remaining different economic events.

Canonical NiceChunk villager boy feeds one small rectangular copper piece into an iron fee brazier while the canonical villager girl pushes a separate cart of copper blocks toward a large stone record foundation; a dark owner chest and smaller copper box stand apart in the same real Chunk.js coastal yard.
The scene separates a small processing-cost metaphor from a much larger account foundation and two ordinary-address containers. Their sizes and contents are illustrative only and do not prove who signed, who paid, how much SOL moved, whether a transfer occurred, or whether any account was created.

BYTES ARE FIXED; THE LAMPORT MINIMUM IS QUERIED

Account rent is ordinary lamports held at a program-owned address

NiceChunk programs ask Solana Rent for minimum_balance(account length) when they create or enlarge a PDA. The browser onboarding code independently calls getMinimumBalanceForRentExemption for the selected RPC and byte length. That means a source constant such as 184 bytes is stable for this account version, while an exact SOL example is only a point-in-time result for a particular runtime.

A rent-exempt account is not a hidden player wallet. Its lamports belong to the account controlled by its program. The owner or Session key that originally funded it cannot spend those lamports with a normal System Program transfer. Return requires the owning program to expose and accept a close instruction.

A resize can require only the missing difference. PlayerEquipment and ChunkBroken use current account lamports and the new byte length to calculate a top-up before reallocating. A failed atomic transaction does not leave a successfully enlarged account behind, but a successfully enlarged account keeps the additional lamports until an applicable close path exists.

The onboarding constant NETWORK_FEE_SOL = 0.000005 is presentation policy, not a permanent Solana fee quote. Exact processing cost comes from transaction metadata after an accepted transaction; exact rent comes from a runtime rent query before creation or from the account's current lamports afterward.

PlayerProfile 773 bytes

Owner-funded, deterministic by owner, and no Player close instruction.

PlayerSession 184 bytes

Owner-funded during setup; expiry does not release its lamports.

PlayerEquipment and PlayerAppearance 7,040 and 9,612 bytes

Equipment has no close; Appearance can be closed only by the configured treasury.

UsernameIndex and InviteIndex 256 and 2,688 bytes

The reviewed Player dispatch provides creation and append paths but no release or close.

Backpack and Listing 8,048 and 216 bytes

Backpack and Listing have no close; cancellation restores the listed item but retains Listing lamports.

ChunkBroken 16-byte header + 3 bytes per capacity slot

It starts at capacity 64, grows by 64 up to 2,048, and the Session payer funds each required rent top-up.

BuildSite, manifest, and shard 160 bytes, 160 bytes, and 64 bytes + payload

Only uploading manifest and created shard accounts have the reviewed cancellation close path; BuildSite remains.

Canonical NiceChunk villager boy holds an unmarked wooden measuring frame beside three increasingly large stone record shells while the canonical villager girl sorts increasingly large stacks of rectangular copper blocks into separate foundation trays in a real Chunk.js coastal work yard.
Different shell sizes and copper stacks illustrate that account length and required storage funding vary, while the finished stone recess keeps its blocks inside the account metaphor. The scene encodes no rent value, cluster response, account balance, spendable wallet, or proof that any PDA exists.

REJECTION, FAILURE, AND UNKNOWN ARE DIFFERENT

Use four outcome classes before saying a fee was charged or refunded

Local validation can stop before a transaction exists, and a player can reject a wallet prompt before signing. Those paths do not execute on chain. NiceChunk sends raw transactions with skipPreflight: false, so a preflight simulation error also does not demonstrate an accepted on-chain transaction or an on-chain fee.

After the cluster accepts a signed transaction, its status can contain an error. Solana atomicity rolls back the program's account writes, transfers, account creation, and reallocations from that transaction. The processing work was still performed, so the network fee may remain charged. NiceChunk's polling helper preserves the signature inside TransactionStatusError instead of converting the result into a local-only failure.

A timeout, expired blockheight, browser crash, or transport error can leave the outcome unknown. The browser message alone cannot establish success, failure, or refund. Preserve the signature when one exists, query its status on the correct cluster, inspect meta.err and meta.fee, and read the affected accounts before retrying.

Blind retry is especially risky for setup or upload flows. The first transaction may already have funded a Session signer or allocated an account even if the browser did not receive confirmation. A new key or revision can then strand a second ordinary balance or second set of account lamports.

Local or wallet rejection no chain execution

No program state change is possible without a signed, submitted transaction.

Preflight rejection no accepted execution demonstrated

Do not infer an on-chain fee solely from the browser error.

Accepted status.err atomic game-state rollback; fee may remain

Use the preserved signature and transaction metadata.

Timeout or transport failure unknown until checked

Do not retry a fund or create operation only because the UI timed out.

Canonical NiceChunk villager boy stands beside an open stone press, a tiny ember basin, and two unchanged record crates while the canonical villager girl studies a blank receipt tile toward a distant hazy signal tower; an untouched parcel, closed inspection gate, and waiting handcart occupy separate parts of the same coastal court.
The untouched parcel, closed inspection gate, returned press, unchanged crates, blank tile, and waiting cart provide four outcome metaphors without declaring which one occurred. The tiny ember is not a fee destination, and no prop reports a real submission, signature, charge, rollback, confirmation level, timeout, or RPC status.

CREATION DOES NOT IMPLY PLAYER-CONTROLLED CLOSURE

Profile, Session, Equipment, indexes, and Backpack persist under their programs

PlayerProfile is a 773-byte PDA derived from the owner. The owner funds creation and can reconstruct the address after browser cache loss. There is no Player close tag, so retaining the owner key restores write authority but not a rent-refund path. Losing the owner key leaves public data readable and deterministic, but it removes the ability to authorize ordinary Profile updates.

PlayerSession is a separate 184-byte PDA derived from both owner and Session authority. Setup requires both signatures and uses the owner as payer. Expiry only causes Session-aware consumers to reject later actions; it does not close the PDA, return rent, revoke the ordinary key, or transfer that key's SOL. A new temporary key produces a different PDA and cannot sign for the old key's balance.

PlayerEquipment is 7,040 bytes and may be created or reallocated with owner funds. PlayerAppearance is 9,612 bytes and is covered by the next chapter's treasury-only close. UsernameIndex is 256 bytes and each InviteIndex page is 2,688 bytes. The current Player dispatch has no Equipment, Session, Profile, UsernameIndex, or InviteIndex close path; changing a name does not release the prior index allocation.

Backpack is an 8,048-byte PDA derived from owner plus backpack ID. The owner funds it, and the unified Backpack instruction map exposes no close. Normal browser recovery first checks its local cache, then reads the equipped Backpack pointer from PlayerProfile. That recovers an equipped Backpack when the owner and Profile remain available; it does not enumerate an unknown unequipped backpack ID.

Profile recovery owner public key rederives address

Owner private key is still required for writes; no close exists.

Session recovery old temporary secret controls old ordinary SOL

Owner can create a replacement Session but cannot sweep an old signer address or close the old Session PDA.

Equipment recovery owner relationship and deterministic PDA

Data can be found again; its stored rent is not player-withdrawable.

Name and invite records append or create without release

Allocated pages and prior indexes remain under Player Program control.

Backpack recovery known ID/address or equipped Profile pointer

The normal fallback does not reconstruct an unknown unequipped ID.

Canonical NiceChunk villager boy holds a dark owner key beside a central stone record cabinet while the canonical villager girl opens a separate anchored inventory locker containing two backpacks; an equipment cabinet, hourglass plinth, narrow index posts, and empty wooden cubbies remain distinct in a real Chunk.js coastal archive.
The key, durable record cabinets, anchored Backpack locker, and empty local cubbies illustrate rediscovery and persistence as different questions. Opening the locker is a data-recovery metaphor, not an account close, and the scene proves no PDA, owner signature, stored pointer, byte length, balance, refund, or close authority.

ITEM RETURN IS NOT AN ACCOUNT REFUND

Listing cancellation restores the item but retains rent; Appearance can close only to treasury

Creating a 216-byte Market Listing uses the seller as signer, fee payer in the browser flow, and Listing storage payer. The item is removed from its source custody and stored in the Listing. Cancel requires the same seller, an active Listing, a valid destination Backpack path, the Listing address, and the Market authority. The program restores the exact item, marks the Listing canceled, and decrements the seller's active count. It does not clear the Listing data or transfer the Listing's lamports.

Buying has a different custody outcome but the same account-persistence result. The buyer pays the purchase transaction fee and, for NCK, may create missing seller or treasury associated token accounts. Market moves the item into the buyer's Backpack and marks the Listing sold. Both sold and canceled Listing records remain allocated with their lamports because the current Market dispatch exposes no Listing-close instruction.

PlayerAppearance reverses the intuitive payer rule. The player's wallet funds its 9,612-byte allocation during upsert, but the stored policy and close instruction reserve reclaim authority for NICECHUNK_TREASURY_AUTHORITY. The signer and recipient must both be that treasury. The player owner cannot redirect those lamports to itself through the reviewed Player Program.

A real close returns an account's current lamports, not a separately tracked receipt for the original deposit. Market cancellation is a state transition rather than a close. PlayerAppearance close is itself a transaction with a fee payer, so even its treasury recipient's net wallet change need not equal an earlier estimated rent number.

Active Listing seller cancel returns the item, not Listing lamports

Requires active state, seller signature, valid Listing PDA, and valid Backpack destination accounts; the canceled Listing remains allocated.

Sold Listing marked sold, not closed

The 216-byte account and its lamports remain under the Market namespace.

PlayerAppearance treasury-only close to the same treasury

Funding the account does not grant the player a reclaim instruction.

Close transaction network fee remains separate

A returned account balance is not a promise of zero net cost.

Canonical NiceChunk villager boy returns a forged tool from an opened market crate to his backpack beside a separate tray of rectangular copper blocks, while the canonical villager girl holds a small owner key outside a barred stone treasury gate whose oversized copper lock and foundation blocks remain inside.
The market stall stages return of the listed tool to its seller, not return of the crate's storage lamports; the barred archive makes the player key visibly different from the treasury-sized lock. Neither side proves a real Listing state, treasury authority, Backpack capacity, lamport amount, close signature, Listing cleanup, or completed refund.

ONLY THE PENDING UPLOAD LAYER CAN BE RECLAIMED

Building cancellation closes uploading shards and manifest, not the BuildSite or active building

Creating a BuildSite allocates a persistent 160-byte account and uses the valid Session authority as instruction payer. Beginning an upload creates a 160-byte manifest; writing each shard creates 64 header bytes plus that shard's payload. The browser estimates missing-account rent at runtime and, in plugin mode, transfers the required total plus a 1,000,000-lamport buffer from owner to Session authority before the Session key pays creation transactions.

Cancellation accepts only the BuildSite's pending revision whose manifest status is UPLOADING. It validates the current Session into an owner, checks that owner against BuildSite and manifest, requires the complete expected shard-account list, closes each shard that already exists, and then closes the manifest. The persistent BuildSite is updated to remove its pending revision but is not closed.

Every closed shard and the manifest transfer their current lamports to the currently supplied valid Session authority, not directly to the owner wallet. A new valid Session for the same owner can authorize cancellation of an older pending upload because Building compares the recovered owner relationship, not equality with the original temporary key. The returned lamports therefore land at the new Session address used for cancellation.

Finalization changes the manifest to active and makes the revision the BuildSite's active building. The cancel instruction rejects non-uploading manifests, and the dispatch exposes no active-manifest, active-shard, or BuildSite close. The current browser invokes cancellation automatically when a pending upload does not match the payload being resumed; it does not expose a general player-facing account sweep.

BuildSite 160 bytes, persistent

Created by Session payer and never closed by upload cancellation.

Pending manifest 160 bytes, closeable while uploading

Owner, foundation, revision, status, and shard count must all validate.

Created shards 64-byte header + bounded payload

All expected addresses must be supplied; existing program-owned shards are closed.

Recipient current valid Session authority

The program does not transfer cancellation proceeds directly to owner.

Active building no reviewed close path

Active manifest and shards retain their lamports.

Canonical NiceChunk villager girl handles rectangular copper foundation blocks beside an unfinished timber scaffold and several shard crates while the canonical villager boy holds two separate keys between that pending work and a finished stone-and-timber cottage with a full closed storage rack.
The unfinished scaffold, loose shard crates, two keys, copper box, completed cottage, and full rack distinguish pending recovery from finalized persistence. Their arrangement does not prove revision status, owner recovery, Session validity, shard completeness, recipient address, returned lamports, or a cancellation transaction.

WORLD RECORDS AND GOVERNANCE RECORDS ALSO NEED EXPLICIT CLOSE LOGIC

Mining, Core, Guardian, Civilization, and Smelting accounts currently retain their rent

Chunk mining uses the writable Session authority as payer. A missing 128-byte Chunk PlayerProgress PDA is created for the recovered owner, and each affected ChunkBroken PDA starts at 208 bytes, then grows in 192-byte steps as its capacity rises by 64 records. Each growth tops up the account to the runtime minimum before reallocating. The Chunk instruction map has no close path for progress, ChunkBroken, rule tables, or foundation indexes.

Smelting is an owner-wallet flow in the current browser. Its program can create a separate 128-byte PlayerProgress PDA under the Smelting or unified Game program and can initialize a 9,936-byte RecipeTable. Neither the Smelting instruction map nor the unified namespace dispatcher provides a close instruction. A similarly named progress seed under a different program ID derives a different account.

Core accepts only GlobalConfig initialization and creates a 293-byte PDA. Guardian creates a 160-byte Registry and 288-byte Region records through treasury or registration payers. Its update instructions do not include removal with lamport return. The Region status layout contains a removed value, but the reviewed instruction dispatch exposes no close or stake-refund instruction, so a status constant must not be advertised as a reclaim path.

Civilization creates PowerSnapshot, CitizenPower, RuleBook, RuleSignature, RuleTally, and ExecutionReceipt accounts with the payer named by each instruction. Their sizes are 128, 144, 320, 136, 128, and 128 bytes. Publish, sign, tally, execute, snapshot, and settle are lifecycle transitions, not closes. None of the reviewed Civilization dispatch tags drains those PDA lamports.

Across the reviewed Rust tree, the only direct try_borrow_mut_lamports account drains are the two implementations explained in the Appearance and Building chapters. Market cancellation contains no such drain. This is a source-revision statement, not a guarantee that future upgrades can never add or change recovery logic.

Chunk PlayerProgress 128 bytes, Session-funded

Program-scoped progress remains after Session expiry.

ChunkBroken 208 bytes initially; up to 6,160 bytes at capacity 2,048

Session funds creation and each rent top-up; no close exists.

Smelting 128-byte progress and 9,936-byte RecipeTable

Owner or configured authority pays creation according to the instruction; no close tag exists.

Core 293-byte GlobalConfig

One initialization instruction and no close.

Guardian 160-byte Registry and 288-byte Region

Registration and updates expose no account close or stake-refund path.

Civilization six persistent PDA families

State finalization or execution does not release their account lamports.

Canonical NiceChunk villager boy inspects a quarry recess beside two separate stone record plinths while the canonical villager girl works at a smelter beside protected foundation frames; a root monument, Guardian tower, and civic hall stand on distinct foundations across one real Chunk.js coastal terrace.
The quarry records, smelter record, protected foundations, monument, tower, and civic hall represent separate program-owned account families that remain intact in the reviewed lifecycle. The scene is not chain evidence and proves no program ID, byte length, payer, recipe result, Guardian state, governance outcome, balance, or future close policy.

RECOVER THE KEY, THE RECORD, OR NEITHER

Logout changes browser state; recovery depends on which private key still exists

Modular Play disconnect clears wallet identity and runtime prefixes, including the stored plugin Session secret, then redirects. It sends no Solana instruction. The login page's Disconnect Wallet action clears only its wallet binding, and the Local Game Wallet provider's disconnect method is a no-op. The Local Game Wallet secret uses separate browser keys and is not removed by plugin Session-prefix cleanup.

The plugin Session secret is Base64 text in localStorage; the Local Game Wallet secret is unencrypted Base58 text. Those encodings are not encryption. If the plugin secret survives and the owner wallet remains available, setup normally reuses the same Keypair and refreshes its PlayerSession. If that only copy is deleted, the owner may establish a new Session but cannot sign an ordinary transfer from the old temporary address.

Browser cache loss does not erase deterministic public accounts. With the owner public key, a client can rederive Profile, Equipment, Appearance, Session candidates when the authority is known, and program-scoped progress. The equipped Backpack can be recovered through Profile. Unknown IDs, transaction signatures, pending revisions, and unequipped Backpacks may require independent records or RPC enumeration beyond the normal browser fallback.

Loss of the Local Game Wallet owner secret is more severe because that key is both owner and Session authority. The public address and chain records remain readable, but NiceChunk cannot derive the private key from them. A backup of the exact valid secret can restore signing control; the address, username, Profile, or support message cannot.

For compromise, stop using the exposed key. An independent plugin Session key can still sign ordinary Solana transfers until its funds move or the attacker moves them; NiceChunk Session expiry only blocks cooperating NiceChunk consumers. If the owner key is compromised, all owner-authorized assets and replacement Sessions are at risk. Never paste any private key into an explorer, form, support chat, or unknown recovery site.

Owner key retained; browser cache lost deterministic records can be rediscovered

Profile writes and a replacement Session remain possible; absent close paths stay absent.

Plugin Session secret retained ordinary authority SOL may be transferable

A valid owner relationship can refresh Session authorization; use a trusted signing environment.

Plugin Session secret lost old authority SOL cannot be signed by owner alone

Create a replacement Session; old Session PDA rent also has no close path.

Local Game Wallet secret lost no private-key reconstruction from address

Only an existing valid backup restores signing control.

Active Listing seller key plus destination Backpack path

Cancel can return the item while state remains active, but it does not return Listing lamports.

Pending Building upload owner plus valid current Session and all upload addresses

Cancel returns manifest and shard lamports to that Session authority.

Expired, sold, finalized, or no-close account no automatic refund

A state label, logout, or new key does not substitute for a close instruction.

Canonical NiceChunk villager boy stands beside swept-empty wooden cubbies, a closed gate, and an hourglass while old public stone plinths remain beyond them; the canonical villager girl holds a new copper key at a separate bench with a closed owner chest, backup book, old key, new Session plinth, and locked old copper box.
Empty cubbies, a closed gate, persistent public plinths, two distinct keys, a new plinth, and the still-locked old box separate logout, expiry, replacement, and old-key control. No prop proves private-key possession, backup validity, Session setup, balance access, account state, refund eligibility, or any recovery transaction.

VERIFY THE PAYER, ACCOUNT, STATE, AND RECIPIENT

Use formulas and literal source ranges to audit every SOL claim

Each formula belongs to one chapter. Every code excerpt below is copied as a continuous range from the current repository and followed by a plain-language walkthrough. Source behavior is evidence for this revision, not proof that a particular live transaction executed.

Separate one payer's total outflow

payerOutflow = processingFee + ordinaryTransfers + newAccountLamports + rentTopUps + otherInstructionOutflows

This is an accounting checklist, not a promise that every term appears. A balance delta can be larger than meta.fee because the transaction also moved or deposited lamports.

processingFee
The transaction metadata fee.
ordinaryTransfers
Spendable SOL moved to another ordinary address, such as a Session authority.
newAccountLamports
Lamports placed in newly allocated program or token accounts.
rentTopUps
Additional lamports required before an existing account can grow.

Query the current rent minimum

rentMinimum(bytes, cluster, context) = RPC.getMinimumBalanceForRentExemption(bytes)

The account length comes from the program version. The lamport result belongs to the selected runtime context and should be timestamped when published.

bytes
The account's allocated data length.
cluster
The selected Solana network.
context
The RPC commitment and observation context.

A resize funds only the missing difference

rentTopUp = max(0, rentMinimum(newBytes) - currentAccountLamports)

PlayerEquipment and ChunkBroken implement this shape before reallocating. The resulting lamports stay in the program-owned account.

newBytes
The intended allocation after growth.
currentAccountLamports
The account's balance before the resize.

Atomic failure boundary

acceptedStatusErr => gameWrites = 0, programTransfers = 0, accountCreates = 0, processingFee may remain

The zeroes describe committed effects from that failed transaction, not balances that existed before it.

acceptedStatusErr
A submitted signature whose cluster status contains an execution error.
may remain
Read meta.fee instead of assuming either zero or a fixed constant.

A deterministic address does not imply a refund path

dataRecovery = knownSeeds + correctProgramId + readableChain; lamportRecovery = applicableClose + validAuthority + validState + recipient

The two sides answer different questions. Re-deriving a Profile or Backpack address does not let the wallet spend its PDA lamports.

knownSeeds
Owner, IDs, revisions, coordinates, or other derivation inputs.
applicableClose
A close instruction that accepts this exact account type and state.

Market cancellation retains Listing lamports

listingLamportsAfterCancel = listingLamportsBeforeCancel; sellerReturnFromListing = 0

Market restores the stored item, marks the Listing canceled, and decrements active count without clearing data or draining the Listing account.

listingLamportsAfterCancel
The retained Listing account balance after its state changes to canceled.
sellerReturnFromListing
Lamports transferred out of the Listing account to the seller by cancellation; currently zero.

Pending upload cancellation return

sessionReturn = uploadingManifestLamports + sum(existingUploadingShardLamports)

BuildSite lamports and active-revision account lamports are excluded because this cancellation does not close them.

sessionReturn
Lamports credited to the current valid Session authority.
existingUploadingShardLamports
Balances of expected shard accounts that are already owned by Building.

ChunkBroken allocation growth

chunkBrokenBytes(capacity) = 16 + 3 × capacity, where capacity ∈ {64, 128, ..., 2048}

Initial length is 208 bytes and maximum length is 6,160 bytes. Each successful growth can require a rent top-up paid by the Session authority.

16
Fixed ChunkBroken header bytes.
3
Bytes per packed broken-block record.

Ordinary key-balance recovery rule

canMoveSignerSOL(address) = usablePrivateKey(address) AND acceptedTransfer

An owner relationship, Session PDA, username, public address, or support request cannot replace the private key for an ordinary System Program transfer.

usablePrivateKey
Signing material that can be loaded safely and actually matches the address.
acceptedTransfer
A signed transfer that reaches successful chain execution.

Wallet and keypair helpers assign different fee payers

JavaScript src/chain/nicechunkChain.js
async function signAndSendWalletTransaction(provider, transaction, conn = getNicechunkConnection(), extraSigners = []) {
  transaction.feePayer = provider.publicKey;

  if (typeof conn.prepareTransaction === "function") {
    await conn.prepareTransaction(transaction, { commitment: "confirmed" });
  } else {
    const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash("confirmed");
    transaction.recentBlockhash = blockhash;
    transaction.lastValidBlockHeight = lastValidBlockHeight;
  }

  for (const signer of extraSigners) transaction.partialSign(signer);

  if (typeof provider.signTransaction === "function") {
    const signed = await provider.signTransaction(transaction);
    const signature = await sendRawTransactionWithLogs(conn, signed.serialize(), "wallet");
    await confirmTransactionByHttpPolling(conn, {
      signature,
      blockhash: transaction.recentBlockhash,
      lastValidBlockHeight: transaction.lastValidBlockHeight,
    }, "confirmed");
    return signature;
  }

  if (typeof provider.signAndSendTransaction !== "function") {
    throw new Error("Wallet does not support transaction signing.");
  }
  const result = await provider.signAndSendTransaction(transaction);
  const signature = typeof result === "string" ? result : result?.signature;
  if (!signature) throw new Error("Wallet did not return a transaction signature.");
  await confirmTransactionByHttpPolling(conn, {
    signature,
    blockhash: transaction.recentBlockhash,
    lastValidBlockHeight: transaction.lastValidBlockHeight,
  }, "confirmed");
  return signature;
}

async 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 outer helper makes the wallet provider or supplied Keypair the fee payer. An extra signer can co-sign without becoming fee payer.

  1. Lines 1–12

    The wallet public key is assigned as fee payer before blockhash preparation and any extra partial signatures.

  2. Lines 13–36

    The wallet signs, sends, and waits for confirmed commitment.

  3. Lines 38–46

    The keypair path instead assigns the supplied signer as fee payer and signs directly.

Fresh plugin Session setup can combine transfer, storage creation, and two signers

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();
  const [profileAccount, sessionAccount] = await conn.getMultipleAccountsInfo(
    [playerProfile, keypair.publicKey],
    "confirmed",
  );
  const sessionBalance = accountLamports(sessionAccount);

  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,
  }));

  await signAndSendWalletTransaction(provider, tx, conn, [keypair]);

One owner-fee-paid transaction may initialize Profile, top up the ordinary temporary address, and create or refresh its Session PDA. The keypair is an extra signer.

  1. Lines 1–10

    Reuse or generate the Session key, derive its accounts, and read Profile existence plus ordinary authority balance.

  2. Lines 12–24

    Optionally add Profile initialization and an owner-to-authority System Program transfer.

  3. Lines 25–33

    Add Session setup and send with the wallet as fee payer plus the Session key as co-signer.

The onboarding client asks the selected RPC for rent

JavaScript play/play-onboarding.js
  const promise = fetch(rpcUrl, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "getMinimumBalanceForRentExemption",
      params: [bytes, { commitment: "processed" }],
    }),

The client sends the byte length to the configured RPC rather than deriving a permanent SOL number from source constants.

  1. Lines 1–3

    Send a JSON-RPC POST request.

  2. Lines 4–9

    Request the rent-exempt minimum for this byte length at processed commitment.

PlayerSession creation asks Solana Rent and uses the owner payer

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,
        );
        invoke_signed(
            &create,
            &[
                payer.clone(),
                player_session.clone(),
                system_program_account.clone(),
            ],
            &[&[
                PLAYER_SESSION_SEED,
                payer.key.as_ref(),
                session_authority.as_ref(),
                &[bump],
            ]],
        )?;
        return Ok(());
    }

The program queries rent for the fixed 184-byte layout and places that amount in the Session PDA using payer.key.

  1. Lines 1–2

    Read the current Rent sysvar and calculate the minimum for PlayerSession::LEN.

  2. Lines 4–10

    Build account creation using the instruction payer as funding source.

  3. Lines 11–25

    Sign for the PDA with owner and Session-authority derivation seeds.

Submission requests preflight

JavaScript src/chain/nicechunkChain.js
async function sendRawTransactionWithLogs(conn, serializedTransaction, context) {
  try {
    return await conn.sendRawTransaction(serializedTransaction, { skipPreflight: false });
  } catch (error) {
    await attachSendTransactionLogs(error, conn, context);
    throw error;
  }
}

A send error on this path is accompanied by preflight-enabled submission. It does not by itself demonstrate accepted execution.

  1. Lines 1–3

    Submit the signed bytes with preflight enabled.

  2. Lines 4–7

    Attach available diagnostic logs and preserve the failure.

Confirmed polling treats status.err as a signed transaction failure

JavaScript src/chain/nicechunkChain.js
  while (true) {
    const statusResponse = await conn.getSignatureStatuses([signature]);
    const status = statusResponse?.value?.[0] ?? null;
    if (status?.err) throw createTransactionStatusError(signature, status.err);
    if (hasReachedSignatureCommitment(status, commitment)) {
      return {
        context: statusResponse.context,
        value: { err: null },
      };
    }

The helper checks the submitted signature and keeps successful commitment distinct from a status that contains an execution error.

  1. Lines 1–4

    Poll the signature and throw a status error that retains the signature when execution failed.

  2. Lines 5–10

    Return success only after the requested commitment is reached without an error.

The client exposes actual meta.fee separately from total payer decrease

JavaScript src/chain/nicechunkChain.js
function transactionSolSpendFromMeta(transaction, payerKey, signature) {
  const meta = transaction?.meta;
  const preBalances = meta?.preBalances;
  const postBalances = meta?.postBalances;
  if (!Array.isArray(preBalances) || !Array.isArray(postBalances)) return null;
  const accountKeys = transactionAccountKeys(transaction);
  let payerIndex = accountKeys.findIndex((key) => key === payerKey);
  if (payerIndex < 0) payerIndex = 0;
  const pre = Number(preBalances[payerIndex]);
  const post = Number(postBalances[payerIndex]);
  const balanceDelta = Number.isFinite(pre) && Number.isFinite(post) ? Math.max(0, pre - post) : 0;
  const fee = Number(meta?.fee);
  const lamports = Math.max(balanceDelta, Number.isFinite(fee) ? fee : 0);
  if (!Number.isFinite(lamports) || lamports <= 0) return null;
  return {
    signature,
    payer: payerKey,
    lamports: Math.floor(lamports),
    feeLamports: Number.isFinite(fee) ? Math.floor(fee) : null,
  };
}

lamports is a broad payer-decrease measure; feeLamports is the actual processing-fee field when transaction metadata is available.

  1. Lines 1–8

    Find the payer inside the transaction balance arrays.

  2. Lines 9–13

    Calculate positive payer decrease and read meta.fee separately.

  3. Lines 14–20

    Return both the broad spend number and the narrower fee field.

Player dispatch exposes one Appearance close and no other account close

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()),
    }

The complete dispatch exposes Appearance close at tag 9, equipment-to-Market release at tag 16, and no Profile, Session, Equipment, UsernameIndex, or InviteIndex close.

  1. Lines 1–8

    Dispatch ordinary Player initialization, position, Backpack style, Session setup, equipment selection, XP, name, and Appearance updates.

  2. Lines 9–12

    Expose the sole account-close handler at tag 9 for PlayerAppearance, followed by invitation-index operations that do not close accounts.

  3. Lines 13–17

    Handle equipment transfer, swap, durability, and Market release, then reject unknown tags without another Player account close.

Backpack dispatch contains no Backpack close

Rust programs/nicechunk_backpack/src/lib.rs
    match tag {
        0 => initialize_backpack(program_id, accounts, payload),
        1 => append_mined_resource(program_id, accounts, payload),
        2 => remove_resource(program_id, accounts, payload),
        3 => append_market_resource(program_id, accounts, payload),
        4 => remove_resources(program_id, accounts, payload),
        5 => append_smelting_item(program_id, accounts, payload),
        6 => append_mined_resources_batch(program_id, accounts, payload),
        7 => Err(NicechunkBackpackError::UnverifiedForgeInstructionDisabled.into()),
        8 => forge_equipment_with_material_verification(program_id, accounts, payload),
        9 => issue_blueprint(program_id, accounts, payload),
        10 => transfer_backpack_item_to_equipment(program_id, accounts, payload),
        11 => transfer_equipment_item_to_backpack(program_id, accounts, payload),
        12 => configure_material_physics(program_id, accounts, payload),
        13 => record_mining_action(program_id, accounts, payload),
        14 => consume_smelting_resources(program_id, accounts, payload),
        _ => Err(NicechunkBackpackError::InvalidInstruction.into()),
    }
}

The unified Game program routes a namespace to this dispatch. It initializes and mutates Backpack custody but exposes no account close.

  1. Lines 1–8

    Initialization and inventory append or removal actions are listed.

  2. Lines 9–15

    Forging, blueprint, custody transfers, material physics, mining snapshots, and quantity-aware smelting consumption complete the map without a close tag.

Cancellation marks the Listing but does not close it

Rust programs/nicechunk_market/src/lib.rs
    let clock = Clock::get()?;
    {
        let mut data = listing.try_borrow_mut_data()?;
        ListingAccount::mark_canceled(&mut data, clock.slot, clock.unix_timestamp)?;
    }
    decrement_market_user_active(market_user, seller.key, clock.slot)?;
    Ok(())

After restoring the item, cancellation marks the Listing and decrements seller active count. It does not clear data, drain lamports, or close the account, so rent remains allocated.

  1. Lines 1–6

    Read chain time, borrow Listing data, and persist the canceled state with its slot and timestamp.

  2. Lines 7–8

    Decrease the seller's active Listing count and return without draining or closing the Listing account.

Purchase marks the Listing sold without closing it

Rust programs/nicechunk_market/src/lib.rs
    append_market_slot_to_backpack(
        program_id,
        market_authority,
        buyer,
        buyer_backpack,
        backpack_program,
        material_physics,
        &source_slot,
    )?;

    let clock = Clock::get()?;
    {
        let mut data = listing.try_borrow_mut_data()?;
        ListingAccount::mark_sold(&mut data, buyer.key, clock.slot, clock.unix_timestamp)?;
    }
    decrement_market_user_active(seller_market_user, seller.key, clock.slot)?;
    Ok(())

The item enters the buyer Backpack, the Listing records buyer and sale time, and seller active count decreases. No lamport drain or account close occurs.

  1. Lines 1–9

    Append the escrowed source record to the buyer's validated Backpack before changing Listing status.

  2. Lines 11–17

    Record buyer, sale slot, and sale time, then decrement seller active count without draining or closing the Listing.

Appearance close accepts only treasury and sends treasury the lamports

Rust programs/nicechunk_player/src/lib.rs
    if !treasury_authority.is_signer || !treasury_authority.is_writable || !recipient.is_writable {
        return Err(NicechunkPlayerError::InvalidTreasuryAuthority.into());
    }
    require_key_eq(
        treasury_authority.key,
        &NICECHUNK_TREASURY_AUTHORITY,
        NicechunkPlayerError::InvalidTreasuryAuthority,
    )?;
    require_key_eq(
        recipient.key,
        treasury_authority.key,
        NicechunkPlayerError::InvalidTreasuryAuthority,
    )?;
    require_key_eq(
        appearance.owner,
        program_id,
        NicechunkPlayerError::InvalidAppearanceOwner,
    )?;

    {
        let data = appearance.try_borrow_data()?;
        PlayerAppearance::validate_treasury_authority(&data, treasury_authority.key)?;
        let owner = PlayerAppearance::owner(&data)?;
        let (expected_appearance, _) =
            Pubkey::find_program_address(&[PLAYER_APPEARANCE_SEED, owner.as_ref()], program_id);
        require_key_eq(
            appearance.key,
            &expected_appearance,
            NicechunkPlayerError::InvalidAppearancePda,
        )?;
    }
    {
        let mut data = appearance.try_borrow_mut_data()?;
        data.fill(0);
    }
    let reclaimed_lamports = appearance.lamports();
    **recipient.try_borrow_mut_lamports()? = recipient
        .lamports()
        .checked_add(reclaimed_lamports)
        .ok_or(NicechunkPlayerError::InvalidAppearanceData)?;
    **appearance.try_borrow_mut_lamports()? = 0;

Both signing authority and recipient must be the configured treasury. The player's owner key is not accepted as the reclaim recipient.

  1. Lines 1–19

    Require writable treasury signer, treasury recipient equality, and Player ownership of Appearance.

  2. Lines 21–33

    Validate stored treasury policy and the owner-derived Appearance PDA.

  3. Lines 34–43

    Clear data, transfer all current lamports to treasury, and drain Appearance.

Pending upload cancellation closes only shards and manifest to Session authority

Rust programs/nicechunk_building/src/lib.rs
    {
        let mut data = build_site.try_borrow_mut_data()?;
        BuildSiteState::cancel_building(
            &mut data,
            &context.owner,
            global_config.key,
            revision,
            context.clock.slot,
        )?;
    }
    for account in shard_accounts {
        if account.owner == program_id {
            close_program_account(account, session_authority)?;
        }
    }
    close_program_account(manifest, session_authority)
}

fn close_program_account(account: &AccountInfo, recipient: &AccountInfo) -> ProgramResult {
    let account_lamports = account.lamports();
    let recipient_lamports = recipient.lamports();
    **recipient.try_borrow_mut_lamports()? = recipient_lamports
        .checked_add(account_lamports)
        .ok_or(NicechunkBuildingError::InvalidSystemAccount)?;
    **account.try_borrow_mut_lamports()? = 0;
    account.try_borrow_mut_data()?.fill(0);
    Ok(())
}

The BuildSite is updated, each existing program-owned shard is drained, and the manifest is drained. The recipient passed by this instruction is session_authority.

  1. Lines 1–10

    Clear only the pending revision on the persistent BuildSite.

  2. Lines 11–16

    Close existing shards and then the manifest to the current Session authority.

  3. Lines 19–28

    The close helper adds all account lamports to the recipient, zeros lamports, and clears data.

The browser pre-funds missing upload accounts plus a buffer

JavaScript src/chain/nicechunkChain.js
async function fundBuildingUploadSession(provider, sessionAuthority, accountLengths, conn) {
  if (provider.publicKey.equals(sessionAuthority)) return;
  const lengths = (accountLengths ?? []).map((value) => Math.max(0, Math.trunc(Number(value) || 0))).filter(Boolean);
  const uniqueLengths = [...new Set(lengths)];
  const rentValues = await Promise.all(uniqueLengths.map((length) => conn.getMinimumBalanceForRentExemption(length)));
  const rentByLength = new Map(uniqueLengths.map((length, index) => [length, rentValues[index]]));
  const required = lengths.reduce((sum, length) => sum + (rentByLength.get(length) || 0), 0) + 1_000_000;
  const balance = await conn.getBalance(sessionAuthority, "confirmed");
  if (balance >= required) return;
  const transaction = new Transaction().add(SystemProgram.transfer({
    fromPubkey: provider.publicKey,
    toPubkey: sessionAuthority,
    lamports: required - balance,
  }));
  await signAndSendWalletTransaction(provider, transaction, conn);
}

Plugin mode estimates rent for missing account lengths and tops the Session authority up to that total plus one million lamports. The buffer is ordinary Session SOL, not PDA rent.

  1. Lines 1–6

    Skip when owner and Session are one address, then query rent once per unique account length.

  2. Lines 7–9

    Sum each account requirement, add the buffer, and compare the ordinary Session balance.

  3. Lines 10–16

    Transfer only the missing amount from owner with an owner-fee-paid transaction.

Chunk creates PlayerProgress with the Session payer

Rust programs/nicechunk_chunk/src/lib.rs
    let rent = Rent::get()?;
    let lamports = rent.minimum_balance(PLAYER_PROGRESS_LEN);
    if player_progress.lamports() == 0 {
        let create = system_create_account(
            payer.key,
            player_progress.key,
            lamports,
            PLAYER_PROGRESS_LEN as u64,
            program_id,
        );
        invoke_signed(
            &create,
            &[
                payer.clone(),
                player_progress.clone(),
                system_program_account.clone(),
            ],
            &[seeds],
        )?;
    } else {

The mining flow passes Session authority as payer into this helper, which creates the 128-byte progress account at the runtime minimum.

  1. Lines 1–2

    Calculate current rent for PLAYER_PROGRESS_LEN.

  2. Lines 3–10

    Create the PDA using payer.key as lamport source.

  3. Lines 11–19

    Invoke with payer, progress, and System Program accounts plus PDA seeds.

The unified Game program routes three active account families by namespace

Rust programs/nicechunk_game/src/lib.rs
    let (tag, payload) = instruction_data
        .split_first()
        .ok_or(NicechunkGameError::InvalidInstruction)?;

    match *tag {
        NS_BACKPACK => nicechunk_backpack::process_instruction(program_id, accounts, payload),
        NS_CHUNK => nicechunk_chunk::process_instruction(program_id, accounts, payload),
        NS_SMELTING => nicechunk_smelting::process_instruction(program_id, accounts, payload),
        NS_MARKET => nicechunk_market::process_instruction(program_id, accounts, payload),
        _ => Err(NicechunkGameError::InvalidInstruction.into()),
    }

Current configuration uses this Game program for Backpack, Market, and Smelting. The namespace dispatcher adds no separate close behavior; each module's own dispatch remains decisive.

  1. Lines 1–3

    Read the first byte as a namespace tag.

  2. Lines 5–10

    Delegate to one module or reject an unknown namespace.

Core exposes initialization only

Rust programs/nicechunk_core/src/lib.rs
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    if instruction_data != [0] {
        return Err(NicechunkError::InvalidInstruction.into());
    }

    initialize_global_config(program_id, accounts)
}

Core accepts only tag 0 GlobalConfig initialization in this revision. There is no update or close dispatch.

  1. Lines 1–8

    Reject every instruction payload other than one byte containing zero.

  2. Line 10

    Run GlobalConfig initialization as the only action.

Guardian dispatch has registration and updates but no close

Rust programs/nicechunk_guardian/src/lib.rs
    match tag {
        0 => initialize_registry(program_id, accounts),
        1 => register_genesis_guardian(program_id, accounts, payload),
        2 => register_guardian(program_id, accounts, payload),
        5 => update_guardian_endpoint(program_id, accounts, payload),
        6 => update_guardian_blueprint(program_id, accounts, payload),
        8 => update_guardian_operator(program_id, accounts, payload),
        _ => Err(NicechunkGuardianError::InvalidInstruction.into()),
    }

Registry and Region records can be created or updated, but no current tag closes them or returns their account lamports.

  1. Lines 1–4

    Initialize registry and register genesis or later Regions.

  2. Lines 5–8

    Update endpoint, blueprint, or operator and reject every other tag.

Civilization lifecycle transitions do not include close

Rust programs/nicechunk_civilization/src/lib.rs
    match tag {
        0 => publish_rule_book(program_id, accounts, payload),
        1 => sign_rule(program_id, accounts, payload),
        2 => finalize_rule_tally(program_id, accounts),
        3 => execute_rule_receipt(program_id, accounts),
        4 => publish_power_snapshot(program_id, accounts, payload),
        5 => settle_citizen_power(program_id, accounts, payload),
        _ => Err(NicechunkCivilizationError::InvalidInstruction.into()),
    }

Publishing, signing, finalizing, executing, and settling all leave their program accounts allocated.

  1. Lines 1–4

    Create a rule, signature, tally, or receipt transition.

  2. Lines 5–8

    Create power records or reject an unrecognized action; no close appears.

Smelting dispatch has recipes and execution but no close

Rust programs/nicechunk_smelting/src/lib.rs
    match tag {
        0 => initialize_recipe_table(program_id, accounts, payload),
        1 => upsert_recipe(program_id, accounts, payload),
        2 => execute_smelting(program_id, accounts, payload),
        4 => apply_civilization_recipe_receipt(program_id, accounts, payload),
        _ => Err(NicechunkSmeltingError::InvalidInstruction.into()),
    }

The Smelting map exposes treasury-controlled initialization and upsert, owner execution, and Civilization receipt application. Tag 3 authority transfer is absent, and no branch closes RecipeTable or PlayerProgress accounts.

  1. Lines 1–4

    Initialize or update recipes and execute a player Smelting action.

  2. Lines 5–6

    Apply a Civilization receipt or reject every other tag; no authority-transfer or close branch exists.

Modular logout clears browser prefixes without a chain call

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);
  }
}

This function deletes local identity and runtime-cache keys. It constructs no transaction, calls no RPC, and invokes no program close.

  1. Lines 1–2

    Remove saved wallet identity values.

  2. Lines 3–11

    Enumerate browser storage, returning early if enumeration fails.

  3. Lines 12–14

    Remove keys matching plugin Session and related runtime prefixes.

Local Game Wallet disconnect does not delete its secret

JavaScript src/localGameWallet.js
export 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;
    },

The Local Game Wallet provider stays backed by the same loaded keypair, and disconnect returns without removing it.

  1. Lines 1–7

    Load the persistent key and expose its public address.

  2. Lines 8–13

    Connect returns the address, while disconnect performs no deletion.

  3. Lines 14–17

    The same keypair remains able to sign transactions.

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/chain/nicechunkChain.js

Wallet sends assign provider.publicKey as fee payer; gameplay keypair sends assign signer.publicKey.

sdk/nicechunk-player.ts

PlayerSession setup marks owner writable signer and Session authority signer, separating the storage payer from the co-signer.

public/mainnet.json

Current source configuration declares Devnet and the active Player, Chunk, Building, and unified Game program routes.

play/play-onboarding.js

The browser requests getMinimumBalanceForRentExemption by byte length and keeps the 0.000005 SOL network-fee value as a presentation constant.

programs/nicechunk_player/src/state.rs

Defines the exact current lengths for Profile, Session, Equipment, Appearance, UsernameIndex, and InviteIndex.

programs/nicechunk_chunk/src/state.rs

Defines 128-byte PlayerProgress and ChunkBroken header, record, initial capacity, growth, and maximum capacity constants.

src/chain/nicechunkChain.js

Submission enables preflight, confirmation polls signature status, status errors retain signatures, and transaction metadata exposes both payer decrease and meta.fee.

programs/nicechunk_player/src/lib.rs

Player creates or grows Profile, Session, Equipment, Appearance, UsernameIndex, and InviteIndex accounts while dispatch exposes only Appearance close.

sdk/nicechunk-player.ts

Profile, Appearance, Equipment, and Session addresses are derived from their documented owner and authority seeds.

programs/nicechunk_backpack/src/lib.rs

The owner funds Backpack creation and the Backpack instruction map contains no close.

src/chain/nicechunkChain.js

Normal Backpack recovery first tests local cache and then reads the equipped Backpack pointer from PlayerProfile.

programs/nicechunk_market/src/lib.rs

Seller funds Listing creation; active cancellation restores the item and marks the account canceled without draining lamports, while purchase marks the account sold without closing it.

src/chain/nicechunkChain.js

The Market buyer is wallet fee payer and can fund missing seller or treasury associated token accounts during an NCK purchase.

programs/nicechunk_player/src/lib.rs

PlayerAppearance close requires the configured treasury as signer and recipient and drains the Appearance account only to that treasury.

programs/nicechunk_player/src/state.rs

The Appearance state policy explicitly says the wallet funds rent while the protocol treasury owns reclaim authority.

programs/nicechunk_building/src/building.rs

Defines 160-byte BuildSite, 160-byte manifest, 64-byte shard header, uploading and active states, and bounded payload sizes.

programs/nicechunk_building/src/lib.rs

Session authority funds Building accounts; cancellation validates an uploading pending revision, updates BuildSite, and closes existing shards plus manifest to the current Session authority.

src/chain/nicechunkChain.js

The browser queries upload-account rent, adds a one-million-lamport Session buffer, and invokes cancellation only in its pending-payload mismatch recovery branch.

programs/nicechunk_chunk/src/lib.rs

Session authority funds missing Chunk PlayerProgress, ChunkBroken creation, and later ChunkBroken rent top-ups; dispatch has no close.

programs/nicechunk_game/src/lib.rs

The unified Game dispatcher contains Backpack, Chunk, Smelting, and Market namespaces without adding a separate close path; current source configuration selects it for Backpack, Smelting, and Market while Chunk uses its dedicated program.

programs/nicechunk_core/src/lib.rs

Core accepts only GlobalConfig initialization and exposes no close instruction.

programs/nicechunk_guardian/src/lib.rs

Guardian dispatch creates or updates Registry and Region state without an account close or stake-return instruction.

programs/nicechunk_guardian/src/state.rs

Defines 160-byte Registry and 288-byte Region layouts, including a removed status value that is not itself a close implementation.

programs/nicechunk_civilization/src/lib.rs

Civilization allocates its rule and power PDA families and dispatches publish, sign, tally, execute, snapshot, and settle without close.

programs/nicechunk_civilization/src/state.rs

Defines the exact lengths of PowerSnapshot, CitizenPower, RuleBook, RuleSignature, RuleTally, and ExecutionReceipt.

programs/nicechunk_smelting/src/lib.rs

Smelting creates RecipeTable and program-scoped PlayerProgress accounts but exposes no close tag.

play/play-auth-session.js

Modular logout deletes local wallet identity and Session-related browser prefixes without submitting a chain transaction.

login/login.js

Login-page Disconnect Wallet clears its wallet binding after a best-effort provider disconnect and does not perform Session cleanup or a chain close.

src/localGameWallet.js

Local Game Wallet stores an unencrypted Base58 secret under separate keys and its provider disconnect method is a no-op.

src/chain/nicechunkChain.js

Plugin Session secrets are Base64 browser records; a surviving key is reused, while a missing key leads to a different authority and Session PDA.

programs/nicechunk_player/src/state.rs

Session expiry validation rejects expired actions but does not close the Session account or move the authority's ordinary SOL.