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> {
|
||||
return try {
|
||||
// 检查 Builder API Key 是否已配置
|
||||
if (!relayClientService.isBuilderApiKeyConfigured()) {
|
||||
return Result.failure(
|
||||
IllegalStateException("Builder API Key 未配置,无法执行 Gasless 交易。请前往系统设置页面配置 Builder API Key。")
|
||||
)
|
||||
}
|
||||
|
||||
if (request.positions.isEmpty()) {
|
||||
return Result.failure(IllegalArgumentException("赎回仓位列表不能为空"))
|
||||
}
|
||||
@@ -1404,7 +1397,15 @@ class AccountService(
|
||||
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 accountRedeemedInfo =
|
||||
mutableMapOf<Long, MutableList<com.wrbug.polymarketbot.dto.RedeemedPositionInfo>>()
|
||||
@@ -1447,7 +1448,7 @@ class AccountService(
|
||||
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>()
|
||||
var totalRedeemedValue = BigDecimal.ZERO
|
||||
|
||||
@@ -1503,7 +1504,7 @@ class AccountService(
|
||||
)
|
||||
}
|
||||
|
||||
// 6. 发送赎回推送通知(异步,不阻塞)
|
||||
// 7. 发送赎回推送通知(异步,不阻塞)
|
||||
notificationScope.launch {
|
||||
try {
|
||||
// 获取当前语言设置
|
||||
|
||||
+77
-3
@@ -300,7 +300,14 @@ class RelayClientService(
|
||||
val nonce = relayPayload.nonce
|
||||
|
||||
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(
|
||||
from = fromAddress,
|
||||
@@ -365,6 +372,20 @@ class RelayClientService(
|
||||
/**
|
||||
* 编码 ProxyFactory.proxy(calls) 调用数据
|
||||
* 参考: 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 {
|
||||
val selector = EthereumUtils.getFunctionSelector("proxy((uint8,address,uint256,bytes)[])")
|
||||
@@ -373,19 +394,72 @@ class RelayClientService(
|
||||
val dataLenPadded = (dataLen + 31) / 32 * 32 * 2
|
||||
val dataPadded = callData.padEnd(dataLenPadded, '0')
|
||||
|
||||
// ABI 编码:tuple 数组,tuple 包含动态类型 bytes
|
||||
// 1. array offset: 32 (指向 array length)
|
||||
val arrayOffset = EthereumUtils.encodeUint256(BigInteger.valueOf(32))
|
||||
// 2. array length: 1
|
||||
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)
|
||||
// - to: address
|
||||
val toEncoded = EthereumUtils.encodeAddress(safeTx.to)
|
||||
// - value: 0
|
||||
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)
|
||||
// - data length
|
||||
val dataLengthEncoded = EthereumUtils.encodeUint256(BigInteger.valueOf(dataLen.toLong()))
|
||||
// - data (padded)
|
||||
|
||||
return "0x" + selector.removePrefix("0x") + arrayOffset + arrayLength +
|
||||
typeCode + toEncoded + valueEncoded + dataOffsetEncoded +
|
||||
tupleOffset + typeCode + toEncoded + valueEncoded + dataOffsetEncoded +
|
||||
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
|
||||
* concat: "rlx:" + from + to + data + txFee + gasPrice + gasLimit + nonce + relayHub + relay, then keccak256
|
||||
|
||||
Reference in New Issue
Block a user