69: Add SPL-token burn support to SolanaWallet and SolanaBlockChain

This commit is contained in:
2026-08-11 15:42:56 +02:00
parent b86ec37bb1
commit b33db6da49
11 changed files with 517 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-11
@@ -0,0 +1,55 @@
## Context
The current SPL transfer flow separates responsibilities cleanly: `SolanaBlockChain` serializes unsigned low-level transactions, while `SolanaWallet` resolves mint and holding state, validates human-readable amounts, signs, and submits. The transaction serializer already supports legacy-message account ordering, compact-u16 encoding, recent blockhash metadata, and selecting either supported SPL token program.
## Goals / Non-Goals
**Goals:**
- Extend the existing API/implementation boundary with checked SPL-token burning.
- Keep all state-dependent ownership and balance checks in the wallet and all wire serialization in the blockchain implementation.
- Use identical behavior for the original SPL Token Program and Token-2022 except for the selected program address.
- Preserve the existing submission-only signing flow.
**Non-Goals:**
- Confirmation awaiting, retry, account closure, rent recovery, multisignatures, delegates, or permanent delegates.
- Native or wrapped SOL burning, a burn-all convenience API, or integration with Evelyn and Discord.
- New ValueTags, configuration, dependencies, tests, or changes to unrelated consumers.
## Decisions
### Mirror the existing transfer responsibility split
The wallet will reuse its existing mint-program detection, supply loading, holding lookup, decimal consistency checks, exact decimal conversion, and `signAndSendTransaction()` flow. The blockchain method accepts already-resolved raw units and program identity and only validates/serializes those low-level inputs.
Moving state lookup into the transaction builder was rejected because it would duplicate wallet policy and blur the existing API boundary. Building the transaction directly in the wallet was rejected because low-level Solana serialization belongs to `SolanaBlockChain`.
### Encode one standard BurnChecked instruction
The unsigned legacy transaction will contain four account keys in privilege order: owner as writable signer/fee payer, token account and mint as writable unsigned accounts, and the selected token program as read-only unsigned. Its one compiled instruction will invoke program index 3 with account indexes token account, mint, owner and data consisting of the `BurnChecked` discriminator, little-endian unsigned-u64 amount bits, and unsigned-byte decimals. The message declares one required signature, zero read-only signed accounts, and one read-only unsigned account.
This follows the repository's current `TransferChecked` serializer instead of introducing a transaction framework or external dependency. Although Java `long` is signed, a validated unsigned-u64 `BigInteger` is serialized through its low 64 bits with `longValue()`, preserving the complete bit pattern.
### Validate all local builder inputs before RPC access
The builder will parse and range-check the raw amount, range-check decimals, require a non-null supported enum program, and decode all supplied addresses before requesting a recent blockhash. This keeps invalid input deterministic and avoids unnecessary RPC traffic. The same shared address decoder used by existing transaction builders provides Base58 and 32-byte validation.
### Treat complete-balance burn as an ordinary amount
Wallet balance validation rejects only raw amounts greater than the holding, so equality is deliberately allowed. No close instruction is appended; the token account remains open with a zero balance.
### Delegate construction through the cache boundary
`CachedSolanaBlockChain` will forward the call directly because each build requires a fresh recent blockhash. Caching or deduplicating transaction construction could return stale blockhashes and is therefore unsafe.
## Risks / Trade-offs
- [Hand-written Solana wire encoding can be sensitive to account ordering and instruction discriminators] → Mirror the existing compiled-instruction serializer and use the standard `BurnChecked` layout with explicit constants.
- [Unsigned-u64 values above signed-long maximum appear negative as Java longs] → Validate with `BigInteger` first and serialize the low 64-bit two's-complement representation in little-endian order.
- [Submission success does not establish on-chain burn success] → Keep the API explicitly submission-only; callers may separately await the returned signature when their domain policy requires it.
- [Mint and holding state can change between validation and execution] → Let Solana's checked instruction and runtime enforce final state; do not retry automatically.
## Migration Plan
Add the API methods, implementations, and direct cached delegation in one source-compatible change. Existing callers need no migration because the new methods are additive. Rollback removes those additions; no data or configuration migration is involved.
@@ -0,0 +1,25 @@
## Why
Future Evelyn functionality needs a generic way to burn wallet-owned SPL tokens, including EVE under Token-2022. The existing wallet/blockchain separation for SPL transfers provides the right boundary for adding checked burn construction and a high-level validate-sign-submit operation without coupling confirmation or account cleanup to submission.
## What Changes
- Add a public blockchain operation that builds one unsigned `BurnChecked` SPL-token transaction for either the legacy SPL Token Program or Token-2022.
- Add a public wallet operation that validates blockchain state and an exact human-readable burn amount, then builds, signs, and submits the transaction once.
- Delegate burn transaction construction directly through the cached blockchain decorator without caching.
- Preserve submission-only behavior: no confirmation awaiting, retry, token-account closure, or rent recovery.
- Reuse the existing `ΩRawAmountΩ`, `ΩamountDecimalsΩ`, address, amount, and transaction ValueTags without changing Detag configuration.
## Capabilities
### New Capabilities
- `solana-spl-token-burning`: Defines safe checked SPL-token burn construction and wallet-owned submission for legacy SPL Token and Token-2022 mints.
### Modified Capabilities
None.
## Impact
The public `SolanaBlockChain` and `SolanaWallet` APIs gain burn operations. Their reference implementations and `CachedSolanaBlockChain` gain corresponding construction, validation, submission, and direct-delegation behavior. No consumers, configuration, tests, generated sources, or unrelated services change.
@@ -0,0 +1,72 @@
## Purpose
Defines safe construction and wallet submission of checked SPL-token burns for both supported Solana token programs.
## ADDED Requirements
### Requirement: Checked SPL-token burn transaction construction
The blockchain API SHALL build an unsigned transaction containing exactly one checked SPL-token burn instruction for a supplied owner, token account, mint, raw amount, decimal count, and supported token program. The owner SHALL be the fee payer, token-account authority, and sole required signer. The transaction SHALL burn from the supplied token account and mint, use a recent blockhash, and remain unsigned and unsubmitted.
#### Scenario: Legacy SPL Token burn construction
- **WHEN** valid burn inputs identify the original SPL Token Program
- **THEN** the returned unsigned transaction contains one `BurnChecked` instruction owned by that program with the supplied account, mint, amount, and decimals
#### Scenario: Token-2022 burn construction
- **WHEN** valid burn inputs identify Token-2022
- **THEN** the returned unsigned transaction contains one `BurnChecked` instruction owned by Token-2022 with the supplied account, mint, amount, and decimals
#### Scenario: Builder does not submit
- **WHEN** a checked burn transaction is built successfully
- **THEN** the blockchain returns the unsigned serialized transaction and recent blockhash metadata without signing or submitting it
### Requirement: Low-level burn input validation
Before fetching a blockhash, the blockchain SHALL reject null, blank, malformed, or unsupported required arguments. The raw amount SHALL be a valid positive integer representable as an unsigned 64-bit value, and the decimal count SHALL be representable as an unsigned byte.
#### Scenario: Invalid raw amount
- **WHEN** the raw amount is null, non-numeric, zero, negative, or greater than unsigned 64-bit maximum
- **THEN** transaction construction fails with `IllegalArgumentException` before fetching a blockhash
#### Scenario: Invalid decimals
- **WHEN** the decimal count is less than zero or greater than 255
- **THEN** transaction construction fails with `IllegalArgumentException` before fetching a blockhash
#### Scenario: Invalid address or program
- **WHEN** the owner, token account, or mint is null, blank, malformed, or does not decode to a Solana address, or the token program is null
- **THEN** transaction construction fails locally before fetching a blockhash
### Requirement: Wallet-owned burn validation and exact conversion
The wallet SHALL accept a mint and positive human-readable amount, load the mint account, detect whether it is owned by the original SPL Token Program or Token-2022, obtain the mint decimals, and find its own holding under that program. It SHALL require the holding decimals to equal the mint decimals and SHALL convert the requested amount exactly to positive raw units without rounding. It SHALL reject unsupported mints, missing or inconsistent state, excessive precision, and amounts exceeding the wallet balance before transaction construction or submission.
#### Scenario: Exact partial balance burn
- **WHEN** a supported mint and positive amount are exactly representable with the mint decimals and do not exceed the wallet holding
- **THEN** the wallet supplies the corresponding exact raw amount, its own token account, detected program, and mint decimals to checked burn construction
#### Scenario: Complete balance burn
- **WHEN** the exact raw burn amount equals the wallet's complete token balance
- **THEN** the wallet permits the burn without requesting token-account closure or rent recovery
#### Scenario: Amount has excessive precision
- **WHEN** the requested human-readable amount contains a non-zero fraction beyond the mint decimal precision
- **THEN** the wallet rejects it with `IllegalArgumentException` before building, signing, or submitting a transaction
#### Scenario: Blockchain state cannot authorize burn
- **WHEN** the mint is missing or unsupported, the wallet has no holding, the holding decimals differ from the mint decimals, or the amount exceeds the holding balance
- **THEN** the wallet rejects the operation with `IllegalStateException` before building, signing, or submitting a transaction
### Requirement: Single submission without automatic completion handling
After successful validation, the wallet SHALL build the burn through the blockchain API, sign it through its existing signing flow, submit it exactly once through its existing submission flow, and return the resulting transaction signature. It SHALL return immediately after submission without awaiting confirmation, retrying, rebuilding, re-signing, resubmitting, or closing the token account.
#### Scenario: Burn is submitted successfully
- **WHEN** validation, construction, signing, and submission succeed
- **THEN** the wallet returns the transaction signature from the single submission without invoking transaction awaiting
#### Scenario: Burn flow is interrupted or fails
- **WHEN** blockchain access, signing, or submission throws an I/O error or the calling thread is interrupted
- **THEN** the corresponding exception propagates and no automatic retry or subsequent completion action occurs
### Requirement: Cached blockchain delegates burn construction
The cached blockchain decorator SHALL delegate every burn transaction construction call directly to its underlying blockchain without caching, reuse, or deduplication.
#### Scenario: Repeated construction through cached decorator
- **WHEN** callers request burn transaction construction multiple times through the cached decorator
- **THEN** every invocation reaches the underlying blockchain independently
@@ -0,0 +1,22 @@
## 1. Public APIs
- [x] 1.1 Add the documented checked-burn transaction builder to `SolanaBlockChain` using the existing burn input ValueTags.
- [x] 1.2 Add the documented human-readable SPL-token burn operation to `SolanaWallet` with submission-only semantics.
## 2. Blockchain Construction
- [x] 2.1 Implement pre-RPC raw amount, decimal, program, and Solana address validation in `SolanaBlockChainImpl`.
- [x] 2.2 Serialize one unsigned `BurnChecked` transaction with owner authority/fee-payer semantics, a recent blockhash, and selectable legacy SPL Token or Token-2022 program.
- [x] 2.3 Add direct, uncached burn-construction delegation to `CachedSolanaBlockChain`.
## 3. Wallet Burn Flow
- [x] 3.1 Implement mint lookup, supported-program detection, supply/holding lookup, decimal consistency, and exact human-to-raw amount conversion in `SolanaWalletImpl`.
- [x] 3.2 Enforce missing-state and balance failures while permitting an exact complete-balance burn without closing the token account.
- [x] 3.3 Build, sign, and submit exactly once through existing flows, return the signature immediately, and add no awaiting, retry, or resubmission.
## 4. Verification
- [x] 4.1 Compile main and test source sets without adding or modifying tests and without executing a live burn or submission.
- [x] 4.2 Run strict OpenSpec validation and `git diff --check`.
- [x] 4.3 Review the complete diff for both token programs, exact amount conversion, complete-balance support, direct cached delegation, submission-only behavior, no generated/test changes, and no unrelated modifications.
@@ -0,0 +1,74 @@
# solana-spl-token-burning Specification
## Purpose
Defines safe construction and wallet submission of checked SPL-token burns for both supported Solana token programs.
## Requirements
### Requirement: Checked SPL-token burn transaction construction
The blockchain API SHALL build an unsigned transaction containing exactly one checked SPL-token burn instruction for a supplied owner, token account, mint, raw amount, decimal count, and supported token program. The owner SHALL be the fee payer, token-account authority, and sole required signer. The transaction SHALL burn from the supplied token account and mint, use a recent blockhash, and remain unsigned and unsubmitted.
#### Scenario: Legacy SPL Token burn construction
- **WHEN** valid burn inputs identify the original SPL Token Program
- **THEN** the returned unsigned transaction contains one `BurnChecked` instruction owned by that program with the supplied account, mint, amount, and decimals
#### Scenario: Token-2022 burn construction
- **WHEN** valid burn inputs identify Token-2022
- **THEN** the returned unsigned transaction contains one `BurnChecked` instruction owned by Token-2022 with the supplied account, mint, amount, and decimals
#### Scenario: Builder does not submit
- **WHEN** a checked burn transaction is built successfully
- **THEN** the blockchain returns the unsigned serialized transaction and recent blockhash metadata without signing or submitting it
### Requirement: Low-level burn input validation
Before fetching a blockhash, the blockchain SHALL reject null, blank, malformed, or unsupported required arguments. The raw amount SHALL be a valid positive integer representable as an unsigned 64-bit value, and the decimal count SHALL be representable as an unsigned byte.
#### Scenario: Invalid raw amount
- **WHEN** the raw amount is null, non-numeric, zero, negative, or greater than unsigned 64-bit maximum
- **THEN** transaction construction fails with `IllegalArgumentException` before fetching a blockhash
#### Scenario: Invalid decimals
- **WHEN** the decimal count is less than zero or greater than 255
- **THEN** transaction construction fails with `IllegalArgumentException` before fetching a blockhash
#### Scenario: Invalid address or program
- **WHEN** the owner, token account, or mint is null, blank, malformed, or does not decode to a Solana address, or the token program is null
- **THEN** transaction construction fails locally before fetching a blockhash
### Requirement: Wallet-owned burn validation and exact conversion
The wallet SHALL accept a mint and positive human-readable amount, load the mint account, detect whether it is owned by the original SPL Token Program or Token-2022, obtain the mint decimals, and find its own holding under that program. It SHALL require the holding decimals to equal the mint decimals and SHALL convert the requested amount exactly to positive raw units without rounding. It SHALL reject unsupported mints, missing or inconsistent state, excessive precision, and amounts exceeding the wallet balance before transaction construction or submission.
#### Scenario: Exact partial balance burn
- **WHEN** a supported mint and positive amount are exactly representable with the mint decimals and do not exceed the wallet holding
- **THEN** the wallet supplies the corresponding exact raw amount, its own token account, detected program, and mint decimals to checked burn construction
#### Scenario: Complete balance burn
- **WHEN** the exact raw burn amount equals the wallet's complete token balance
- **THEN** the wallet permits the burn without requesting token-account closure or rent recovery
#### Scenario: Amount has excessive precision
- **WHEN** the requested human-readable amount contains a non-zero fraction beyond the mint decimal precision
- **THEN** the wallet rejects it with `IllegalArgumentException` before building, signing, or submitting a transaction
#### Scenario: Blockchain state cannot authorize burn
- **WHEN** the mint is missing or unsupported, the wallet has no holding, the holding decimals differ from the mint decimals, or the amount exceeds the holding balance
- **THEN** the wallet rejects the operation with `IllegalStateException` before building, signing, or submitting a transaction
### Requirement: Single submission without automatic completion handling
After successful validation, the wallet SHALL build the burn through the blockchain API, sign it through its existing signing flow, submit it exactly once through its existing submission flow, and return the resulting transaction signature. It SHALL return immediately after submission without awaiting confirmation, retrying, rebuilding, re-signing, resubmitting, or closing the token account.
#### Scenario: Burn is submitted successfully
- **WHEN** validation, construction, signing, and submission succeed
- **THEN** the wallet returns the transaction signature from the single submission without invoking transaction awaiting
#### Scenario: Burn flow is interrupted or fails
- **WHEN** blockchain access, signing, or submission throws an I/O error or the calling thread is interrupted
- **THEN** the corresponding exception propagates and no automatic retry or subsequent completion action occurs
### Requirement: Cached blockchain delegates burn construction
The cached blockchain decorator SHALL delegate every burn transaction construction call directly to its underlying blockchain without caching, reuse, or deduplication.
#### Scenario: Repeated construction through cached decorator
- **WHEN** callers request burn transaction construction multiple times through the cached decorator
- **THEN** every invocation reaches the underlying blockchain independently