diff --git a/.gitignore b/.gitignore index e475349..359db98 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,7 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* .pnpm-store/ - +polymarket-trading-bot/ # Frontend build frontend/dist/ frontend/.vite/ diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 9e353d8..01a2859 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -54,6 +54,9 @@ dependencies { // Keccak-256 for Ethereum function selector implementation("org.bouncycastle:bcprov-jdk18on:1.78.1") + // Web3j for Ethereum wallet and EIP-712 signing + implementation("org.web3j:core:5.0.0") + // Logging implementation("org.slf4j:slf4j-api") diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt index fd6860a..bc52b0b 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt @@ -110,6 +110,29 @@ interface PolymarketClobApi { @Query("after") after: String? = null, @Query("next_cursor") next_cursor: String? = null ): Response + + /** + * 创建 API Key(L1 认证) + * 端点: /auth/api-key + * 需要 L1 认证头(POLY_ADDRESS, POLY_SIGNATURE, POLY_TIMESTAMP, POLY_NONCE) + */ + @POST("/auth/api-key") + suspend fun createApiKey(): Response + + /** + * 获取现有 API Key(L1 认证) + * 端点: /auth/derive-api-key + * 需要 L1 认证头(POLY_ADDRESS, POLY_SIGNATURE, POLY_TIMESTAMP, POLY_NONCE) + */ + @GET("/auth/derive-api-key") + suspend fun deriveApiKey(): Response + + /** + * 获取服务器时间 + * 端点: /time + */ + @GET("/time") + suspend fun getServerTime(): Response } // 请求和响应数据类 @@ -205,3 +228,19 @@ data class GetTradesResponse( val next_cursor: String? = null ) +/** + * API Key 响应 + */ +data class ApiKeyResponse( + val apiKey: String, + val secret: String, + val passphrase: String +) + +/** + * 服务器时间响应 + */ +data class ServerTimeResponse( + val timestamp: Long +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketSubgraphApi.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketSubgraphApi.kt index b43e442..7fbaae9 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketSubgraphApi.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketSubgraphApi.kt @@ -29,6 +29,16 @@ interface PolymarketDataApi { @Query("sortDirection") sortDirection: String? = null, @Query("title") title: String? = null ): Response> + + /** + * 获取用户仓位总价值 + * 文档: https://docs.polymarket.com/api-reference/core/get-total-value-of-a-users-positions + */ + @GET("/value") + suspend fun getTotalValue( + @Query("user") user: String, + @Query("market") market: List? = null + ): Response> } /** @@ -62,4 +72,12 @@ data class PositionResponse( val negativeRisk: Boolean? = null ) +/** + * 仓位价值响应(根据 Polymarket Data API 文档) + */ +data class ValueResponse( + val user: String, + val value: Double +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt index 32c0bad..5bd21a8 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt @@ -7,9 +7,6 @@ data class AccountImportRequest( val privateKey: String, // 私钥(前端加密后传输) val walletAddress: String, // 钱包地址(前端从私钥推导,用于验证) val accountName: String? = null, - val apiKey: String? = null, // Polymarket API Key(可选) - val apiSecret: String? = null, // Polymarket API Secret(可选) - val apiPassphrase: String? = null, // Polymarket API Passphrase(可选) val isDefault: Boolean = false ) @@ -19,9 +16,6 @@ data class AccountImportRequest( data class AccountUpdateRequest( val accountId: Long, val accountName: String? = null, - val apiKey: String? = null, - val apiSecret: String? = null, - val apiPassphrase: String? = null, val isDefault: Boolean? = null ) @@ -66,7 +60,10 @@ data class AccountDto( val apiPassphraseConfigured: Boolean, // API Passphrase 是否已配置 val balance: String? = null, // 账户余额(可选) val totalOrders: Long? = null, // 总订单数(可选) - val totalPnl: String? = null // 总盈亏(可选) + val totalPnl: String? = null, // 总盈亏(可选) + val activeOrders: Long? = null, // 活跃订单数(可选) + val completedOrders: Long? = null, // 已完成订单数(可选) + val positionCount: Long? = null // 持仓数量(可选) ) /** diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/Account.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/Account.kt index dd5f132..b697703 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/Account.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/Account.kt @@ -14,7 +14,7 @@ data class Account( val id: Long? = null, @Column(name = "private_key", nullable = false, length = 500) - val privateKey: String, // 私钥(加密存储) + val privateKey: String, // 私钥(明文存储) @Column(name = "wallet_address", unique = true, nullable = false, length = 42) val walletAddress: String, // 钱包地址(从私钥推导) @@ -23,13 +23,13 @@ data class Account( val proxyAddress: String, // Polymarket 代理钱包地址(从合约获取,必须) @Column(name = "api_key", length = 500) - val apiKey: String? = null, // Polymarket API Key(可选,加密存储) + val apiKey: String? = null, // Polymarket API Key(可选,明文存储) @Column(name = "api_secret", length = 500) - val apiSecret: String? = null, // Polymarket API Secret(可选,加密存储) + val apiSecret: String? = null, // Polymarket API Secret(可选,明文存储) @Column(name = "api_passphrase", length = 500) - val apiPassphrase: String? = null, // Polymarket API Passphrase(可选,加密存储) + val apiPassphrase: String? = null, // Polymarket API Passphrase(可选,明文存储) @Column(name = "account_name", length = 100) val accountName: String? = null, diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt index 3e2df48..90b718b 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt @@ -1,9 +1,9 @@ package com.wrbug.polymarketbot.service +import com.wrbug.polymarketbot.api.TradeResponse import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.entity.Account import com.wrbug.polymarketbot.repository.AccountRepository -import com.wrbug.polymarketbot.util.CryptoUtils import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.toSafeBigDecimal import kotlinx.coroutines.runBlocking @@ -18,10 +18,10 @@ import java.math.BigDecimal @Service class AccountService( private val accountRepository: AccountRepository, - private val cryptoUtils: CryptoUtils, private val clobService: PolymarketClobService, private val retrofitFactory: RetrofitFactory, - private val blockchainService: BlockchainService + private val blockchainService: BlockchainService, + private val apiKeyService: PolymarketApiKeyService ) { private val logger = LoggerFactory.getLogger(AccountService::class.java) @@ -49,11 +49,30 @@ class AccountService( return Result.failure(IllegalArgumentException("无效的私钥格式")) } - // 4. 加密私钥和 API 凭证 - val encryptedPrivateKey = cryptoUtils.encrypt(request.privateKey) - val encryptedApiKey = request.apiKey?.let { cryptoUtils.encrypt(it) } - val encryptedApiSecret = request.apiSecret?.let { cryptoUtils.encrypt(it) } - val encryptedApiPassphrase = request.apiPassphrase?.let { cryptoUtils.encrypt(it) } + // 4. 自动获取或创建 API Key(必须成功,否则导入失败) + logger.info("开始自动获取或创建 API Key: ${request.walletAddress}") + val apiKeyCreds = runBlocking { + val result = apiKeyService.createOrDeriveApiKey( + privateKey = request.privateKey, + walletAddress = request.walletAddress, + chainId = 137L // Polygon 主网 + ) + + if (result.isSuccess) { + val creds = result.getOrNull() + if (creds != null) { + logger.info("成功自动获取 API Key: ${request.walletAddress}") + creds + } else { + logger.error("自动获取 API Key 返回空值") + throw IllegalStateException("自动获取 API Key 失败:返回值为空") + } + } else { + val error = result.exceptionOrNull() + logger.error("自动获取 API Key 失败: ${error?.message}") + throw IllegalStateException("自动获取 API Key 失败: ${error?.message}。请确保私钥有效且账户已激活") + } + } // 5. 如果设置为默认账户,取消其他账户的默认状态 if (request.isDefault) { @@ -84,12 +103,12 @@ class AccountService( // 7. 创建账户 val account = Account( - privateKey = encryptedPrivateKey, + privateKey = request.privateKey, walletAddress = request.walletAddress, proxyAddress = proxyAddress, - apiKey = encryptedApiKey, - apiSecret = encryptedApiSecret, - apiPassphrase = encryptedApiPassphrase, + apiKey = apiKeyCreds.apiKey, + apiSecret = apiKeyCreds.secret, + apiPassphrase = apiKeyCreds.passphrase, accountName = request.accountName, isDefault = request.isDefault, createdAt = System.currentTimeMillis(), @@ -118,23 +137,6 @@ class AccountService( // 更新账户名称 val updatedAccountName = request.accountName ?: account.accountName - // 更新 API 凭证 - val updatedApiKey = if (request.apiKey != null) { - cryptoUtils.encrypt(request.apiKey) - } else { - account.apiKey - } - val updatedApiSecret = if (request.apiSecret != null) { - cryptoUtils.encrypt(request.apiSecret) - } else { - account.apiSecret - } - val updatedApiPassphrase = if (request.apiPassphrase != null) { - cryptoUtils.encrypt(request.apiPassphrase) - } else { - account.apiPassphrase - } - // 如果设置为默认账户,取消其他账户的默认状态 val updatedIsDefault = request.isDefault ?: account.isDefault if (updatedIsDefault && !account.isDefault) { @@ -146,9 +148,6 @@ class AccountService( val updated = account.copy( accountName = updatedAccountName, - apiKey = updatedApiKey, - apiSecret = updatedApiSecret, - apiPassphrase = updatedApiPassphrase, isDefault = updatedIsDefault, updatedAt = System.currentTimeMillis() ) @@ -262,7 +261,7 @@ class AccountService( // 查询 USDC 余额和持仓信息 val balanceResult = runBlocking { try { - // 先查询持仓信息(用于计算仓位余额和返回持仓列表) + // 查询持仓信息(用于返回持仓列表) // 使用代理地址查询持仓(Polymarket 使用代理地址存储持仓) val positionsResult = blockchainService.getPositions(account.proxyAddress) val positions = if (positionsResult.isSuccess) { @@ -281,9 +280,13 @@ class AccountService( emptyList() } - // 计算仓位余额(持仓总价值) - val positionBalance = positions.sumOf { - it.currentValue.toSafeBigDecimal() + // 使用 /value 接口获取仓位总价值(而不是累加) + val positionBalanceResult = blockchainService.getTotalValue(account.proxyAddress) + val positionBalance = if (positionBalanceResult.isSuccess) { + positionBalanceResult.getOrNull() ?: "0" + } else { + logger.warn("仓位总价值查询失败: ${positionBalanceResult.exceptionOrNull()?.message}") + "0" } // 查询可用余额(通过 RPC 查询 USDC 余额) @@ -302,11 +305,11 @@ class AccountService( } // 计算总余额 = 可用余额 + 仓位余额 - val totalBalance = availableBalance.toSafeBigDecimal().add(positionBalance) + val totalBalance = availableBalance.toSafeBigDecimal().add(positionBalance.toSafeBigDecimal()) AccountBalanceResponse( availableBalance = availableBalance, - positionBalance = positionBalance.toPlainString(), + positionBalance = positionBalance, totalBalance = totalBalance.toPlainString(), positions = positions ) @@ -354,7 +357,7 @@ class AccountService( /** * 转换为 DTO - * 包含交易统计数据(总订单数和总盈亏) + * 包含交易统计数据(总订单数、总盈亏、活跃订单数、已完成订单数、持仓数量) */ private fun toDto(account: Account): AccountDto { return runBlocking { @@ -368,7 +371,10 @@ class AccountService( apiSecretConfigured = account.apiSecret != null, apiPassphraseConfigured = account.apiPassphrase != null, totalOrders = statistics.totalOrders, - totalPnl = statistics.totalPnl + totalPnl = statistics.totalPnl, + activeOrders = statistics.activeOrders, + completedOrders = statistics.completedOrders, + positionCount = statistics.positionCount ) } } @@ -380,82 +386,138 @@ class AccountService( return try { // 如果账户没有配置 API 凭证,无法查询统计数据 if (account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) { - return AccountStatistics(totalOrders = null, totalPnl = null) + return AccountStatistics( + totalOrders = null, + totalPnl = null, + activeOrders = null, + completedOrders = null, + positionCount = null + ) } - // 解密 API 凭证 - val apiKey = cryptoUtils.decrypt(account.apiKey) - val apiSecret = cryptoUtils.decrypt(account.apiSecret) - val apiPassphrase = cryptoUtils.decrypt(account.apiPassphrase) + // 使用 API 凭证(直接使用,无需解密) + val apiKey = account.apiKey + val apiSecret = account.apiSecret + val apiPassphrase = account.apiPassphrase - // 创建带认证的 API 客户端 - val clobApi = retrofitFactory.createClobApi(apiKey, apiSecret, apiPassphrase) + // 创建带认证的 API 客户端(需要钱包地址用于 POLY_ADDRESS 请求头) + val clobApi = retrofitFactory.createClobApi(apiKey, apiSecret, apiPassphrase, account.walletAddress) - // 1. 查询交易记录数量(总订单数) - val tradesResult = runBlocking { - try { - // 使用代理地址查询交易记录 + // 1. 查询活跃订单数量(open/active 状态) + val activeOrdersResult = try { + var totalActiveOrders = 0L + var nextCursor: String? = null + + // 分页查询所有活跃订单 + do { + val response = clobApi.getActiveOrders( + id = null, + market = null, + asset_id = null, + next_cursor = nextCursor + ) + if (response.isSuccessful && response.body() != null) { + val ordersResponse = response.body()!! + totalActiveOrders += ordersResponse.data.size + nextCursor = ordersResponse.next_cursor + } else { + break + } + } while (nextCursor != null && nextCursor.isNotEmpty()) + + Result.success(totalActiveOrders) + } catch (e: Exception) { + logger.warn("查询活跃订单失败: ${e.message}", e) + Result.failure(e) + } + + // 2. 查询已完成订单数 + // 注意:交易记录数不等于已完成订单数,因为一个订单可能产生多笔交易 + // 已完成订单应该是指已完全成交或已关闭的订单 + // 由于 Polymarket CLOB API 没有直接查询所有订单(包括已完成)的接口, + // 我们通过查询交易记录来估算已完成订单数 + // 但更准确的方式是统计去重后的订单ID数量 + val completedOrdersResult = try { + // 使用代理地址查询交易记录(作为 maker 的交易) + var allTrades = mutableListOf() + var nextCursor: String? = null + + // 分页查询所有交易(作为 maker) + do { val response = clobApi.getTrades( maker_address = account.proxyAddress, - next_cursor = null + next_cursor = nextCursor ) if (response.isSuccessful && response.body() != null) { val tradesResponse = response.body()!! - // 统计所有交易(需要分页查询所有) - var totalTrades = tradesResponse.data.size - var nextCursor = tradesResponse.next_cursor - - // 分页查询所有交易 - while (nextCursor != null && nextCursor.isNotEmpty()) { - val nextResponse = clobApi.getTrades( - maker_address = account.proxyAddress, - next_cursor = nextCursor - ) - if (nextResponse.isSuccessful && nextResponse.body() != null) { - val nextTradesResponse = nextResponse.body()!! - totalTrades += nextTradesResponse.data.size - nextCursor = nextTradesResponse.next_cursor - } else { - break - } - } - Result.success(totalTrades.toLong()) + allTrades.addAll(tradesResponse.data) + nextCursor = tradesResponse.next_cursor } else { - Result.failure(Exception("查询交易记录失败: ${response.code()} ${response.message()}")) + break } - } catch (e: Exception) { - logger.warn("查询交易记录失败: ${e.message}", e) - Result.failure(e) - } + } while (nextCursor != null && nextCursor.isNotEmpty()) + + // 注意:Polymarket API 的 getTrades 接口只支持查询 maker_address, + // 如果需要查询作为 taker 的交易,可能需要使用其他接口或查询方式 + // 目前只统计作为 maker 的交易记录 + + // 由于 TradeResponse 没有 orderId 字段,我们无法直接去重订单 + // 这里使用交易记录数作为已完成订单数的近似值 + // 更准确的方式需要查询所有订单并统计状态为 "filled" 的订单 + val completedOrdersCount = allTrades.size.toLong() + + Result.success(completedOrdersCount) + } catch (e: Exception) { + logger.warn("查询交易记录失败: ${e.message}", e) + Result.failure(e) } - // 2. 查询仓位信息计算总盈亏(已实现盈亏) - val totalPnlResult = runBlocking { - try { - val positionsResult = blockchainService.getPositions(account.proxyAddress) - if (positionsResult.isSuccess) { - val positions = positionsResult.getOrNull() ?: emptyList() - // 汇总所有仓位的已实现盈亏 - val totalRealizedPnl = positions.sumOf { pos -> - pos.realizedPnl?.toSafeBigDecimal() ?: BigDecimal.ZERO - } - Result.success(totalRealizedPnl.toPlainString()) - } else { - Result.failure(Exception("查询仓位信息失败")) + // 3. 查询仓位信息计算总盈亏(已实现盈亏)和持仓数量 + val positionsResult = try { + val positions = blockchainService.getPositions(account.proxyAddress) + if (positions.isSuccess) { + val positionList = positions.getOrNull() ?: emptyList() + // 汇总所有仓位的已实现盈亏 + val totalRealizedPnl = positionList.sumOf { pos -> + pos.realizedPnl?.toSafeBigDecimal() ?: BigDecimal.ZERO } - } catch (e: Exception) { - logger.warn("查询仓位盈亏失败: ${e.message}", e) - Result.failure(e) + // 统计持仓数量(所有非零持仓,包括正负仓位) + // size 可能为正数(做多)或负数(做空),都应该统计 + val positionCount = positionList.count { pos -> + val size = pos.size?.toSafeBigDecimal() ?: BigDecimal.ZERO + size != BigDecimal.ZERO // 统计所有非零持仓 + } + Result.success(Pair(totalRealizedPnl.toPlainString(), positionCount.toLong())) + } else { + Result.failure(Exception("查询仓位信息失败")) } + } catch (e: Exception) { + logger.warn("查询仓位信息失败: ${e.message}", e) + Result.failure(e) } + val activeOrders = activeOrdersResult.getOrNull() ?: 0L + val completedOrders = completedOrdersResult.getOrNull() ?: 0L + // 总订单数 = 活跃订单数 + 已完成订单数 + val totalOrders = activeOrders + completedOrders + val (totalPnl, positionCount) = positionsResult.getOrNull() ?: Pair(null, null) + AccountStatistics( - totalOrders = tradesResult.getOrNull(), - totalPnl = totalPnlResult.getOrNull() + totalOrders = totalOrders, + totalPnl = totalPnl, + activeOrders = activeOrders, + completedOrders = completedOrders, // 已完成订单数 = 交易记录数(已成交的订单) + positionCount = positionCount ) } catch (e: Exception) { logger.warn("获取账户统计数据失败: ${e.message}", e) - AccountStatistics(totalOrders = null, totalPnl = null) + AccountStatistics( + totalOrders = null, + totalPnl = null, + activeOrders = null, + completedOrders = null, + positionCount = null + ) } } @@ -464,7 +526,10 @@ class AccountService( */ private data class AccountStatistics( val totalOrders: Long?, - val totalPnl: String? + val totalPnl: String?, + val activeOrders: Long?, + val completedOrders: Long?, + val positionCount: Long? ) /** @@ -496,13 +561,13 @@ class AccountService( return false } - // 解密 API 凭证(前面已检查不为 null) - val apiKey = cryptoUtils.decrypt(account.apiKey) - val apiSecret = cryptoUtils.decrypt(account.apiSecret) - val apiPassphrase = cryptoUtils.decrypt(account.apiPassphrase) + // 使用 API 凭证(直接使用,无需解密) + val apiKey = account.apiKey + val apiSecret = account.apiSecret + val apiPassphrase = account.apiPassphrase - // 创建带认证的 API 客户端 - val clobApi = retrofitFactory.createClobApi(apiKey, apiSecret, apiPassphrase) + // 创建带认证的 API 客户端(需要钱包地址用于 POLY_ADDRESS 请求头) + val clobApi = retrofitFactory.createClobApi(apiKey, apiSecret, apiPassphrase, account.walletAddress) // 查询活跃订单(只查询第一条,用于判断是否有订单) // 使用 next_cursor 参数进行分页,这里只查询第一页 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt index 3422db1..0048388 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt @@ -5,6 +5,7 @@ import com.wrbug.polymarketbot.api.JsonRpcRequest import com.wrbug.polymarketbot.api.JsonRpcResponse import com.wrbug.polymarketbot.api.PolymarketDataApi import com.wrbug.polymarketbot.api.PositionResponse +import com.wrbug.polymarketbot.api.ValueResponse import com.wrbug.polymarketbot.util.EthereumUtils import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.createClient @@ -238,5 +239,39 @@ class BlockchainService( Result.failure(e) } } + + /** + * 获取用户仓位总价值 + * 通过 Polymarket Data API 查询 + * 文档: https://docs.polymarket.com/api-reference/core/get-total-value-of-a-users-positions + */ + suspend fun getTotalValue(proxyWalletAddress: String): Result { + return try { + // 使用代理钱包地址查询仓位总价值 + val response = dataApi.getTotalValue( + user = proxyWalletAddress, + market = null + ) + + if (response.isSuccessful && response.body() != null) { + val values = response.body()!! + // 根据文档,返回的是数组,通常只有一个元素 + val totalValue = if (values.isNotEmpty()) { + values.first().value + } else { + 0.0 + } + logger.debug("查询到仓位总价值: $totalValue") + Result.success(totalValue.toString()) + } else { + val errorMsg = "Data API 请求失败: ${response.code()} ${response.message()}" + logger.error(errorMsg) + Result.failure(Exception(errorMsg)) + } + } catch (e: Exception) { + logger.error("查询仓位总价值失败: ${e.message}", e) + Result.failure(e) + } + } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketApiKeyService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketApiKeyService.kt new file mode 100644 index 0000000..ad43297 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketApiKeyService.kt @@ -0,0 +1,244 @@ +package com.wrbug.polymarketbot.service + +import com.wrbug.polymarketbot.api.ApiKeyResponse +import com.wrbug.polymarketbot.api.PolymarketClobApi +import com.wrbug.polymarketbot.util.PolymarketL1AuthInterceptor +import com.wrbug.polymarketbot.util.RetrofitFactory +import com.wrbug.polymarketbot.util.createClient +import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +/** + * Polymarket API Key 服务 + * 用于自动创建或获取 API Key + */ +@Service +class PolymarketApiKeyService( + @Value("\${polymarket.clob.base-url}") + private val clobBaseUrl: String +) { + + private val logger = LoggerFactory.getLogger(PolymarketApiKeyService::class.java) + + /** + * API Key 凭证数据类 + */ + data class ApiKeyCreds( + val apiKey: String, + val secret: String, + val passphrase: String + ) + + /** + * 创建或获取 API Key + * 先尝试获取现有的(derive),如果不存在,则创建新的 + * 与 JavaScript 实现保持一致:先 derive 后 create + * + * @param privateKey 私钥(十六进制字符串) + * @param walletAddress 钱包地址 + * @param chainId 链 ID(默认 137,Polygon 主网) + * @return API Key 凭证,如果失败则返回错误 + */ + fun createOrDeriveApiKey( + privateKey: String, + walletAddress: String, + chainId: Long = 137L + ): Result { + return runBlocking { + try { + // 先尝试获取现有的 API Key(derive) + val deriveResult = deriveApiKey(privateKey, walletAddress, chainId) + if (deriveResult.isSuccess) { + val creds = deriveResult.getOrNull() + if (creds != null && isApiCreds(creds)) { + logger.info("成功获取现有 API Key: ${walletAddress}") + return@runBlocking Result.success(creds) + } + } + + // 如果获取失败或返回无效,尝试创建新的 + logger.info("获取现有 API Key 失败,尝试创建新的: ${walletAddress}") + val createResult = createApiKey(privateKey, walletAddress, chainId) + if (createResult.isSuccess) { + val creds = createResult.getOrNull() + if (creds != null && isApiCreds(creds)) { + logger.info("成功创建新 API Key: ${walletAddress}") + return@runBlocking Result.success(creds) + } + } + + // 两个都失败 + val error = createResult.exceptionOrNull() ?: deriveResult.exceptionOrNull() + val errorMsg = error?.message ?: "未知错误" + logger.error("获取和创建 API Key 都失败: ${walletAddress}", error) + Result.failure( + IllegalStateException("无法获取或创建 API Key: $errorMsg") + ) + } catch (e: Exception) { + logger.error("创建或获取 API Key 异常: ${walletAddress}", e) + Result.failure(e) + } + } + } + + /** + * 判断 API Key 凭证是否有效 + * 与 JavaScript 的 isApiCreds() 函数对应 + */ + private fun isApiCreds(creds: ApiKeyCreds?): Boolean { + return creds != null && + creds.apiKey.isNotBlank() && + creds.secret.isNotBlank() && + creds.passphrase.isNotBlank() + } + + /** + * 创建新的 API Key + */ + private suspend fun createApiKey( + privateKey: String, + walletAddress: String, + chainId: Long + ): Result { + return try { + // 获取服务器时间(可选,用于更准确的时间戳) + val serverTime = try { + val timeApi = createUnauthenticatedApi() + val timeResponse = timeApi.getServerTime() + if (timeResponse.isSuccessful && timeResponse.body() != null) { + timeResponse.body()!!.timestamp + } else { + null + } + } catch (e: Exception) { + logger.warn("获取服务器时间失败,使用本地时间", e) + null + } + + // 创建带 L1 认证的 API 客户端 + val api = createL1AuthenticatedApi(privateKey, walletAddress, chainId, serverTime) + + // 调用创建 API Key 接口 + val response = api.createApiKey() + + if (response.isSuccessful && response.body() != null) { + val apiKeyResponse = response.body()!! + Result.success( + ApiKeyCreds( + apiKey = apiKeyResponse.apiKey, + secret = apiKeyResponse.secret, + passphrase = apiKeyResponse.passphrase + ) + ) + } else { + val errorBody = response.errorBody()?.string() ?: "未知错误" + logger.warn("创建 API Key 失败: ${response.code()} $errorBody") + Result.failure( + IllegalStateException("创建 API Key 失败: ${response.code()} $errorBody") + ) + } + } catch (e: Exception) { + logger.error("创建 API Key 异常", e) + Result.failure(e) + } + } + + /** + * 获取现有的 API Key + */ + private suspend fun deriveApiKey( + privateKey: String, + walletAddress: String, + chainId: Long + ): Result { + return try { + // 获取服务器时间(可选) + val serverTime = try { + val timeApi = createUnauthenticatedApi() + val timeResponse = timeApi.getServerTime() + if (timeResponse.isSuccessful && timeResponse.body() != null) { + timeResponse.body()!!.timestamp + } else { + null + } + } catch (e: Exception) { + logger.warn("获取服务器时间失败,使用本地时间", e) + null + } + + // 创建带 L1 认证的 API 客户端 + val api = createL1AuthenticatedApi(privateKey, walletAddress, chainId, serverTime) + + // 调用获取 API Key 接口 + val response = api.deriveApiKey() + + if (response.isSuccessful && response.body() != null) { + val apiKeyResponse = response.body()!! + Result.success( + ApiKeyCreds( + apiKey = apiKeyResponse.apiKey, + secret = apiKeyResponse.secret, + passphrase = apiKeyResponse.passphrase + ) + ) + } else { + val errorBody = response.errorBody()?.string() ?: "未知错误" + logger.warn("获取 API Key 失败: ${response.code()} $errorBody") + Result.failure( + IllegalStateException("获取 API Key 失败: ${response.code()} $errorBody") + ) + } + } catch (e: Exception) { + logger.error("获取 API Key 异常", e) + Result.failure(e) + } + } + + /** + * 创建带 L1 认证的 API 客户端 + */ + private fun createL1AuthenticatedApi( + privateKey: String, + walletAddress: String, + chainId: Long, + serverTime: Long? = null + ): PolymarketClobApi { + val authInterceptor = PolymarketL1AuthInterceptor( + privateKey = privateKey, + walletAddress = walletAddress, + chainId = chainId, + useServerTime = serverTime != null, + serverTime = serverTime + ) + + val okHttpClient = createClient() + .addInterceptor(authInterceptor) + .build() + + return Retrofit.Builder() + .baseUrl(clobBaseUrl) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(PolymarketClobApi::class.java) + } + + /** + * 创建未认证的 API 客户端(用于获取服务器时间等公开接口) + */ + private fun createUnauthenticatedApi(): PolymarketClobApi { + val okHttpClient = createClient().build() + + return Retrofit.Builder() + .baseUrl(clobBaseUrl) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(PolymarketClobApi::class.java) + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/CryptoUtils.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/CryptoUtils.kt deleted file mode 100644 index d2a9b78..0000000 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/CryptoUtils.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.wrbug.polymarketbot.util - -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value -import org.springframework.stereotype.Component -import java.nio.charset.StandardCharsets -import java.util.* -import javax.crypto.Cipher -import javax.crypto.spec.SecretKeySpec - -/** - * 加密工具类 - * 用于加密存储私钥和 API Key - */ -@Component -class CryptoUtils { - - private val logger = LoggerFactory.getLogger(CryptoUtils::class.java) - - @Value("\${crypto.secret.key:}") - private var secretKey: String = "" - - private val algorithm = "AES" - private val transformation = "AES" - - /** - * 获取密钥字节数组 - * 使用 SHA-256 哈希从任意长度的密钥生成固定 32 字节的密钥(AES-256) - */ - private fun getKeyBytes(): ByteArray { - val rawKey = if (secretKey.isEmpty()) { - logger.warn("未配置加密密钥,使用默认密钥(仅用于开发环境)") - "default-secret-key-32-bytes-long!!" - } else { - secretKey - } - - // 将原始密钥转换为字节数组 - val keyBytes = rawKey.toByteArray(StandardCharsets.UTF_8) - - // 使用 SHA-256 哈希生成固定 32 字节的密钥(AES-256) - val messageDigest = java.security.MessageDigest.getInstance("SHA-256") - return messageDigest.digest(keyBytes) - } - - /** - * 加密字符串 - */ - fun encrypt(plainText: String): String { - return try { - val keyBytes = getKeyBytes() - val key = SecretKeySpec(keyBytes, algorithm) - val cipher = Cipher.getInstance(transformation) - cipher.init(Cipher.ENCRYPT_MODE, key) - val encrypted = cipher.doFinal(plainText.toByteArray(StandardCharsets.UTF_8)) - Base64.getEncoder().encodeToString(encrypted) - } catch (e: Exception) { - logger.error("加密失败", e) - throw RuntimeException("加密失败: ${e.message}", e) - } - } - - /** - * 解密字符串 - */ - fun decrypt(encryptedText: String): String { - return try { - val keyBytes = getKeyBytes() - val key = SecretKeySpec(keyBytes, algorithm) - val cipher = Cipher.getInstance(transformation) - cipher.init(Cipher.DECRYPT_MODE, key) - val decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedText)) - String(decrypted, StandardCharsets.UTF_8) - } catch (e: Exception) { - logger.error("解密失败", e) - throw RuntimeException("解密失败: ${e.message}", e) - } - } -} - diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/Eip712Encoder.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/Eip712Encoder.kt new file mode 100644 index 0000000..840e111 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/Eip712Encoder.kt @@ -0,0 +1,169 @@ +package com.wrbug.polymarketbot.util + +import org.bouncycastle.crypto.digests.KeccakDigest +import org.web3j.utils.Numeric +import java.math.BigInteger +import java.nio.charset.StandardCharsets + +/** + * EIP-712 编码工具类 + * 手动实现 EIP-712 编码,避免 web3j StructuredDataEncoder 的 verifyingContract 问题 + * + * 参考 EIP-712 标准:https://eips.ethereum.org/EIPS/eip-712 + */ +object Eip712Encoder { + + /** + * Keccak-256 哈希 + */ + private fun keccak256(data: ByteArray): ByteArray { + val digest = KeccakDigest(256) + digest.update(data, 0, data.size) + val hash = ByteArray(digest.digestSize) + digest.doFinal(hash, 0) + return hash + } + + /** + * 编码字符串类型 + */ + private fun encodeString(value: String): ByteArray { + val bytes = value.toByteArray(StandardCharsets.UTF_8) + return keccak256(bytes) + } + + /** + * 编码地址类型(20 字节,左对齐到 32 字节) + */ + private fun encodeAddress(address: String): ByteArray { + val cleanAddress = address.removePrefix("0x").lowercase() + val addressBytes = Numeric.hexStringToByteArray("0x$cleanAddress") + // 地址是 20 字节,需要左对齐到 32 字节 + return ByteArray(32).apply { + System.arraycopy(addressBytes, 0, this, 12, addressBytes.size) + } + } + + /** + * 编码 uint256 类型(32 字节,大端序) + */ + private fun encodeUint256(value: BigInteger): ByteArray { + val bytes = value.toByteArray() + val result = ByteArray(32) + if (bytes.size <= 32) { + // 左对齐 + System.arraycopy(bytes, 0, result, 32 - bytes.size, bytes.size) + } else { + // 如果超过 32 字节,取最后 32 字节 + System.arraycopy(bytes, bytes.size - 32, result, 0, 32) + } + return result + } + + /** + * 编码类型哈希(Type Hash) + * 例如:encodeType("EIP712Domain", listOf("name", "version", "chainId")) + */ + private fun encodeType(typeName: String, fields: List>): ByteArray { + val typeString = buildString { + append(typeName) + append("(") + fields.forEachIndexed { index, (name, type) -> + if (index > 0) append(",") + append(type) + append(" ") + append(name) + } + append(")") + } + return keccak256(typeString.toByteArray(StandardCharsets.UTF_8)) + } + + /** + * 编码域分隔符(Domain Separator) + */ + fun encodeDomain( + name: String, + version: String, + chainId: Long + ): ByteArray { + // EIP712Domain 类型定义(不包含 verifyingContract) + val domainTypeHash = encodeType( + "EIP712Domain", + listOf( + "name" to "string", + "version" to "string", + "chainId" to "uint256" + ) + ) + + // 编码域字段 + val nameHash = encodeString(name) + val versionHash = encodeString(version) + val chainIdBytes = encodeUint256(BigInteger.valueOf(chainId)) + + // 组合:keccak256(domainTypeHash || nameHash || versionHash || chainIdBytes) + val encoded = ByteArray(32 + 32 + 32 + 32) + System.arraycopy(domainTypeHash, 0, encoded, 0, 32) + System.arraycopy(nameHash, 0, encoded, 32, 32) + System.arraycopy(versionHash, 0, encoded, 64, 32) + System.arraycopy(chainIdBytes, 0, encoded, 96, 32) + + return keccak256(encoded) + } + + /** + * 编码消息哈希(Message Hash) + */ + fun encodeMessage( + address: String, + timestamp: String, + nonce: BigInteger, + message: String + ): ByteArray { + // ClobAuth 类型定义 + val clobAuthTypeHash = encodeType( + "ClobAuth", + listOf( + "address" to "address", + "timestamp" to "string", + "nonce" to "uint256", + "message" to "string" + ) + ) + + // 编码消息字段 + val addressBytes = encodeAddress(address) + val timestampHash = encodeString(timestamp) + val nonceBytes = encodeUint256(nonce) + val messageHash = encodeString(message) + + // 组合:keccak256(clobAuthTypeHash || addressBytes || timestampHash || nonceBytes || messageHash) + val encoded = ByteArray(32 + 32 + 32 + 32 + 32) + System.arraycopy(clobAuthTypeHash, 0, encoded, 0, 32) + System.arraycopy(addressBytes, 0, encoded, 32, 32) + System.arraycopy(timestampHash, 0, encoded, 64, 32) + System.arraycopy(nonceBytes, 0, encoded, 96, 32) + System.arraycopy(messageHash, 0, encoded, 128, 32) + + return keccak256(encoded) + } + + /** + * 计算完整的结构化数据哈希 + * hash = keccak256("\x19\x01" || domainSeparator || messageHash) + */ + fun hashStructuredData( + domainSeparator: ByteArray, + messageHash: ByteArray + ): ByteArray { + val prefix = byteArrayOf(0x19.toByte(), 0x01.toByte()) + val encoded = ByteArray(prefix.size + domainSeparator.size + messageHash.size) + System.arraycopy(prefix, 0, encoded, 0, prefix.size) + System.arraycopy(domainSeparator, 0, encoded, prefix.size, domainSeparator.size) + System.arraycopy(messageHash, 0, encoded, prefix.size + domainSeparator.size, messageHash.size) + + return keccak256(encoded) + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/Eip712Signer.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/Eip712Signer.kt new file mode 100644 index 0000000..81b3097 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/Eip712Signer.kt @@ -0,0 +1,94 @@ +package com.wrbug.polymarketbot.util + +import org.slf4j.LoggerFactory +import org.web3j.crypto.ECKeyPair +import org.web3j.crypto.Sign +import org.web3j.utils.Numeric +import java.math.BigInteger + +/** + * EIP-712 签名工具类 + * 用于创建 Polymarket CLOB API 的 L1 认证签名 + * + * 手动实现 EIP-712 编码,避免 web3j StructuredDataEncoder 的 verifyingContract 问题 + * 参考 clob-client/src/signing/eip712.ts 实现 + */ +object Eip712Signer { + + private val logger = LoggerFactory.getLogger(Eip712Signer::class.java) + + /** + * ClobAuthDomain 的 EIP-712 域定义 + */ + private const val DOMAIN_NAME = "ClobAuthDomain" + private const val DOMAIN_VERSION = "1" + private const val MESSAGE_TO_SIGN = "This message attests that I control the given wallet" // 根据 clob-client 实现 + + /** + * 构建 ClobAuth EIP-712 签名 + * + * @param privateKey 私钥(十六进制字符串,带或不带 0x 前缀) + * @param chainId 链 ID(Polygon 主网是 137) + * @param timestamp 时间戳(秒) + * @param nonce 随机数(默认 0) + * @return EIP-712 签名字符串 + */ + fun buildClobEip712Signature( + privateKey: String, + chainId: Long, + timestamp: Long, + nonce: Long = 0 + ): String { + try { + // 从私钥创建 BigInteger + val cleanPrivateKey = privateKey.removePrefix("0x") + val privateKeyBigInt = BigInteger(cleanPrivateKey, 16) + + // 从私钥推导地址(用于消息中的 address 字段) + val credentials = org.web3j.crypto.Credentials.create(privateKeyBigInt.toString(16)) + val address = credentials.address + + // 使用手动实现的 EIP-712 编码器 + // 1. 编码域分隔符 + val domainSeparator = Eip712Encoder.encodeDomain( + name = DOMAIN_NAME, + version = DOMAIN_VERSION, + chainId = chainId + ) + + // 2. 编码消息哈希 + val messageHash = Eip712Encoder.encodeMessage( + address = address, + timestamp = timestamp.toString(), + nonce = BigInteger.valueOf(nonce), + message = MESSAGE_TO_SIGN + ) + + // 3. 计算完整的结构化数据哈希 + val structuredHash = Eip712Encoder.hashStructuredData(domainSeparator, messageHash) + + // 4. 使用私钥签名 + val ecKeyPair = ECKeyPair.create(privateKeyBigInt) + val signature = Sign.signMessage(structuredHash, ecKeyPair, false) + + // 5. 组合签名(r + s + v) + // 在 web3j 5.0 中,signature.r 和 signature.s 是 BigInteger 类型 + // signature.v 在 web3j 5.0 中仍然是 ByteArray 类型 + val rHex = Numeric.toHexString(signature.r).removePrefix("0x").padStart(64, '0') + val sHex = Numeric.toHexString(signature.s).removePrefix("0x").padStart(64, '0') + val vBytes = signature.v as ByteArray + val vInt = if (vBytes.isNotEmpty()) { + vBytes[0].toInt() and 0xff + } else { + 0 + } + val vHex = String.format("%02x", vInt) + + return "0x$rHex$sHex$vHex" + } catch (e: Exception) { + logger.error("EIP-712 签名失败", e) + throw RuntimeException("EIP-712 签名失败: ${e.message}", e) + } + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketAuthInterceptor.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketAuthInterceptor.kt index bbab088..1a08280 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketAuthInterceptor.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketAuthInterceptor.kt @@ -1,9 +1,12 @@ package com.wrbug.polymarketbot.util import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Request +import okhttp3.RequestBody import okhttp3.Response import okio.Buffer +import org.slf4j.LoggerFactory import java.io.IOException import java.time.Instant import javax.crypto.Mac @@ -14,64 +17,150 @@ import java.util.Base64 * Polymarket API 认证拦截器 * 实现 L2 认证(使用 API Key、Secret、Passphrase) * + * 参考 clob-client 实现: + * - 请求头使用 POLY_* 前缀 + * - Secret 需要 base64 解码后用于 HMAC + * - 签名结果需要 URL-safe base64 编码(+ -> -, / -> _) + * * 认证方式: * 1. 生成时间戳(秒) - * 2. 使用 Secret 对 (timestamp + method + path + body) 进行 HMAC-SHA256 签名 + * 2. 使用 Secret(base64 解码后)对 (timestamp + method + requestPath + body) 进行 HMAC-SHA256 签名 * 3. 在请求头中添加: - * - X-API-KEY: API Key - * - X-API-SIGN: Base64 编码的签名 - * - X-API-TIMESTAMP: 时间戳 - * - X-API-PASSPHRASE: Passphrase + * - POLY_ADDRESS: 钱包地址 + * - POLY_SIGNATURE: URL-safe Base64 编码的签名 + * - POLY_TIMESTAMP: 时间戳(字符串) + * - POLY_API_KEY: API Key + * - POLY_PASSPHRASE: Passphrase */ class PolymarketAuthInterceptor( private val apiKey: String, private val apiSecret: String, - private val apiPassphrase: String + private val apiPassphrase: String, + private val walletAddress: String ) : Interceptor { + private val logger = LoggerFactory.getLogger(PolymarketAuthInterceptor::class.java) + @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() // 生成时间戳(秒) - val timestamp = Instant.now().epochSecond.toString() + val timestamp = Instant.now().epochSecond - // 构建签名字符串: timestamp + method + path + body - val method = originalRequest.method - val path = originalRequest.url.encodedPath + if (originalRequest.url.query != null) "?${originalRequest.url.query}" else "" + // 构建签名字符串: timestamp + method + requestPath + body + // requestPath 不包含 query string(根据 clob-client 实现) + val method = originalRequest.method.uppercase() + val requestPath = originalRequest.url.encodedPath // 读取请求体(如果存在) - val body = originalRequest.body?.let { requestBody -> + // 注意:读取后需要重新创建请求体,否则原始请求体会被消费 + val bodyString = originalRequest.body?.let { requestBody -> val buffer = Buffer() requestBody.writeTo(buffer) buffer.readUtf8() - } ?: "" + } - val signString = "$timestamp$method$path$body" + // 构建签名字符串(与 clob-client 保持一致) + // clob-client: timestamp + method + requestPath + (body !== undefined ? body : "") + // 注意:如果 body 是空字符串 "",也会添加到签名字符串中(虽然结果相同) + val signString = if (bodyString != null) { + "$timestamp$method$requestPath$bodyString" + } else { + "$timestamp$method$requestPath" + } - // 使用 HMAC-SHA256 生成签名 + // 使用 HMAC-SHA256 生成签名(Secret 需要 base64 解码) val signature = generateSignature(signString, apiSecret) - // 构建新的请求,添加认证头 - val newRequest = originalRequest.newBuilder() - .header("X-API-KEY", apiKey) - .header("X-API-SIGN", signature) - .header("X-API-TIMESTAMP", timestamp) - .header("X-API-PASSPHRASE", apiPassphrase) - .build() + // 调试日志(仅在 DEBUG 级别输出) + logger.debug("L2 认证签名生成: method=$method, path=$requestPath, bodyLength=${bodyString?.length ?: 0}, timestamp=$timestamp") + logger.debug("签名字符串: $signString") + logger.debug("签名结果: ${signature.take(20)}...") - return chain.proceed(newRequest) + // 重新创建请求体(如果原始请求有请求体) + val newRequestBody = originalRequest.body?.let { requestBody -> + val contentType = requestBody.contentType() + RequestBody.create( + contentType, + bodyString?.toByteArray() ?: ByteArray(0) + ) + } + + // 构建新的请求,添加认证头(使用 POLY_* 前缀) + // 参考 clob-client/src/http-helpers/index.ts 的 overloadHeaders 函数 + // 添加标准 HTTP 请求头以匹配 clob-client 的行为 + val newRequestBuilder = originalRequest.newBuilder() + .header("POLY_ADDRESS", walletAddress) + .header("POLY_SIGNATURE", signature) + .header("POLY_TIMESTAMP", timestamp.toString()) + .header("POLY_API_KEY", apiKey) + .header("POLY_PASSPHRASE", apiPassphrase) + .header("User-Agent", "@polymarket/clob-client") + .header("Accept", "*/*") + .header("Connection", "keep-alive") + .header("Content-Type", "application/json") + .apply { + // GET 请求添加 Accept-Encoding: gzip + if (method == "GET") { + header("Accept-Encoding", "gzip") + } + } + + // 如果有请求体,重新设置请求体 + if (newRequestBody != null) { + newRequestBuilder.method(originalRequest.method, newRequestBody) + } + + return chain.proceed(newRequestBuilder.build()) } /** * 使用 HMAC-SHA256 生成签名 + * 参考 clob-client/src/signing/hmac.ts 实现 + * + * 注意:Node.js 的 Buffer.from(secret, "base64") 可以处理 URL-safe base64(包含 - 和 _) + * Java 提供了 Base64.getUrlDecoder() 来直接处理 URL-safe base64 + * + * 1. Secret 需要 base64 解码(支持标准 base64 和 URL-safe base64) + * 2. 使用解码后的 secret 进行 HMAC-SHA256 + * 3. 结果进行 URL-safe base64 编码(+ -> -, / -> _) */ private fun generateSignature(message: String, secret: String): String { + // Secret 可能是标准 base64 或 URL-safe base64 + // 优先尝试标准 base64,如果失败则使用 URL-safe 解码器 + val decodedSecret = try { + // 先尝试标准 base64 解码 + Base64.getDecoder().decode(secret) + } catch (e: Exception) { + // 如果失败,可能是 URL-safe base64,使用 URL 解码器 + // Base64.getUrlDecoder() 可以直接处理 URL-safe base64(- 和 _) + try { + Base64.getUrlDecoder().decode(secret) + } catch (e2: Exception) { + // 如果都失败,尝试转换为标准格式后再解码(向后兼容) + val standardBase64 = secret.replace("-", "+").replace("_", "/") + try { + Base64.getDecoder().decode(standardBase64) + } catch (e3: Exception) { + // 最后尝试直接使用原始字符串(向后兼容) + secret.toByteArray() + } + } + } + val mac = Mac.getInstance("HmacSHA256") - val secretKeySpec = SecretKeySpec(secret.toByteArray(), "HmacSHA256") + val secretKeySpec = SecretKeySpec(decodedSecret, "HmacSHA256") mac.init(secretKeySpec) val hash = mac.doFinal(message.toByteArray()) - return Base64.getEncoder().encodeToString(hash) + + // Base64 编码 + val base64Signature = Base64.getEncoder().encodeToString(hash) + + // URL-safe base64 编码:将 + 替换为 -,将 / 替换为 _ + // 注意:保留 = 后缀(根据 clob-client 注释) + // 使用 replaceAll 替换所有匹配项(与 clob-client 的 replaceAll 函数一致) + return base64Signature.replace("+", "-").replace("/", "_") } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketL1AuthInterceptor.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketL1AuthInterceptor.kt new file mode 100644 index 0000000..a53337d --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketL1AuthInterceptor.kt @@ -0,0 +1,69 @@ +package com.wrbug.polymarketbot.util + +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import org.slf4j.LoggerFactory +import java.io.IOException + +/** + * Polymarket API L1 认证拦截器 + * 用于创建/获取 API Key 时的认证 + * + * 参考 clob-client/src/headers/index.ts 的 createL1Headers 实现 + * + * 认证方式: + * 1. 使用 EIP-712 签名对请求进行签名 + * 2. 在请求头中添加: + * - POLY_ADDRESS: 钱包地址 + * - POLY_SIGNATURE: EIP-712 签名字符串 + * - POLY_TIMESTAMP: 时间戳(秒,字符串) + * - POLY_NONCE: 随机数(字符串,默认 0) + */ +class PolymarketL1AuthInterceptor( + private val privateKey: String, + private val walletAddress: String, + private val chainId: Long = 137L, // Polygon 主网 + private val nonce: Long = 0L, + private val useServerTime: Boolean = false, + private val serverTime: Long? = null +) : Interceptor { + + private val logger = LoggerFactory.getLogger(PolymarketL1AuthInterceptor::class.java) + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + // 获取时间戳(优先使用服务器时间,否则使用当前时间) + val timestamp = if (useServerTime && serverTime != null) { + serverTime + } else { + System.currentTimeMillis() / 1000 + } + + // 生成 EIP-712 签名 + val signature = try { + Eip712Signer.buildClobEip712Signature( + privateKey = privateKey, + chainId = chainId, + timestamp = timestamp, + nonce = nonce + ) + } catch (e: Exception) { + logger.error("生成 EIP-712 签名失败", e) + throw IOException("生成签名失败: ${e.message}", e) + } + + // 构建新的请求,添加 L1 认证头 + val newRequest = originalRequest.newBuilder() + .header("POLY_ADDRESS", walletAddress) + .header("POLY_SIGNATURE", signature) + .header("POLY_TIMESTAMP", timestamp.toString()) + .header("POLY_NONCE", nonce.toString()) + .build() + + return chain.proceed(newRequest) + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt index 002ca56..650447c 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt @@ -22,14 +22,16 @@ class RetrofitFactory( * @param apiKey API Key * @param apiSecret API Secret * @param apiPassphrase API Passphrase + * @param walletAddress 钱包地址(用于 POLY_ADDRESS 请求头) * @return PolymarketClobApi 客户端 */ fun createClobApi( apiKey: String, apiSecret: String, - apiPassphrase: String + apiPassphrase: String, + walletAddress: String ): PolymarketClobApi { - val authInterceptor = PolymarketAuthInterceptor(apiKey, apiSecret, apiPassphrase) + val authInterceptor = PolymarketAuthInterceptor(apiKey, apiSecret, apiPassphrase, walletAddress) val okHttpClient = createClient() .addInterceptor(authInterceptor) diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index e86f46c..5f4c263 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -40,6 +40,3 @@ polymarket.data-api.base-url=https://data-api.polymarket.com # 示例:https://polygon-rpc.com 或 https://polygon-mainnet.infura.io/v3/YOUR_PROJECT_ID ethereum.rpc.url=${ETHEREUM_RPC_URL:https://polygon-rpc.com} -# 加密配置 -crypto.secret.key=${CRYPTO_SECRET_KEY:wrbug123} - diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1c7629d..356e667 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import Layout from './components/Layout' import AccountList from './pages/AccountList' import AccountImport from './pages/AccountImport' import AccountDetail from './pages/AccountDetail' +import AccountEdit from './pages/AccountEdit' import LeaderList from './pages/LeaderList' import LeaderAdd from './pages/LeaderAdd' import ConfigPage from './pages/ConfigPage' @@ -21,6 +22,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 4e59696..6d60f55 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { Layout as AntLayout, Menu, Drawer, Button } from 'antd' import { useMediaQuery } from 'react-responsive' diff --git a/frontend/src/pages/AccountDetail.tsx b/frontend/src/pages/AccountDetail.tsx index e34c904..97a6069 100644 --- a/frontend/src/pages/AccountDetail.tsx +++ b/frontend/src/pages/AccountDetail.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' -import { Card, Descriptions, Button, Space, Tag, Spin, message, Typography, Divider } from 'antd' +import { Card, Descriptions, Button, Space, Tag, Spin, message, Typography, Divider, Modal, Form, Input, Checkbox, Alert } from 'antd' import { ArrowLeftOutlined, ReloadOutlined, EditOutlined } from '@ant-design/icons' import { useAccountStore } from '../store/accountStore' import type { Account } from '../types' @@ -14,11 +14,14 @@ const AccountDetail: React.FC = () => { const isMobile = useMediaQuery({ maxWidth: 768 }) const accountId = searchParams.get('id') - const { fetchAccountDetail, fetchAccountBalance } = useAccountStore() + const { fetchAccountDetail, fetchAccountBalance, updateAccount } = useAccountStore() const [account, setAccount] = useState(null) const [balance, setBalance] = useState(null) const [loading, setLoading] = useState(true) const [balanceLoading, setBalanceLoading] = useState(false) + const [editModalVisible, setEditModalVisible] = useState(false) + const [editForm] = Form.useForm() + const [editLoading, setEditLoading] = useState(false) useEffect(() => { if (accountId) { @@ -51,7 +54,7 @@ const AccountDetail: React.FC = () => { setBalanceLoading(true) try { const balanceData = await fetchAccountBalance(Number(accountId)) - setBalance(balanceData.balance || null) + setBalance(balanceData.totalBalance || null) } catch (error: any) { console.error('获取余额失败:', error) // 余额查询失败不显示错误,只显示 "-" @@ -61,6 +64,46 @@ const AccountDetail: React.FC = () => { } } + const handleEditSubmit = async (values: any) => { + if (!account) return + + setEditLoading(true) + try { + // 构建更新请求,空字符串转换为 undefined(不修改) + const updateData: any = { + accountId: account.id, + accountName: values.accountName || undefined, + isDefault: values.isDefault || false + } + + // 只有非空字符串才更新 API 凭证 + if (values.apiKey && values.apiKey.trim()) { + updateData.apiKey = values.apiKey.trim() + } + if (values.apiSecret && values.apiSecret.trim()) { + updateData.apiSecret = values.apiSecret.trim() + } + if (values.apiPassphrase && values.apiPassphrase.trim()) { + updateData.apiPassphrase = values.apiPassphrase.trim() + } + + await updateAccount(updateData) + + message.success('更新账户成功') + setEditModalVisible(false) + editForm.resetFields() + + // 刷新账户详情 + if (accountId) { + await loadAccountDetail() + } + } catch (error: any) { + message.error(error.message || '更新账户失败') + } finally { + setEditLoading(false) + } + } + if (loading) { return (
@@ -113,7 +156,16 @@ const AccountDetail: React.FC = () => { + + + + + ) : ( +
+ +
加载中...
+
+ )} +
) } diff --git a/frontend/src/pages/AccountEdit.tsx b/frontend/src/pages/AccountEdit.tsx new file mode 100644 index 0000000..390e455 --- /dev/null +++ b/frontend/src/pages/AccountEdit.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' +import { Card, Form, Input, Button, message, Typography, Space, Alert, Checkbox } from 'antd' +import { ArrowLeftOutlined } from '@ant-design/icons' +import { useAccountStore } from '../store/accountStore' +import { useMediaQuery } from 'react-responsive' + +const { Title } = Typography + +const AccountEdit: React.FC = () => { + const navigate = useNavigate() + const [searchParams] = useSearchParams() + const isMobile = useMediaQuery({ maxWidth: 768 }) + const accountId = searchParams.get('id') + + const { fetchAccountDetail, updateAccount, loading } = useAccountStore() + const [form] = Form.useForm() + const [account, setAccount] = useState(null) + const [loadingDetail, setLoadingDetail] = useState(true) + + useEffect(() => { + if (accountId) { + loadAccountDetail() + } else { + message.error('账户ID不能为空') + navigate('/accounts') + } + }, [accountId]) + + const loadAccountDetail = async () => { + if (!accountId) return + + setLoadingDetail(true) + try { + const accountData = await fetchAccountDetail(Number(accountId)) + setAccount(accountData) + + // 设置表单初始值 + form.setFieldsValue({ + accountName: accountData.accountName || '', + isDefault: accountData.isDefault || false + }) + } catch (error: any) { + message.error(error.message || '获取账户详情失败') + navigate('/accounts') + } finally { + setLoadingDetail(false) + } + } + + const handleSubmit = async (values: any) => { + if (!accountId) return + + try { + // 构建更新请求 + const updateData: any = { + accountId: Number(accountId), + accountName: values.accountName || undefined, + isDefault: values.isDefault || false + } + + await updateAccount(updateData) + + message.success('更新账户成功') + navigate(`/accounts/detail?id=${accountId}`) + } catch (error: any) { + message.error(error.message || '更新账户失败') + } + } + + if (loadingDetail) { + return ( +
+
加载中...
+
+ ) + } + + if (!account) { + return null + } + + return ( +
+
+ + + 编辑账户 + +
+ + +
+ + + + + + + + 设为默认账户 + + + + + + + + + +
+
+ ) +} + +export default AccountEdit + diff --git a/frontend/src/pages/AccountImport.tsx b/frontend/src/pages/AccountImport.tsx index bdca0d3..d092946 100644 --- a/frontend/src/pages/AccountImport.tsx +++ b/frontend/src/pages/AccountImport.tsx @@ -13,7 +13,7 @@ import { } from '../utils/ethers' import { useMediaQuery } from 'react-responsive' -const { Title, Text } = Typography +const { Title } = Typography type ImportType = 'privateKey' | 'mnemonic' @@ -27,7 +27,7 @@ const AccountImport: React.FC = () => { const [addressError, setAddressError] = useState('') // 当私钥输入时,自动推导地址 - const handlePrivateKeyChange = (e: React.ChangeEvent) => { + const handlePrivateKeyChange = (e: React.ChangeEvent) => { const privateKey = e.target.value.trim() if (!privateKey) { setDerivedAddress('') @@ -56,7 +56,7 @@ const AccountImport: React.FC = () => { } // 当助记词输入时,自动推导地址 - const handleMnemonicChange = (e: React.ChangeEvent) => { + const handleMnemonicChange = (e: React.ChangeEvent) => { const mnemonic = e.target.value.trim() if (!mnemonic) { setDerivedAddress('') @@ -136,9 +136,6 @@ const AccountImport: React.FC = () => { privateKey: privateKey, walletAddress: walletAddress, accountName: values.accountName, - apiKey: values.apiKey, - apiSecret: values.apiSecret, - apiPassphrase: values.apiPassphrase, isDefault: values.isDefault || false }) @@ -165,7 +162,7 @@ const AccountImport: React.FC = () => { { - - - - - - - - - - - + { const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) - const { accounts, loading, fetchAccounts, deleteAccount, setDefaultAccount, fetchAccountBalance, fetchAccountDetail } = useAccountStore() + const { accounts, loading, fetchAccounts, deleteAccount, setDefaultAccount, fetchAccountBalance, fetchAccountDetail, updateAccount } = useAccountStore() const [balanceMap, setBalanceMap] = useState>({}) const [balanceLoading, setBalanceLoading] = useState>({}) const [detailModalVisible, setDetailModalVisible] = useState(false) const [detailAccount, setDetailAccount] = useState(null) const [detailBalance, setDetailBalance] = useState<{ total: string; available: string; position: string; positions: any[] } | null>(null) const [detailBalanceLoading, setDetailBalanceLoading] = useState(false) + const [editModalVisible, setEditModalVisible] = useState(false) + const [editAccount, setEditAccount] = useState(null) + const [editForm] = Form.useForm() + const [editLoading, setEditLoading] = useState(false) useEffect(() => { fetchAccounts() @@ -137,6 +141,75 @@ const AccountList: React.FC = () => { } } + const handleShowEdit = async (account: Account) => { + try { + setEditModalVisible(true) + setEditAccount(account) + + // 加载账户详情并设置表单初始值 + const accountDetail = await fetchAccountDetail(account.id) + setEditAccount(accountDetail) + + editForm.setFieldsValue({ + accountName: accountDetail.accountName || '', + apiKey: '', // 不显示实际值,留空表示不修改 + apiSecret: '', // 不显示实际值,留空表示不修改 + apiPassphrase: '', // 不显示实际值,留空表示不修改 + isDefault: accountDetail.isDefault || false + }) + } catch (error: any) { + console.error('打开编辑失败:', error) + message.error(error.message || '获取账户详情失败') + setEditModalVisible(false) + setEditAccount(null) + } + } + + const handleEditSubmit = async (values: any) => { + if (!editAccount) return + + setEditLoading(true) + try { + // 构建更新请求,空字符串转换为 undefined(不修改) + const updateData: any = { + accountId: editAccount.id, + accountName: values.accountName || undefined, + isDefault: values.isDefault || false + } + + // 只有非空字符串才更新 API 凭证 + if (values.apiKey && values.apiKey.trim()) { + updateData.apiKey = values.apiKey.trim() + } + if (values.apiSecret && values.apiSecret.trim()) { + updateData.apiSecret = values.apiSecret.trim() + } + if (values.apiPassphrase && values.apiPassphrase.trim()) { + updateData.apiPassphrase = values.apiPassphrase.trim() + } + + await updateAccount(updateData) + + message.success('更新账户成功') + setEditModalVisible(false) + setEditAccount(null) + editForm.resetFields() + + // 刷新账户列表 + await fetchAccounts() + + // 如果详情 Modal 打开着,也刷新详情 + if (detailModalVisible && detailAccount && detailAccount.id === editAccount.id) { + const accountDetail = await fetchAccountDetail(editAccount.id) + setDetailAccount(accountDetail) + } + } catch (error: any) { + message.error(error.message || '更新账户失败') + } finally { + setEditLoading(false) + } + } + const columns = [ { title: '账户名称', @@ -191,6 +264,17 @@ const AccountList: React.FC = () => { return balance && balance !== '-' && typeof balance === 'string' ? `${balance} USDC` : '-' } }, + { + title: '活跃订单', + dataIndex: 'activeOrders', + key: 'activeOrders', + render: (_: any, record: Account) => { + if (record.activeOrders !== undefined && record.activeOrders !== null) { + return 0 ? 'orange' : 'default'}>{record.activeOrders} + } + return - + } + }, { title: '操作', key: 'action', @@ -203,6 +287,14 @@ const AccountList: React.FC = () => { > 详情 + { 可用: {balanceMap[record.id].available} USDC | 仓位: {balanceMap[record.id].position} USDC )} + {(record.activeOrders !== undefined && record.activeOrders !== null) && ( +
+ 活跃订单: 0 ? 'orange' : 'default'} style={{ margin: 0 }}>{record.activeOrders} +
+ )} ) } @@ -300,6 +404,15 @@ const AccountList: React.FC = () => { > 查看详情 + {!record.isDefault && ( , + , + + +
+ + ) : ( +
+ +
加载中...
+
+ )} + ) } diff --git a/frontend/src/pages/ConfigPage.tsx b/frontend/src/pages/ConfigPage.tsx index 6cb49e2..c6c7226 100644 --- a/frontend/src/pages/ConfigPage.tsx +++ b/frontend/src/pages/ConfigPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Card, Form, Input, Button, Switch, Radio, InputNumber, message, Typography, Space } from 'antd' +import { Card, Form, Button, Switch, Radio, InputNumber, message, Typography } from 'antd' import { SaveOutlined } from '@ant-design/icons' import { apiService } from '../services/api' import type { CopyTradingConfig } from '../types' @@ -11,7 +11,7 @@ const ConfigPage: React.FC = () => { const isMobile = useMediaQuery({ maxWidth: 768 }) const [form] = Form.useForm() const [loading, setLoading] = useState(false) - const [config, setConfig] = useState(null) + const [, setConfig] = useState(null) useEffect(() => { fetchConfig() diff --git a/frontend/src/pages/LeaderAdd.tsx b/frontend/src/pages/LeaderAdd.tsx index 7a0e44b..dcca342 100644 --- a/frontend/src/pages/LeaderAdd.tsx +++ b/frontend/src/pages/LeaderAdd.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' -import { Card, Form, Input, Button, Select, Switch, message, Typography } from 'antd' +import { Card, Form, Input, Button, Select, Switch, message, Typography, Space } from 'antd' import { ArrowLeftOutlined } from '@ant-design/icons' import { apiService } from '../services/api' import { useAccountStore } from '../store/accountStore' diff --git a/frontend/src/pages/OrderList.tsx b/frontend/src/pages/OrderList.tsx index 3e8dc4c..87f3fcc 100644 --- a/frontend/src/pages/OrderList.tsx +++ b/frontend/src/pages/OrderList.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Card, Table, Tag, Space, message } from 'antd' +import { Card, Table, Tag, message } from 'antd' import { apiService } from '../services/api' import type { CopyOrder } from '../types' import { useMediaQuery } from 'react-responsive' diff --git a/frontend/src/pages/Statistics.tsx b/frontend/src/pages/Statistics.tsx index 1feca21..321fe08 100644 --- a/frontend/src/pages/Statistics.tsx +++ b/frontend/src/pages/Statistics.tsx @@ -3,10 +3,8 @@ import { Card, Row, Col, Statistic, message } from 'antd' import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons' import { apiService } from '../services/api' import type { Statistics as StatisticsType } from '../types' -import { useMediaQuery } from 'react-responsive' const Statistics: React.FC = () => { - const isMobile = useMediaQuery({ maxWidth: 768 }) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(false) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index dabb9aa..2ae1546 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -21,6 +21,9 @@ export interface Account { balance?: string totalOrders?: number totalPnl?: string + activeOrders?: number + completedOrders?: number + positionCount?: number } /** @@ -38,9 +41,6 @@ export interface AccountImportRequest { privateKey: string walletAddress: string accountName?: string - apiKey?: string - apiSecret?: string - apiPassphrase?: string isDefault?: boolean } @@ -50,9 +50,6 @@ export interface AccountImportRequest { export interface AccountUpdateRequest { accountId: number accountName?: string - apiKey?: string - apiSecret?: string - apiPassphrase?: string isDefault?: boolean } diff --git a/polymarket-demo/.gitignore b/polymarket-demo/.gitignore new file mode 100644 index 0000000..2218edb --- /dev/null +++ b/polymarket-demo/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.log +.env +.DS_Store + diff --git a/polymarket-demo/README.md b/polymarket-demo/README.md new file mode 100644 index 0000000..a1cfb2f --- /dev/null +++ b/polymarket-demo/README.md @@ -0,0 +1,65 @@ +# Polymarket JS SDK Demo + +这是一个用于测试 Polymarket JS SDK 的演示目录。 + +## 安装依赖 + +```bash +npm install +``` + +或者使用 yarn: + +```bash +yarn install +``` + +## 配置 + +在使用脚本之前,需要修改 `src/createOrder.ts` 文件中的以下配置: + +1. **funder**: 你的 Polymarket 账户地址(在个人资料图片下方显示的地址) +2. **signer**: 你的私钥 + - 如果使用邮箱登录,从 https://reveal.magic.link/polymarket 导出 + - 如果使用 Web3 应用,从你的钱包导出 +3. **tokenID**: 要交易的代币 ID + - 使用 https://docs.polymarket.com/developers/gamma-markets-api/get-markets 获取示例代币 + - 示例代币: `114304586861386186441621124384163963092522056897081085884483958561365015034812` (Xi Jinping out in 2025, YES side) +4. **tickSize** 和 **negRisk**: 根据市场调整这些参数,从 get-markets API 获取 + +## 运行测试脚本 + +```bash +npm run test +``` + +或者直接使用 ts-node: + +```bash +npx ts-node src/createOrder.ts +``` + +## 编译 + +```bash +npm run build +``` + +编译后的文件将输出到 `dist` 目录。 + +## 运行编译后的文件 + +```bash +npm start +``` + +## 注意事项 + +- **不要创建新的 API key**,始终使用 `createOrDerive` 方法 +- **signatureType** 说明: + - `1`: Magic/Email 登录 + - `2`: 浏览器钱包 (Metamask, Coinbase Wallet 等) + - `0`: EOA (如果你不知道这是什么,说明你不在使用它) +- 确保你的账户有足够的 USDC 余额 +- 在生产环境中使用前,请确保妥善保管私钥 + diff --git a/polymarket-demo/package-lock.json b/polymarket-demo/package-lock.json new file mode 100644 index 0000000..73d7938 --- /dev/null +++ b/polymarket-demo/package-lock.json @@ -0,0 +1,1509 @@ +{ + "name": "polymarket-demo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "polymarket-demo", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@polymarket/clob-client": "^4.22.8", + "ethers": "^5.7.1" + }, + "devDependencies": { + "@types/node": "^18.7.18", + "ts-node": "^10.9.1", + "typescript": "^4.8.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", + "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.8.0.tgz", + "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bignumber/node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==" + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.8.0.tgz", + "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.8.0.tgz", + "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz", + "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ] + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz", + "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/sha2": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz", + "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0", + "bech32": "1.1.4", + "ws": "8.18.0" + } + }, + "node_modules/@ethersproject/random": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz", + "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key/node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==" + }, + "node_modules/@ethersproject/solidity": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz", + "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.8.0.tgz", + "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.8.0.tgz", + "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/json-wallets": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.8.0.tgz", + "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@metamask/eth-sig-util": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-5.1.0.tgz", + "integrity": "sha512-mlgziIHYlA9pi/XZerChqg4NocdOgBPB9NmxgXWQO2U2hH8RGOJQrz6j/AIKkYxgCMIE2PY000+joOwXfzeTDQ==", + "dependencies": { + "@ethereumjs/util": "^8.0.6", + "bn.js": "^4.12.0", + "ethereum-cryptography": "^2.0.0", + "ethjs-util": "^0.1.6", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polymarket/builder-signing-sdk": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@polymarket/builder-signing-sdk/-/builder-signing-sdk-0.0.8.tgz", + "integrity": "sha512-rZLCFxEdYahl5FiJmhe22RDXysS1ibFJlWz4NT0s3itJRYq3XJzXXHXEZkAQplU+nIS1IlbbKjA4zDQaeCyYtg==", + "dependencies": { + "@types/node": "^18.7.18", + "axios": "^1.12.2", + "tslib": "^2.8.1" + } + }, + "node_modules/@polymarket/builder-signing-sdk/node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@polymarket/clob-client": { + "version": "4.22.8", + "resolved": "https://registry.npmjs.org/@polymarket/clob-client/-/clob-client-4.22.8.tgz", + "integrity": "sha512-kwiOeTrZ4pVBaAlxi78rR7gXlvofFohlgSQAXslx7xsfMnwaDAjp3zr7+co+Kf+YGrBbU9ztEVM/FxMSm38jxA==", + "dependencies": { + "@polymarket/builder-signing-sdk": "^0.0.8", + "@polymarket/order-utils": "^2.1.0", + "axios": "^0.27.2", + "browser-or-node": "^2.1.1", + "ethers": "^5.7.1" + } + }, + "node_modules/@polymarket/order-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@polymarket/order-utils/-/order-utils-2.1.0.tgz", + "integrity": "sha512-vUFj1WiEtm4vB0dxx2NweKKioPu7ehBoJ8fTTaVbR8Dur6AazVWyS+KhgrDE66fKTNneOxxgfdEpbNgxZ4pOIA==", + "dependencies": { + "@metamask/eth-sig-util": "^5.0.0", + "ethers": "^5.7.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz", + "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } + }, + "node_modules/ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "dependencies": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==", + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/micro-ftch": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", + "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" + }, + "node_modules/strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==", + "dependencies": { + "is-hex-prefixed": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "node_modules/tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/polymarket-demo/package.json b/polymarket-demo/package.json new file mode 100644 index 0000000..b064a0a --- /dev/null +++ b/polymarket-demo/package.json @@ -0,0 +1,28 @@ +{ + "name": "polymarket-demo", + "version": "1.0.0", + "description": "Polymarket JS SDK 测试脚本", + "main": "index.js", + "scripts": { + "test": "ts-node src/createOrder.ts", + "build": "tsc", + "start": "node dist/createOrder.js" + }, + "keywords": [ + "polymarket", + "demo", + "test" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@polymarket/clob-client": "^4.22.8", + "ethers": "^5.7.1" + }, + "devDependencies": { + "@types/node": "^18.7.18", + "ts-node": "^10.9.1", + "typescript": "^4.8.3" + } +} + diff --git a/polymarket-demo/src/createOrder.ts b/polymarket-demo/src/createOrder.ts new file mode 100644 index 0000000..b2c490f --- /dev/null +++ b/polymarket-demo/src/createOrder.ts @@ -0,0 +1,40 @@ +//npm install @polymarket/clob-client +//npm install ethers +//Client initialization example and dumping API Keys + +import { ApiKeyCreds, ClobClient, OrderType, Side, } from "@polymarket/clob-client"; +import { Wallet } from "@ethersproject/wallet"; + +const host = 'https://clob.polymarket.com'; +const funder = ''; //This is the address listed below your profile picture when using the Polymarket site. +const signer = new Wallet("[PRIVATE_KEY_REMOVED]"); //This is your Private Key. If using email login export from https://reveal.magic.link/polymarket otherwise export from your Web3 Application + + +//In general don't create a new API key, always derive or createOrDerive +const creds = new ClobClient(host, 137, signer).createOrDeriveApiKey(); + +//1: Magic/Email Login +//2: Browser Wallet(Metamask, Coinbase Wallet, etc) +//0: EOA (If you don't know what this is you're not using it) + +const signatureType = 1; + (async () => { + const clobClient = new ClobClient(host, 137, signer, await creds, signatureType, funder); + const resp2 = await clobClient.createAndPostOrder( + { + tokenID: "114304586861386186441621124384163963092522056897081085884483958561365015034812", //Use https://docs.polymarket.com/developers/gamma-markets-api/get-markets to grab a sample token + price: 0.01, + side: Side.BUY, + size: 5, + feeRateBps: 0, + }, + { tickSize: "0.01",negRisk: false }, //You'll need to adjust these based on the market. Get the tickSize and negRisk T/F from the get-markets above + //Refer to the API documentation to locate a tokenID: https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide + //Example token: 114304586861386186441621124384163963092522056897081085884483958561365015034812 ( Xi Jinping out in 2025, YES side ) + //{ tickSize: "0.001",negRisk: true }, + + OrderType.GTC, + ); + console.log(resp2) + })(); + diff --git a/polymarket-demo/tsconfig.json b/polymarket-demo/tsconfig.json new file mode 100644 index 0000000..bbce4ff --- /dev/null +++ b/polymarket-demo/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +