fix(android): transfer flow improvements and message_hash format fix
Transfer Screen improvements: - Add QR code scanning for recipient address (using zxing library) - Support EIP-681 URI format (ethereum:0x...) and plain address - Remove password requirement - TSS wallets don't need passwords - Remove unused onScanQrCode callback parameter WalletsScreen changes: - Simplify onTransfer callback to only pass shareId - Remove TransferDialog - now navigates directly to TransferScreen - Remove unused state variables (showTransferDialog, transferWallet) Bug fix: - Remove 0x prefix from message_hash before sending to API - Backend expects pure hex, not 0x-prefixed hex string 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
480251b85f
commit
ed55be2b86
|
|
@ -170,9 +170,8 @@ fun TssPartyApp(
|
||||||
networkType = settings.networkType,
|
networkType = settings.networkType,
|
||||||
onDeleteShare = { viewModel.deleteShare(it) },
|
onDeleteShare = { viewModel.deleteShare(it) },
|
||||||
onRefreshBalance = { address -> viewModel.fetchBalance(address) },
|
onRefreshBalance = { address -> viewModel.fetchBalance(address) },
|
||||||
onTransfer = { shareId, toAddress, amount, password ->
|
onTransfer = { shareId ->
|
||||||
transferWalletId = shareId
|
transferWalletId = shareId
|
||||||
viewModel.prepareTransfer(shareId, toAddress, amount)
|
|
||||||
navController.navigate("transfer/$shareId")
|
navController.navigate("transfer/$shareId")
|
||||||
},
|
},
|
||||||
onCreateWallet = {
|
onCreateWallet = {
|
||||||
|
|
@ -209,9 +208,6 @@ fun TssPartyApp(
|
||||||
onConfirmTransaction = {
|
onConfirmTransaction = {
|
||||||
viewModel.initiateSignSession(shareId, "")
|
viewModel.initiateSignSession(shareId, "")
|
||||||
},
|
},
|
||||||
onScanQrCode = {
|
|
||||||
// TODO: Launch QR scanner for recipient address
|
|
||||||
},
|
|
||||||
onCopyInviteCode = {
|
onCopyInviteCode = {
|
||||||
signInviteCode?.let { onCopyToClipboard(it) }
|
signInviteCode?.let { onCopyToClipboard(it) }
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1697,10 +1697,16 @@ class TssRepository @Inject constructor(
|
||||||
// Build request body matching account-service API
|
// Build request body matching account-service API
|
||||||
// Note: Android ShareRecordEntity doesn't store wallet_name, use address-based name
|
// Note: Android ShareRecordEntity doesn't store wallet_name, use address-based name
|
||||||
val walletName = "Wallet ${shareEntity.address.take(8)}...${shareEntity.address.takeLast(4)}"
|
val walletName = "Wallet ${shareEntity.address.take(8)}...${shareEntity.address.takeLast(4)}"
|
||||||
|
// Remove 0x prefix from messageHash - backend expects pure hex
|
||||||
|
val cleanMessageHash = if (messageHash.startsWith("0x") || messageHash.startsWith("0X")) {
|
||||||
|
messageHash.substring(2)
|
||||||
|
} else {
|
||||||
|
messageHash
|
||||||
|
}
|
||||||
val requestBody = com.google.gson.JsonObject().apply {
|
val requestBody = com.google.gson.JsonObject().apply {
|
||||||
addProperty("keygen_session_id", shareEntity.sessionId)
|
addProperty("keygen_session_id", shareEntity.sessionId)
|
||||||
addProperty("wallet_name", walletName)
|
addProperty("wallet_name", walletName)
|
||||||
addProperty("message_hash", messageHash)
|
addProperty("message_hash", cleanMessageHash)
|
||||||
add("parties", partiesArray)
|
add("parties", partiesArray)
|
||||||
addProperty("threshold_t", shareEntity.thresholdT)
|
addProperty("threshold_t", shareEntity.thresholdT)
|
||||||
addProperty("initiator_name", initiatorName)
|
addProperty("initiator_name", initiatorName)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import com.durian.tssparty.domain.model.NetworkType
|
||||||
import com.durian.tssparty.domain.model.SessionStatus
|
import com.durian.tssparty.domain.model.SessionStatus
|
||||||
import com.durian.tssparty.domain.model.ShareRecord
|
import com.durian.tssparty.domain.model.ShareRecord
|
||||||
import com.durian.tssparty.util.TransactionUtils
|
import com.durian.tssparty.util.TransactionUtils
|
||||||
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
|
||||||
|
|
@ -34,13 +36,15 @@ import java.math.BigInteger
|
||||||
* Transfer Screen - matches service-party-app Home.tsx transfer flow exactly
|
* Transfer Screen - matches service-party-app Home.tsx transfer flow exactly
|
||||||
*
|
*
|
||||||
* Flow:
|
* Flow:
|
||||||
* 1. input - Enter recipient address, amount, password
|
* 1. input - Enter recipient address and amount (QR scan or manual input)
|
||||||
* 2. preparing - Preparing transaction (getting nonce, gas, etc.)
|
* 2. preparing - Preparing transaction (getting nonce, gas, etc.)
|
||||||
* 3. confirm - Confirm transaction details with gas fees
|
* 3. confirm - Confirm transaction details with gas fees
|
||||||
* 4. signing - TSS multi-party signing in progress
|
* 4. signing - TSS multi-party signing in progress
|
||||||
* 5. broadcasting - Broadcasting signed transaction
|
* 5. broadcasting - Broadcasting signed transaction
|
||||||
* 6. completed - Show transaction hash and explorer link
|
* 6. completed - Show transaction hash and explorer link
|
||||||
* 7. error - Show error message
|
* 7. error - Show error message
|
||||||
|
*
|
||||||
|
* Note: No password required - TSS wallets don't use passwords
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -65,13 +69,31 @@ fun TransferScreen(
|
||||||
onCopyInviteCode: () -> Unit,
|
onCopyInviteCode: () -> Unit,
|
||||||
onBroadcastTransaction: () -> Unit,
|
onBroadcastTransaction: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onBackToWallets: () -> Unit,
|
onBackToWallets: () -> Unit
|
||||||
onScanQrCode: () -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
var toAddress by remember { mutableStateOf("") }
|
var toAddress by remember { mutableStateOf("") }
|
||||||
var amount by remember { mutableStateOf("") }
|
var amount by remember { mutableStateOf("") }
|
||||||
var validationError by remember { mutableStateOf<String?>(null) }
|
var validationError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
// QR Scanner launcher for recipient address
|
||||||
|
val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||||
|
result.contents?.let { scannedContent ->
|
||||||
|
// Handle scanned address - may be plain address or EIP-681 URI
|
||||||
|
val address = if (scannedContent.startsWith("ethereum:")) {
|
||||||
|
// Parse EIP-681 URI: ethereum:0x...[@chainId][/function]?[params]
|
||||||
|
scannedContent.removePrefix("ethereum:").split("@", "/", "?").firstOrNull() ?: scannedContent
|
||||||
|
} else {
|
||||||
|
scannedContent
|
||||||
|
}
|
||||||
|
// Only set if it looks like a valid address
|
||||||
|
if (address.startsWith("0x") && address.length == 42) {
|
||||||
|
toAddress = address
|
||||||
|
} else {
|
||||||
|
validationError = "扫描的不是有效的钱包地址"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine current step
|
// Determine current step
|
||||||
val step = when {
|
val step = when {
|
||||||
txHash != null -> "completed"
|
txHash != null -> "completed"
|
||||||
|
|
@ -132,7 +154,18 @@ fun TransferScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCancel = onCancel,
|
onCancel = onCancel,
|
||||||
onScanQrCode = onScanQrCode
|
onScanQrCode = {
|
||||||
|
val options = ScanOptions().apply {
|
||||||
|
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
|
setPrompt("扫描收款地址二维码")
|
||||||
|
setCameraId(0)
|
||||||
|
setBeepEnabled(true)
|
||||||
|
setBarcodeImageEnabled(false)
|
||||||
|
setOrientationLocked(true)
|
||||||
|
setCaptureActivity(PortraitCaptureActivity::class.java)
|
||||||
|
}
|
||||||
|
scanLauncher.launch(options)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
"preparing" -> PreparingScreen()
|
"preparing" -> PreparingScreen()
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
// Note: Some unused imports kept for ExportBackupDialog which still uses password
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -50,13 +51,11 @@ fun WalletsScreen(
|
||||||
networkType: NetworkType = NetworkType.MAINNET,
|
networkType: NetworkType = NetworkType.MAINNET,
|
||||||
onDeleteShare: (Long) -> Unit,
|
onDeleteShare: (Long) -> Unit,
|
||||||
onRefreshBalance: ((String) -> Unit)? = null,
|
onRefreshBalance: ((String) -> Unit)? = null,
|
||||||
onTransfer: ((shareId: Long, toAddress: String, amount: String, password: String) -> Unit)? = null,
|
onTransfer: ((shareId: Long) -> Unit)? = null,
|
||||||
onExportBackup: ((shareId: Long, password: String) -> Unit)? = null,
|
onExportBackup: ((shareId: Long, password: String) -> Unit)? = null,
|
||||||
onCreateWallet: (() -> Unit)? = null
|
onCreateWallet: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
var selectedWallet by remember { mutableStateOf<ShareRecord?>(null) }
|
var selectedWallet by remember { mutableStateOf<ShareRecord?>(null) }
|
||||||
var showTransferDialog by remember { mutableStateOf(false) }
|
|
||||||
var transferWallet by remember { mutableStateOf<ShareRecord?>(null) }
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -149,8 +148,7 @@ fun WalletsScreen(
|
||||||
balance = balances[share.address],
|
balance = balances[share.address],
|
||||||
onViewDetails = { selectedWallet = share },
|
onViewDetails = { selectedWallet = share },
|
||||||
onTransfer = {
|
onTransfer = {
|
||||||
transferWallet = share
|
onTransfer?.invoke(share.id)
|
||||||
showTransferDialog = true
|
|
||||||
},
|
},
|
||||||
onDelete = { onDeleteShare(share.id) }
|
onDelete = { onDeleteShare(share.id) }
|
||||||
)
|
)
|
||||||
|
|
@ -185,30 +183,13 @@ fun WalletsScreen(
|
||||||
onDismiss = { selectedWallet = null },
|
onDismiss = { selectedWallet = null },
|
||||||
onTransfer = {
|
onTransfer = {
|
||||||
selectedWallet = null
|
selectedWallet = null
|
||||||
transferWallet = wallet
|
onTransfer?.invoke(wallet.id)
|
||||||
showTransferDialog = true
|
|
||||||
},
|
},
|
||||||
onExport = onExportBackup?.let { export ->
|
onExport = onExportBackup?.let { export ->
|
||||||
{ password -> export(wallet.id, password) }
|
{ password -> export(wallet.id, password) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer dialog
|
|
||||||
if (showTransferDialog && transferWallet != null) {
|
|
||||||
TransferDialog(
|
|
||||||
wallet = transferWallet!!,
|
|
||||||
onDismiss = {
|
|
||||||
showTransferDialog = false
|
|
||||||
transferWallet = null
|
|
||||||
},
|
|
||||||
onConfirm = { toAddress, amount, password ->
|
|
||||||
onTransfer?.invoke(transferWallet!!.id, toAddress, amount, password)
|
|
||||||
showTransferDialog = false
|
|
||||||
transferWallet = null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -604,178 +585,6 @@ private fun InfoRow(label: String, value: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TransferDialog(
|
|
||||||
wallet: ShareRecord,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: (toAddress: String, amount: String, password: String) -> Unit
|
|
||||||
) {
|
|
||||||
var toAddress by remember { mutableStateOf("") }
|
|
||||||
var amount by remember { mutableStateOf("") }
|
|
||||||
var password by remember { mutableStateOf("") }
|
|
||||||
var showPassword by remember { mutableStateOf(false) }
|
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(24.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "转账",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "从 ${wallet.address.take(10)}...${wallet.address.takeLast(8)}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// Recipient address
|
|
||||||
OutlinedTextField(
|
|
||||||
value = toAddress,
|
|
||||||
onValueChange = { toAddress = it },
|
|
||||||
label = { Text("收款地址") },
|
|
||||||
placeholder = { Text("0x...") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.Person, contentDescription = null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// Amount
|
|
||||||
OutlinedTextField(
|
|
||||||
value = amount,
|
|
||||||
onValueChange = { amount = it },
|
|
||||||
label = { Text("金额 (KAVA)") },
|
|
||||||
placeholder = { Text("0.0") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.AttachMoney, contentDescription = null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// Password
|
|
||||||
OutlinedTextField(
|
|
||||||
value = password,
|
|
||||||
onValueChange = { password = it },
|
|
||||||
label = { Text("钱包密码") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.Lock, contentDescription = null)
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = { showPassword = !showPassword }) {
|
|
||||||
Icon(
|
|
||||||
if (showPassword) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
error?.let {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// Info card
|
|
||||||
Card(
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(12.dp),
|
|
||||||
verticalAlignment = Alignment.Top
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Info,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "转账需要 ${wallet.thresholdT} 个参与者共同签名。确认后将创建签名会话。",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = onDismiss,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text("取消")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
when {
|
|
||||||
toAddress.isBlank() -> error = "请输入收款地址"
|
|
||||||
!toAddress.startsWith("0x") || toAddress.length != 42 -> error = "地址格式不正确"
|
|
||||||
amount.isBlank() -> error = "请输入金额"
|
|
||||||
amount.toDoubleOrNull() == null || amount.toDouble() <= 0 -> error = "金额无效"
|
|
||||||
password.isBlank() -> error = "请输入密码"
|
|
||||||
else -> {
|
|
||||||
error = null
|
|
||||||
onConfirm(toAddress, amount, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Send,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text("确认转账")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExportBackupDialog(
|
private fun ExportBackupDialog(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue