Add Solana signing support#3211
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds end-to-end Solana signing: new signSolana proto/model fields and mapping, Solana transaction parser and raw-transaction signing helpers, a SignSolana UI component, UI model updates to TransactionDetailsUiModel, test fixture updates, drawable and localized string resources, and renames Solana JSON key Changes
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/SolanaHelper.kt (1)
133-159:⚠️ Potential issue | 🟠 MajorSignSolana signatures won’t match this signing path.
getPreSignedImageHashnow returns hashes derived fromsignSolana.rawTransactions, butgetSignedTransactionstill signs the standardSigningInput. That will produce a different pre‑image key, leading to “Signature not found” or verification failure for Solana raw‑tx flows.💡 Suggested branch (handle raw tx first)
fun getSignedTransaction( keysignPayload: KeysignPayload, signatures: Map<String, tss.KeysignResponse>, ): SignedTransactionResult { + keysignPayload.signSolana?.let { signSolana -> + require(signSolana.rawTransactions.size == 1) { + "Expected exactly one Solana raw transaction" + } + return signRawTransaction( + coinHexPubKey = vaultHexPublicKey, + base64Transaction = signSolana.rawTransactions.first(), + signatures = signatures + ) + } val publicKey = PublicKey(vaultHexPublicKey.toHexByteArray(), PublicKeyType.ED25519) val input = getPreSignedInputData(keysignPayload) ... }
🤖 Fix all issues with AI agents
In
`@app/src/androidTest/java/com/vultisig/wallet/data/blockchain/ChainHelperModels.kt`:
- Around line 307-311: Update the test data model SignSolana to match production
by changing the property rawTransactions from String to List<String> (i.e., val
rawTransactions: List<String>), keeping `@SerialName`("raw_transactions") and
`@Serializable`; then remove or simplify the workaround in
ChainHelperModelExtension that wraps a String in listOf(...) since the test
model will now deserialize directly to a list and align with usages in
SolanaHelper, SignSolanaDisplayView, and VerifySendScreen which expect a
List<String>.
In
`@app/src/main/java/com/vultisig/wallet/ui/components/SignSolanaDisplayView.kt`:
- Around line 118-186: Replace hardcoded UI strings in the transaction summary
UI with localized resources: in the Composable that renders the summary (the
block that checks allInstructions.isNotEmpty()) and inside
InstructionRow(instruction: ParsedSolanaTransaction.ParsedInstruction, index:
Int), swap literal strings "Transaction Instructions Summary", "Instruction
${index + 1}", "Program: $name", "Program ID: ${instruction.programId}", and the
accounts/data line with stringResource(...) calls using formatted string
resources (e.g. solana_instruction_summary, solana_instruction_label with %1$d,
solana_program_label with %1$s, solana_program_id_label with %1$s,
solana_accounts_data_label with %1$d and %2$d). Add the corresponding entries to
strings.xml matching those resource names and formats, and pass index+1, name,
instruction.programId, instruction.accountsCount and instruction.dataLength as
format arguments to stringResource where needed.
In `@app/src/main/java/com/vultisig/wallet/ui/screens/send/VerifySendScreen.kt`:
- Around line 250-258: The lambda passed to takeIf { it.isNotBlank() } uses
tx.signSolana inside the let block which isn't smart-cast; update the let block
in VerifySendScreen.kt so the SignSolanaDisplayView uses the lambda parameter
(it) instead of tx.signSolana (e.g., rawTransactions = listOf(it)) and keep
VerifyCardDivider and SignSolana construction unchanged.
In
`@data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/ParsedSolanaTransaction.kt`:
- Around line 30-32: The null-check on the result of android.util.Base64.decode
in ParsedSolanaTransaction.parse is incorrect because decode returns an empty
ByteArray on invalid input rather than null; update the logic around txData in
parse to detect an empty result (txData.isEmpty()) and throw the existing
Exception when empty, or alternatively wrap the decode in a try/catch for
IllegalArgumentException and throw the same Exception with a clear message —
locate the parse function and the txData variable to apply this change.
In
`@data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/SolanaHelper.kt`:
- Around line 113-121: The function getPreSignedImageHash currently processes
keysignPayload.signSolana without confirming the payload is for Chain.Solana;
update getPreSignedImageHash to first check that keysignPayload.chain ==
Chain.Solana (or equivalent enum/value) before iterating
signSolana.rawTransactions and calling getPreSignedImageHashForRaw, so the logic
mirrors the Chain.Solana guard used in getPreSignedInputData and avoids
computing Solana hashes for non‑Solana payloads.
In `@data/src/main/kotlin/com/vultisig/wallet/data/models/Transaction.kt`:
- Line 6: Remove the unused import "vultisig.keysign.v1.SignSolana" from
Transaction.kt; the model declares the property signSolana as a String? (not
SignSolana), so delete the import to eliminate the unused dependency and any
lint/compile warning referencing SignSolana.
🧹 Nitpick comments (4)
data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/ParsedSolanaTransaction.kt (1)
87-148: Consider usingwhenexpression for program ID matching.The chained
ifstatements could be consolidated into awhenexpression for improved readability, though the current implementation is functionally correct.♻️ Suggested refactor
private fun getInstructionType(programId: String, instructionData: ByteArray): String? { if (instructionData.isEmpty()) return null val discriminator = instructionData[0].toInt() and 0xFF - // System Program - if (programId == "11111111111111111111111111111111") { - return when (discriminator) { + return when (programId) { + "11111111111111111111111111111111" -> when (discriminator) { 0 -> "Create Account" 2 -> "Transfer" 3 -> "Assign" 4 -> "Create Account With Seed" 9 -> "Transfer With Seed" else -> "System ($discriminator)" } - } - - // Token Program - if (programId == "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") { - return when (discriminator) { + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" -> when (discriminator) { 0 -> "Initialize Mint" 1 -> "Initialize Account" 3 -> "Transfer" 7 -> "Mint To" 8 -> "Burn" 9 -> "Close Account" 12 -> "Transfer Checked" else -> "Token ($discriminator)" } - } - - // Token-2022 Program - if (programId == "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") { - return when (discriminator) { + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" -> when (discriminator) { 0 -> "Initialize Mint" 1 -> "Initialize Account" 3 -> "Transfer" 7 -> "Mint To" 8 -> "Burn" 9 -> "Close Account" 12 -> "Transfer Checked" else -> "Token-2022 ($discriminator)" } - } - - // Associated Token Program - if (programId == "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") { - return "Create Associated Token Account" - } - - // Compute Budget Program - if (programId == "ComputeBudget111111111111111111111111111111") { - return when (discriminator) { + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" -> + "Create Associated Token Account" + "ComputeBudget111111111111111111111111111111" -> when (discriminator) { 0 -> "Request Heap Frame" 1 -> "Set Compute Unit Limit" 2 -> "Set Compute Unit Price" else -> "Compute Budget ($discriminator)" } + else -> null } - - return null }app/src/androidTest/java/com/vultisig/wallet/data/blockchain/ChainHelperModels.kt (1)
217-218: Consider renaminghasProgramIdto match the JSON key semantics.The property is named
hasProgramId(suggesting a boolean "has X" pattern) but the JSON key is nowprogram_id. This creates a semantic mismatch. Consider renaming the property toprogramIdfor clarity, or clarify via documentation why this Boolean field uses theprogram_idkey.app/src/androidTest/java/com/vultisig/wallet/data/blockchain/ChainHelperModelExtension.kt (2)
14-14: Remove unused imports.The following imports appear to be unused in the active code:
androidx.compose.runtime.remember(line 14)com.vultisig.wallet.data.chains.helpers.SolanaTransactionParser(line 18)kotlin.text.flatMap(line 33)These were likely added for the commented-out code block and should be cleaned up.
♻️ Proposed fix
-import androidx.compose.runtime.remember import com.vultisig.wallet.data.models.proto.v1.SignDirectProto import com.vultisig.wallet.data.api.models.quotes.EVMSwapQuoteJson import com.vultisig.wallet.data.api.models.quotes.OneInchSwapTxJson -import com.vultisig.wallet.data.chains.helpers.SolanaTransactionParser import com.vultisig.wallet.data.models.Chain ... -import kotlin.text.flatMapAlso applies to: 18-18, 33-33
128-145: Remove commented-out code block.This commented-out function has multiple issues:
- It uses
rememberfrom Compose runtime, which only works inside@Composablefunctions—this is a data mapping extension file- Dead/commented code adds noise and confusion
If this logic is needed in the future, it should be properly designed and implemented in an appropriate location.
🗑️ Proposed fix: remove the commented block
-//internal fun SignSolana.toSignSolana(): vultisig.keysign.v1.SignSolana { -// val allInstructions = remember(this) { -// this.rawTransactions.map { tx -> -// try { -// val parsed = SolanaTransactionParser.parse(tx) -// parsed.instructions -// } catch (e: Exception) { -// emptyList() -// } -// } -// } -// -// -// -// return SignSolana( -// rawTransactions = this.rawTransactions -// ) -//} -
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/vultisig/wallet/ui/components/SignSolanaDisplayView.kt`:
- Line 236: Rename the misspelled preview function SignSolanaDisplayPrewview to
SignSolanaDisplayPreview and update all references/usages accordingly (e.g., any
`@Preview` annotations, composable calls, or test references) to use the corrected
name to avoid unresolved symbol errors; ensure the function signature and any
imports remain unchanged aside from the identifier rename.
- Around line 109-114: In SignSolanaDisplayView where you call
SolanaTransactionParser.parse(tx) inside the try/catch, stop silently swallowing
exceptions and log them—capture the caught Exception (e) and call an appropriate
logger (e.g., Android Log.e with a clear tag like "SignSolanaDisplayView" or
your app's logger) including a descriptive message and the exception stacktrace,
then continue returning emptyList() as the fallback; this keeps the existing
behavior but surfaces parsing failures for debugging.
In
`@app/src/main/java/com/vultisig/wallet/ui/models/keysign/JoinKeysignViewModel.kt`:
- Around line 922-932: Replace the reflection block that reads rawTransactions
with direct property access on the protobuf SignSolana instance: when handling
payload.signSolana in JoinKeysignViewModel.kt, use signSolana.rawTransactions
(e.g., signSolana.rawTransactions.firstOrNull() as? String ?: "") instead of
getDeclaredField("rawTransactions"); remove the try/catch that swallows
exceptions and, if you keep error handling, log the exception via your logger
(or processLogger) to avoid hiding errors while returning an empty string
fallback.
In
`@data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/SolanaHelper.kt`:
- Around line 143-152: getPreSignedImageHash iterates all
keysignPayload.signSolana.rawTransactions but getSignedTransaction only signs
the first one; either align by restricting hashing to the first transaction or
sign all transactions. Update getSignedTransaction to iterate over
keysignPayload.signSolana.rawTransactions (use map) and call signRawTransaction
for each raw tx (passing coinHexPubKey, base64Transaction, signatures) returning
a collection of signed transactions, and ensure getPreSignedImageHash remains
consistent (or conversely adjust getPreSignedImageHash to only hash the first
tx) so both functions operate on the same transaction set; modify function
signatures/return types accordingly if needed.
- Around line 283-287: The nullable guard on android.util.Base64.decode is
incorrect because decode never returns null; replace the safe-call/Elvis pattern
around txData (where base64Transaction is decoded) with a try-catch that catches
IllegalArgumentException from android.util.Base64.decode and throws or logs a
clear error (e.g., error("Invalid base64 transaction: <details>") or rethrow a
descriptive exception); update the code in SolanaHelper.kt around the txData
assignment (base64Transaction / txData) to validate input via try-catch rather
than using ?:.
🧹 Nitpick comments (2)
app/src/androidTest/java/com/vultisig/wallet/data/blockchain/ChainHelperModelExtension.kt (1)
129-146: Consider removing the commented-out SignSolana helper block.
It’s currently dead code and makes this mapper harder to scan. If you drop it, you can also remove the related imports.♻️ Suggested cleanup
-//internal fun SignSolana.toSignSolana(): vultisig.keysign.v1.SignSolana { -// val allInstructions = remember(this) { -// this.rawTransactions.map { tx -> -// try { -// val parsed = SolanaTransactionParser.parse(tx) -// parsed.instructions -// } catch (e: Exception) { -// emptyList() -// } -// } -// } -// -// -// -// return SignSolana( -// rawTransactions = this.rawTransactions -// ) -//}data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/SolanaHelper.kt (1)
282-398: Consider extracting shared decode/parse logic.Both
getPreSignedImageHashForRawandsignRawTransactionduplicate the base64 decode →TransactionDecoder.decode→ error check →hasTransactioncheck →SigningInputbuild sequence. Extracting a helper would reduce duplication and centralize error handling.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
app/src/main/java/com/vultisig/wallet/ui/components/SignSolanaDisplayView.kt (2)
35-35: Redundant import.
flatMapis part of Kotlin's standard library and doesn't need an explicit import.-import kotlin.collections.flatMap
65-76: Small touch target for the expand/collapse button.The
IconButtonis only 10.dp, which is well below Material Design's recommended 48.dp minimum touch target. Users may find it difficult to tap. Consider increasing the button's clickable area while keeping the icon visually small.Suggested approach
IconButton( onClick = { isExpanded = !isExpanded }, - modifier = Modifier.size(10.dp) + modifier = Modifier.size(48.dp) ) { UiIcon( drawableResId = R.drawable.chevron,
6f9a020 to
c5aa39d
Compare
…and remove redundant program_id entries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Description
Fixes #3130
Please include a summary of the change and which issue is fixed.
Sol transfer (legacy):
https://solscan.io/tx/e6BNC4wvRMrZ6N2Na47ZCyeWWksapTiwaJXvWgScZoC945AkniYYbUdpHNQAN5FAnwS9TaEBHa89DBEZ35P8ohD
Sol transfer (version 0):
https://solscan.io/tx/3haNVbcGUBCvyNxMAbhe4UEBGzQTXoSkgjG6kX93MRtGB3PToHwjYK2aMEQrcSGRtoZsTH928RHftkw48KRgFXAe
USDC transfer (legacy):
https://solscan.io/tx/5HyeEasD7TLc5C4JXRCZDq4bNz1T667detXhkki4FAYUThgDYQ4uprVL3VFggiVNTzMZ24cVGhWmwnr538QCar1f
USDC transfer (version 0):
https://solscan.io/tx/3Zat2rNgriQWpYYyX7jYGHNqu5poKfMR59FXSxHNjtJ3xaMzcGrgiTuBtZYCMhHCyjRq7HVtbTPMwrx6Yf2nzmea
Partially sol transfer:
https://solscan.io/tx/3YpUkbF5iTys7F1XBoHAzVBymHN5Cr8hTmGL1PhFf6d6PMwFnNQ33mgstShUyvgPcvLgcydGJoCQZgDgYBEVLk1g
Which feature is affected?
Checklist
Screenshots (if applicable):
Additional context
Summary by CodeRabbit
New Features
Refactor
Tests
Localization
Style