fix: 修复 Magic 账户赎回失败问题
主要修复: 1. 修复 PROXY ABI 编码问题: - 添加缺失的 tuple offset 字段(32字节) - 修正 data offset 从 192 改为 128(符合 ABI 规范) - 与 builder-relayer-client 保持一致 2. 添加 gas limit 动态估算: - 通过 eth_estimateGas 估算合理的 gas limit - 避免硬编码 10M 导致 Relay Hub gasleft() 检查失败 - 估算失败时回退到默认值 3. 优化 API Key 检查逻辑: - 仅在涉及 Magic 账户时提前检查 Builder API Key - Safe 账户可在未配置 API Key 时使用手动交易 修复的问题: - Magic 赎回交易失败:Not enough gasleft() - PROXY 编码格式与官方不一致 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+11
-10
@@ -1376,13 +1376,6 @@ class AccountService(
|
|||||||
*/
|
*/
|
||||||
suspend fun redeemPositions(request: PositionRedeemRequest): Result<PositionRedeemResponse> {
|
suspend fun redeemPositions(request: PositionRedeemRequest): Result<PositionRedeemResponse> {
|
||||||
return try {
|
return try {
|
||||||
// 检查 Builder API Key 是否已配置
|
|
||||||
if (!relayClientService.isBuilderApiKeyConfigured()) {
|
|
||||||
return Result.failure(
|
|
||||||
IllegalStateException("Builder API Key 未配置,无法执行 Gasless 交易。请前往系统设置页面配置 Builder API Key。")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.positions.isEmpty()) {
|
if (request.positions.isEmpty()) {
|
||||||
return Result.failure(IllegalArgumentException("赎回仓位列表不能为空"))
|
return Result.failure(IllegalArgumentException("赎回仓位列表不能为空"))
|
||||||
}
|
}
|
||||||
@@ -1404,7 +1397,15 @@ class AccountService(
|
|||||||
accounts[accountId] = account
|
accounts[accountId] = account
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 验证并收集要赎回的仓位信息(按账户分组)
|
// 4. 若涉及 Magic 账户,必须已配置 Builder API Key(提前判断,避免执行到深层再报错)
|
||||||
|
val hasMagicAccount = accounts.values.any { it.walletType.equals("magic", ignoreCase = true) }
|
||||||
|
if (hasMagicAccount && !relayClientService.isBuilderApiKeyConfigured()) {
|
||||||
|
return Result.failure(
|
||||||
|
IllegalStateException("Builder API Key 未配置,无法执行 Magic 账户赎回(Gasless)。请前往系统设置页面配置 Builder API Key。")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 验证并收集要赎回的仓位信息(按账户分组)
|
||||||
val accountRedeemData = mutableMapOf<Long, MutableList<Pair<AccountPositionDto, BigInteger>>>()
|
val accountRedeemData = mutableMapOf<Long, MutableList<Pair<AccountPositionDto, BigInteger>>>()
|
||||||
val accountRedeemedInfo =
|
val accountRedeemedInfo =
|
||||||
mutableMapOf<Long, MutableList<com.wrbug.polymarketbot.dto.RedeemedPositionInfo>>()
|
mutableMapOf<Long, MutableList<com.wrbug.polymarketbot.dto.RedeemedPositionInfo>>()
|
||||||
@@ -1447,7 +1448,7 @@ class AccountService(
|
|||||||
accountRedeemedInfo[accountId] = accountInfo
|
accountRedeemedInfo[accountId] = accountInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 对每个账户执行赎回(Safe 与 Magic 均支持,Magic 通过 Builder Relayer PROXY Gasless 执行)
|
// 6. 对每个账户执行赎回(Safe 与 Magic 均支持,Magic 通过 Builder Relayer PROXY Gasless 执行)
|
||||||
val accountTransactions = mutableListOf<com.wrbug.polymarketbot.dto.AccountRedeemTransaction>()
|
val accountTransactions = mutableListOf<com.wrbug.polymarketbot.dto.AccountRedeemTransaction>()
|
||||||
var totalRedeemedValue = BigDecimal.ZERO
|
var totalRedeemedValue = BigDecimal.ZERO
|
||||||
|
|
||||||
@@ -1503,7 +1504,7 @@ class AccountService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 发送赎回推送通知(异步,不阻塞)
|
// 7. 发送赎回推送通知(异步,不阻塞)
|
||||||
notificationScope.launch {
|
notificationScope.launch {
|
||||||
try {
|
try {
|
||||||
// 获取当前语言设置
|
// 获取当前语言设置
|
||||||
|
|||||||
+77
-3
@@ -300,7 +300,14 @@ class RelayClientService(
|
|||||||
val nonce = relayPayload.nonce
|
val nonce = relayPayload.nonce
|
||||||
|
|
||||||
val proxyCallData = encodeProxyTransactionData(safeTx)
|
val proxyCallData = encodeProxyTransactionData(safeTx)
|
||||||
val gasLimit = defaultProxyGasLimit
|
|
||||||
|
// 估算 gas limit(参考 builder-relayer-client builder/proxy.ts getGasLimit)
|
||||||
|
val gasLimit = try {
|
||||||
|
estimateProxyGasLimit(fromAddress, proxyFactoryAddress, proxyCallData)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("估算 PROXY gas limit 失败,使用默认值: ${e.message}", e)
|
||||||
|
defaultProxyGasLimit
|
||||||
|
}
|
||||||
|
|
||||||
val structHash = createProxyStructHash(
|
val structHash = createProxyStructHash(
|
||||||
from = fromAddress,
|
from = fromAddress,
|
||||||
@@ -365,6 +372,20 @@ class RelayClientService(
|
|||||||
/**
|
/**
|
||||||
* 编码 ProxyFactory.proxy(calls) 调用数据
|
* 编码 ProxyFactory.proxy(calls) 调用数据
|
||||||
* 参考: builder-relayer-client encode/proxy.ts, abis proxyFactory proxy((uint8,address,uint256,bytes)[])
|
* 参考: builder-relayer-client encode/proxy.ts, abis proxyFactory proxy((uint8,address,uint256,bytes)[])
|
||||||
|
*
|
||||||
|
* ABI 编码规则:当 tuple 数组中的 tuple 包含动态类型(bytes)时,需要先存储 tuple offset
|
||||||
|
* 结构:
|
||||||
|
* - selector (4 bytes)
|
||||||
|
* - array offset (32 bytes) = 32
|
||||||
|
* - array length (32 bytes) = 1
|
||||||
|
* - tuple[0] offset (32 bytes) = 32 (指向 tuple 数据开始,从 array length 之后计算)
|
||||||
|
* - tuple[0] 数据:
|
||||||
|
* - typeCode (32 bytes) = 1
|
||||||
|
* - to (32 bytes)
|
||||||
|
* - value (32 bytes) = 0
|
||||||
|
* - data offset (32 bytes) = 128 (从 tuple 数据开始计算)
|
||||||
|
* - data length (32 bytes)
|
||||||
|
* - data (padded to 32-byte boundary)
|
||||||
*/
|
*/
|
||||||
private fun encodeProxyTransactionData(safeTx: SafeTransaction): String {
|
private fun encodeProxyTransactionData(safeTx: SafeTransaction): String {
|
||||||
val selector = EthereumUtils.getFunctionSelector("proxy((uint8,address,uint256,bytes)[])")
|
val selector = EthereumUtils.getFunctionSelector("proxy((uint8,address,uint256,bytes)[])")
|
||||||
@@ -373,19 +394,72 @@ class RelayClientService(
|
|||||||
val dataLenPadded = (dataLen + 31) / 32 * 32 * 2
|
val dataLenPadded = (dataLen + 31) / 32 * 32 * 2
|
||||||
val dataPadded = callData.padEnd(dataLenPadded, '0')
|
val dataPadded = callData.padEnd(dataLenPadded, '0')
|
||||||
|
|
||||||
|
// ABI 编码:tuple 数组,tuple 包含动态类型 bytes
|
||||||
|
// 1. array offset: 32 (指向 array length)
|
||||||
val arrayOffset = EthereumUtils.encodeUint256(BigInteger.valueOf(32))
|
val arrayOffset = EthereumUtils.encodeUint256(BigInteger.valueOf(32))
|
||||||
|
// 2. array length: 1
|
||||||
val arrayLength = EthereumUtils.encodeUint256(BigInteger.ONE)
|
val arrayLength = EthereumUtils.encodeUint256(BigInteger.ONE)
|
||||||
|
// 3. tuple[0] offset: 32 (指向 tuple 数据开始,从 array length 之后计算)
|
||||||
|
val tupleOffset = EthereumUtils.encodeUint256(BigInteger.valueOf(32))
|
||||||
|
// 4. tuple[0] 数据:
|
||||||
|
// - typeCode: 1
|
||||||
val typeCode = EthereumUtils.encodeUint256(BigInteger.ONE)
|
val typeCode = EthereumUtils.encodeUint256(BigInteger.ONE)
|
||||||
|
// - to: address
|
||||||
val toEncoded = EthereumUtils.encodeAddress(safeTx.to)
|
val toEncoded = EthereumUtils.encodeAddress(safeTx.to)
|
||||||
|
// - value: 0
|
||||||
val valueEncoded = EthereumUtils.encodeUint256(BigInteger.ZERO)
|
val valueEncoded = EthereumUtils.encodeUint256(BigInteger.ZERO)
|
||||||
val dataOffsetInTuple = BigInteger.valueOf(192)
|
// - data offset: 128 (从 tuple 数据开始计算,typeCode+to+value = 3*32 = 96,加上 offset 字段 = 128)
|
||||||
|
val dataOffsetInTuple = BigInteger.valueOf(128)
|
||||||
val dataOffsetEncoded = EthereumUtils.encodeUint256(dataOffsetInTuple)
|
val dataOffsetEncoded = EthereumUtils.encodeUint256(dataOffsetInTuple)
|
||||||
|
// - data length
|
||||||
val dataLengthEncoded = EthereumUtils.encodeUint256(BigInteger.valueOf(dataLen.toLong()))
|
val dataLengthEncoded = EthereumUtils.encodeUint256(BigInteger.valueOf(dataLen.toLong()))
|
||||||
|
// - data (padded)
|
||||||
|
|
||||||
return "0x" + selector.removePrefix("0x") + arrayOffset + arrayLength +
|
return "0x" + selector.removePrefix("0x") + arrayOffset + arrayLength +
|
||||||
typeCode + toEncoded + valueEncoded + dataOffsetEncoded +
|
tupleOffset + typeCode + toEncoded + valueEncoded + dataOffsetEncoded +
|
||||||
dataLengthEncoded + dataPadded
|
dataLengthEncoded + dataPadded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 估算 PROXY 交易的 gas limit
|
||||||
|
* 参考: builder-relayer-client builder/proxy.ts getGasLimit
|
||||||
|
*/
|
||||||
|
private suspend fun estimateProxyGasLimit(
|
||||||
|
from: String,
|
||||||
|
to: String,
|
||||||
|
data: String
|
||||||
|
): String {
|
||||||
|
val rpcApi = polygonRpcApi
|
||||||
|
|
||||||
|
val rpcRequest = JsonRpcRequest(
|
||||||
|
method = "eth_estimateGas",
|
||||||
|
params = listOf(
|
||||||
|
mapOf(
|
||||||
|
"from" to from,
|
||||||
|
"to" to to,
|
||||||
|
"data" to data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = rpcApi.call(rpcRequest)
|
||||||
|
if (!response.isSuccessful || response.body() == null) {
|
||||||
|
throw Exception("eth_estimateGas 调用失败: ${response.code()} ${response.message()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val rpcResponse = response.body()!!
|
||||||
|
if (rpcResponse.error != null) {
|
||||||
|
throw Exception("eth_estimateGas 返回错误: ${rpcResponse.error.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val hexGasLimit = rpcResponse.result?.asString
|
||||||
|
?: throw Exception("eth_estimateGas 结果为空")
|
||||||
|
|
||||||
|
// 将十六进制转换为十进制字符串
|
||||||
|
val gasLimitBigInt = BigInteger(hexGasLimit.removePrefix("0x"), 16)
|
||||||
|
return gasLimitBigInt.toString()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建 PROXY 结构哈希,参考 builder-relayer-client builder/proxy.ts createStructHash
|
* 创建 PROXY 结构哈希,参考 builder-relayer-client builder/proxy.ts createStructHash
|
||||||
* concat: "rlx:" + from + to + data + txFee + gasPrice + gasLimit + nonce + relayHub + relay, then keccak256
|
* concat: "rlx:" + from + to + data + txFee + gasPrice + gasLimit + nonce + relayHub + relay, then keccak256
|
||||||
|
|||||||
Reference in New Issue
Block a user