diff --git a/.env.example b/.env.example index e735967..e7e7e45 100644 --- a/.env.example +++ b/.env.example @@ -1,25 +1,30 @@ -# Database +# Database (Spring Boot reads DB_URL / DB_USERNAME / DB_PASSWORD) DB_URL=jdbc:mysql://127.0.0.1:3306/wallet_db?allowMultiQueries=true&useSSL=false&characterEncoding=UTF-8&autoReconnect=true DB_USERNAME=root DB_PASSWORD=your_secure_password -# RabbitMQ -RABBITMQ_HOST=127.0.0.1 -RABBITMQ_PORT=5672 -RABBITMQ_USERNAME=guest -RABBITMQ_PASSWORD=guest +# Server +SERVER_PORT=8080 -# XXL-Job -XXL_JOB_ADMIN_ADDRESSES=http://127.0.0.1:8099/xxl-job-admin -XXL_JOB_PORT=9999 -XXL_JOB_ACCESS_TOKEN=your_access_token - -# HSM Keystore +# Keystore (tokencore / WalletManager) KEYSTORE_DIR=/data/keystores -KEYSTORE_PASSWORD=your_keystore_password +WALLET_KEYSTORE_PASSWORD=your_keystore_password -# Encryption -WALLET_CRYPTO_PUSH_KEY=your_32_char_encryption_key_here +# Optional: EOS deposit address for EOS flows +EOS_DEPOSIT_ADDRESS= + +# Tokencore signing chain ids (H2 testnet example: ETH_SIGN_CHAIN_ID=11155111, BTC_SIGN_CHAIN_ID=1) +ETH_SIGN_CHAIN_ID=1 +BTC_SIGN_CHAIN_ID=0 + +# External signer (when wallet_chain_config.signing_backend = EXTERNAL) +EXTERNAL_SIGNER_BASE_URL= + +# Optional ETH gas price HTTP fallback (JSON shape must match ETH_GAS_LEVEL keys) +ETH_GAS_FALLBACK_URL= # Logging LOG_LEVEL=INFO + +# Encryption (if used by your deployment) +WALLET_CRYPTO_PUSH_KEY=your_32_char_encryption_key_here diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ba381c..d5c6f29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,39 +13,37 @@ jobs: build: name: Build & Test runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v4 - + - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - + - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 with: cache-read-only: ${{ github.ref != 'refs/heads/master' }} - + - name: Grant execute permission for gradlew run: chmod +x gradlew - + - name: Build run: ./gradlew build -x test --no-daemon - + - name: Run tests run: ./gradlew test --no-daemon continue-on-error: true - + - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-artifacts path: | - wallet-webapi/build/libs/*.jar - wallet-task/build/libs/*.jar - wallet-hsm/build/libs/*.jar + build/libs/*.jar retention-days: 7 docker: @@ -53,23 +51,19 @@ jobs: runs-on: ubuntu-latest needs: build if: github.ref == 'refs/heads/master' - - strategy: - matrix: - service: [wallet-webapi, wallet-task, wallet-hsm] - + steps: - uses: actions/checkout@v4 - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Build Docker image uses: docker/build-push-action@v5 with: context: . - file: ./${{ matrix.service }}/Dockerfile + file: ./Dockerfile push: false - tags: ${{ matrix.service }}:${{ github.sha }} + tags: wallet-app:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0bb389e..1e7bb39 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,17 +19,13 @@ build: stage: build script: - chmod +x ./gradlew - - ./gradlew wallet-webapi:bootJar --no-daemon -x test - - ./gradlew wallet-task:bootJar --no-daemon -x test - - ./gradlew wallet-hsm:bootJar --no-daemon -x test + - ./gradlew bootJar --no-daemon -x test artifacts: paths: - - wallet-webapi/build/libs/*.jar - - wallet-hsm/build/libs/*.jar - - wallet-task/build/libs/*.jar + - build/libs/*.jar expire_in: 1 hour -deploy wallet-webapi: +deploy wallet-app: image: docker:latest services: - docker:dind @@ -37,57 +33,11 @@ deploy wallet-webapi: - wallet-dev stage: deploy script: - - docker stop wallet-webapi || true - - docker rm wallet-webapi || true - - docker rmi wallet-webapi || true - - docker build -t wallet-webapi -f wallet-webapi/Dockerfile . - - docker run -d --name wallet-webapi - --restart unless-stopped - -p 10001:10001 - -v /etc/localtime:/etc/localtime:ro - --env-file /etc/wallet/webapi.env - wallet-webapi - when: manual - -deploy wallet-task: - image: docker:latest - services: - - docker:dind - tags: - - wallet-dev - stage: deploy - script: - - docker stop wallet-task || true - - docker rm wallet-task || true - - docker rmi wallet-task || true - - docker build -t wallet-task -f wallet-task/Dockerfile . - - docker run -d --name wallet-task - --restart unless-stopped - -p 10033:10033 - -v /etc/localtime:/etc/localtime:ro - --env-file /etc/wallet/task.env - wallet-task - when: manual - -deploy wallet-hsm: - image: docker:latest - services: - - docker:dind - tags: - - wallet-dev - stage: deploy - script: - - docker stop wallet-hsm || true - - docker rm wallet-hsm || true - - docker rmi wallet-hsm || true - - docker build -t wallet-hsm -f wallet-hsm/Dockerfile . - - docker run -d --name wallet-hsm - --restart unless-stopped - -p 10888:10888 - -v /etc/localtime:/etc/localtime:ro - -v /mnt/wallets:/data/keystores - --env-file /etc/wallet/hsm.env - wallet-hsm + - docker stop wallet-app || true + - docker rm wallet-app || true + - docker rmi wallet-app || true + - docker build -t wallet-app -f Dockerfile . + - docker run -d --name wallet-app --restart unless-stopped -p 8080:8080 -v /etc/localtime:/etc/localtime:ro -v /mnt/wallets:/data/keystores --env-file /etc/wallet/wallet.env wallet-app when: manual after_script: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5d06d51 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# Single-module Spring Boot app (sources under ./src) +FROM eclipse-temurin:17-jdk-alpine AS builder +WORKDIR /app +COPY gradlew gradlew.bat settings.gradle build.gradle gradle.properties ./ +COPY gradle ./gradle +COPY src ./src +RUN chmod +x gradlew && ./gradlew bootJar --no-daemon -x test + +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app +RUN addgroup -S wallet && adduser -S wallet -G wallet +COPY --from=builder /app/build/libs/*.jar /app/app.jar +USER wallet +EXPOSE 8080 +ENV SERVER_PORT=8080 +ENTRYPOINT ["java","-XX:+UseContainerSupport","-XX:MaxRAMPercentage=75.0","-jar","/app/app.jar"] diff --git a/README.md b/README.md index bc8a9ca..6f7d81f 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ It provides unified wallet management, blockchain synchronization, deposit detec This repository is converging to a **single-service architecture** to make deployment and operations simpler: -- One startup service (`wallet-webapi` runtime) +- One Spring Boot service (sources under [`src/`](src/); root Gradle module) - No RabbitMQ dependency - No xxl-job dependency -- HSM capabilities invoked in-process -- Unified tokencore dependency: `com.github.galaxyscitech:tokencore:2.0.0` +- HSM / signing invoked in-process via [tokencore](https://github.com/GalaxySciTech/tokencore) `com.github.galaxyscitech:tokencore:2.0.1` +- Optional **external HTTP signer** when `wallet_chain_config.signing_backend` = `EXTERNAL` (see `wallet.external-signer` in [`application.yml`](src/main/resources/application.yml)) This means fewer moving parts, easier troubleshooting, and faster onboarding for developers and DevOps teams. @@ -67,9 +67,27 @@ Services: ### Option B: Run locally with Gradle ```bash -./gradlew :wallet-webapi:bootRun +./gradlew bootRun ``` +Initialize the database from [`db/wallet_db.sql`](db/wallet_db.sql). If you already have `wallet_chain_config`, apply [`db/002_wallet_chain_signing_backend.sql`](db/002_wallet_chain_signing_backend.sql) for the `signing_backend` column. + +### Configuration highlights + +| Area | Notes | +|------|--------| +| JDBC | `DB_URL`, `DB_USERNAME`, `DB_PASSWORD` (see [`.env.example`](.env.example)) | +| Keystore | `KEYSTORE_DIR`, `WALLET_KEYSTORE_PASSWORD` | +| Testnet signing | `ETH_SIGN_CHAIN_ID` (e.g. `11155111` for Sepolia), `BTC_SIGN_CHAIN_ID` (`0` mainnet / `1` testnet) | +| Metrics | Actuator: `/actuator/health`, `/actuator/prometheus` | +| Gas fallback | Optional `ETH_GAS_FALLBACK_URL` in `sys_config` if the node does not expose `eth_gasPrice` | + +### H2 (testnet on-chain smoke) + +1. Point `ETH_RPC_URL` in `config` (or env) to a **Sepolia** (or other testnet) HTTP endpoint. +2. Set `ETH_SIGN_CHAIN_ID=11155111` and fund a test hot wallet. +3. Run `./gradlew bootRun` and exercise withdraw/collect paths from Swagger or your integration client. + --- ## First-time user path (10-minute onboarding) diff --git a/README_CN.md b/README_CN.md index 9277dd9..c2194da 100644 --- a/README_CN.md +++ b/README_CN.md @@ -3,11 +3,10 @@ 项目正在收敛为**单体 Spring Boot 钱包服务**。 ## 当前变化 -- 统一单服务启动(以 `wallet-webapi` 作为应用运行时) -- 不再依赖 RabbitMQ -- 不再依赖 xxl-job -- HSM 能力改为进程内调用 -- tokencore 统一为 `com.github.galaxyscitech:tokencore:2.0.0` +- **单模块**:源码仅在仓库根目录 [`src/`](src/),Gradle 在根目录 [`build.gradle`](build.gradle) +- 不再依赖 RabbitMQ / xxl-job +- 签名与地址:**tokencore** `com.github.galaxyscitech:tokencore:2.0.1`(进程内) +- 可选 **外部 HTTP 签名**:表 `wallet_chain_config.signing_backend = EXTERNAL`,配置 `wallet.external-signer.base-url` ## 快速开始 ### 1)Docker Compose 启动 @@ -21,9 +20,13 @@ docker compose up -d --build ### 2)手动启动 ```bash -./gradlew :wallet-webapi:bootRun +./gradlew bootRun ``` +数据库初始化见 [`db/wallet_db.sql`](db/wallet_db.sql)。若已有库,需增加 `signing_backend` 列时执行 [`db/002_wallet_chain_signing_backend.sql`](db/002_wallet_chain_signing_backend.sql)。 + +环境变量示例:[`.env.example`](.env.example)。测试网链上冒烟:配置 Sepolia 等 `ETH_RPC_URL`,设置 `ETH_SIGN_CHAIN_ID=11155111`,准备测试资金后走 Swagger 提现/归集相关接口。 + ## 接口 - 钱包接口:`/wallet/v1` - 链接口:`/block_chain/v1` diff --git a/build.gradle b/build.gradle index 30f11a7..f4c751a 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,9 @@ dependencies { implementation 'wf.bitcoin:bitcoin-rpc-client:1.2.4' implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' implementation 'com.github.sealedtx:bitcoin-cash-converter:1.0' - implementation 'com.github.galaxyscitech:tokencore:2.0.0' + implementation 'com.github.galaxyscitech:tokencore:2.0.1' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' implementation 'commons-codec:commons-codec:1.16.0' implementation 'org.bitcoinj:bitcoinj-core:0.14.7' diff --git a/build.sh b/build.sh index f03bf7b..ead9c08 100644 --- a/build.sh +++ b/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -./gradlew bootRepackage -rm -rf build/* -mv wallet-webapi/build/libs/*.jar build/ -mv wallet-task/build/libs/*.jar build/ -mv wallet-hsm/build/libs/*.jar build/ +set -e +./gradlew bootJar --no-daemon -x test +rm -rf build/dist +mkdir -p build/dist +cp build/libs/*.jar build/dist/ diff --git a/db/002_wallet_chain_signing_backend.sql b/db/002_wallet_chain_signing_backend.sql new file mode 100644 index 0000000..34f0d6c --- /dev/null +++ b/db/002_wallet_chain_signing_backend.sql @@ -0,0 +1,5 @@ +-- Optional migration: per-chain signing backend (TOKENCORE | EXTERNAL) +ALTER TABLE `wallet_chain_config` + ADD COLUMN `signing_backend` varchar(32) NOT NULL DEFAULT 'TOKENCORE' + COMMENT 'TOKENCORE=in-process tokencore; EXTERNAL=wallet.external-signer HTTP' + AFTER `withdraw_enabled`; diff --git a/db/wallet_db.sql b/db/wallet_db.sql index 67e027d..97c9cd0 100644 --- a/db/wallet_db.sql +++ b/db/wallet_db.sql @@ -331,6 +331,7 @@ CREATE TABLE `wallet_chain_config` ( `enabled` tinyint(1) DEFAULT 1, `deposit_scan_enabled` tinyint(1) DEFAULT 1, `withdraw_enabled` tinyint(1) DEFAULT 0, + `signing_backend` varchar(32) NOT NULL DEFAULT 'TOKENCORE' COMMENT 'TOKENCORE|EXTERNAL', `confirmations` int(11) DEFAULT 12, `start_block` bigint(20) DEFAULT 0, `current_block` bigint(20) DEFAULT 0, diff --git a/docker-compose.yml b/docker-compose.yml index 239ac14..f2eaa4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: app: build: context: . - dockerfile: wallet-webapi/Dockerfile + dockerfile: Dockerfile ports: - "8080:8080" environment: diff --git a/src/main/java/com/wallet/entity/domain/WalletChainConfig.java b/src/main/java/com/wallet/entity/domain/WalletChainConfig.java index 7c0f287..1ce18c1 100644 --- a/src/main/java/com/wallet/entity/domain/WalletChainConfig.java +++ b/src/main/java/com/wallet/entity/domain/WalletChainConfig.java @@ -14,6 +14,9 @@ public class WalletChainConfig { private Boolean enabled; private Boolean depositScanEnabled; private Boolean withdrawEnabled; + /** TOKENCORE (default) or EXTERNAL — see application.yml wallet.signing */ + @Column(name = "signing_backend", length = 32) + private String signingBackend = "TOKENCORE"; private Integer confirmations; private Long startBlock; private Long currentBlock; @@ -31,6 +34,8 @@ public class WalletChainConfig { public void setDepositScanEnabled(Boolean depositScanEnabled) { this.depositScanEnabled = depositScanEnabled; } public Boolean getWithdrawEnabled() { return withdrawEnabled; } public void setWithdrawEnabled(Boolean withdrawEnabled) { this.withdrawEnabled = withdrawEnabled; } + public String getSigningBackend() { return signingBackend; } + public void setSigningBackend(String signingBackend) { this.signingBackend = signingBackend; } public Integer getConfirmations() { return confirmations; } public void setConfirmations(Integer confirmations) { this.confirmations = confirmations; } public Long getStartBlock() { return startBlock; } diff --git a/src/main/kotlin/com/wallet/WebApiApplication.kt b/src/main/kotlin/com/wallet/WebApiApplication.kt index 2e3fb82..e45d00f 100644 --- a/src/main/kotlin/com/wallet/WebApiApplication.kt +++ b/src/main/kotlin/com/wallet/WebApiApplication.kt @@ -1,12 +1,15 @@ package com.wallet +import com.wallet.biz.config.WalletProperties import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties /**
 * Created by pie on 2019-04-11 15: 53.
 */ @SpringBootApplication +@EnableConfigurationProperties(WalletProperties::class) open class WebApiApplication fun main() { diff --git a/src/main/kotlin/com/wallet/webapi/config/CorsConfig.kt b/src/main/kotlin/com/wallet/api/config/CorsConfig.kt similarity index 96% rename from src/main/kotlin/com/wallet/webapi/config/CorsConfig.kt rename to src/main/kotlin/com/wallet/api/config/CorsConfig.kt index 22319cb..60d5b17 100644 --- a/src/main/kotlin/com/wallet/webapi/config/CorsConfig.kt +++ b/src/main/kotlin/com/wallet/api/config/CorsConfig.kt @@ -1,4 +1,4 @@ -package com.wallet.webapi.config +package com.wallet.api.config import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration diff --git a/src/main/kotlin/com/wallet/webapi/config/WebMvcConfig.kt b/src/main/kotlin/com/wallet/api/config/WebMvcConfig.kt similarity index 89% rename from src/main/kotlin/com/wallet/webapi/config/WebMvcConfig.kt rename to src/main/kotlin/com/wallet/api/config/WebMvcConfig.kt index dfae070..002e817 100644 --- a/src/main/kotlin/com/wallet/webapi/config/WebMvcConfig.kt +++ b/src/main/kotlin/com/wallet/api/config/WebMvcConfig.kt @@ -1,6 +1,6 @@ -package com.wallet.webapi.config +package com.wallet.api.config -import com.wallet.webapi.config.interceptor.RequestInterceptor +import com.wallet.api.config.interceptor.RequestInterceptor import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.CorsRegistry diff --git a/src/main/kotlin/com/wallet/webapi/config/filter/WebFilter.kt b/src/main/kotlin/com/wallet/api/config/filter/WebFilter.kt similarity index 95% rename from src/main/kotlin/com/wallet/webapi/config/filter/WebFilter.kt rename to src/main/kotlin/com/wallet/api/config/filter/WebFilter.kt index 6f9668a..7648686 100644 --- a/src/main/kotlin/com/wallet/webapi/config/filter/WebFilter.kt +++ b/src/main/kotlin/com/wallet/api/config/filter/WebFilter.kt @@ -1,4 +1,4 @@ -package com.wallet.webapi.config.filter +package com.wallet.api.config.filter import jakarta.servlet.* import jakarta.servlet.http.HttpServletRequest diff --git a/src/main/kotlin/com/wallet/webapi/config/interceptor/RequestInterceptor.kt b/src/main/kotlin/com/wallet/api/config/interceptor/RequestInterceptor.kt similarity index 97% rename from src/main/kotlin/com/wallet/webapi/config/interceptor/RequestInterceptor.kt rename to src/main/kotlin/com/wallet/api/config/interceptor/RequestInterceptor.kt index e479371..12e8768 100644 --- a/src/main/kotlin/com/wallet/webapi/config/interceptor/RequestInterceptor.kt +++ b/src/main/kotlin/com/wallet/api/config/interceptor/RequestInterceptor.kt @@ -1,4 +1,4 @@ -package com.wallet.webapi.config.interceptor +package com.wallet.api.config.interceptor import com.wallet.biz.domain.PageEntity import com.wallet.biz.domain.dict.ErrorCode diff --git a/src/main/kotlin/com/wallet/webapi/controller/AdminController.kt b/src/main/kotlin/com/wallet/api/controller/AdminController.kt similarity index 99% rename from src/main/kotlin/com/wallet/webapi/controller/AdminController.kt rename to src/main/kotlin/com/wallet/api/controller/AdminController.kt index 6d4b98a..a9a49dd 100644 --- a/src/main/kotlin/com/wallet/webapi/controller/AdminController.kt +++ b/src/main/kotlin/com/wallet/api/controller/AdminController.kt @@ -1,4 +1,4 @@ -package com.wallet.webapi.controller +package com.wallet.api.controller import com.wallet.biz.domain.dict.TokenResponse import com.wallet.biz.service.ConfigService diff --git a/src/main/kotlin/com/wallet/webapi/controller/BlockChainController.kt b/src/main/kotlin/com/wallet/api/controller/BlockChainController.kt similarity index 92% rename from src/main/kotlin/com/wallet/webapi/controller/BlockChainController.kt rename to src/main/kotlin/com/wallet/api/controller/BlockChainController.kt index fa91dee..526d42b 100644 --- a/src/main/kotlin/com/wallet/webapi/controller/BlockChainController.kt +++ b/src/main/kotlin/com/wallet/api/controller/BlockChainController.kt @@ -1,6 +1,6 @@ -package com.wallet.webapi.controller +package com.wallet.api.controller -import com.wallet.biz.dict.TokenKey +import com.wallet.biz.token.TokenCatalogService import com.wallet.biz.domain.dict.TokenResponse import com.wallet.biz.domain.po.CalculationFeePo import com.wallet.biz.domain.po.GetAddressBalancePo @@ -86,11 +86,14 @@ class BlockChainController { @GetMapping("get_support_token") @Operation(summary = "获得支持币种") fun getSupportToken(): TokenResponse> { - val chainList = TokenKey.getChainList() - val symbolList = TokenKey.getSymbolList() + val chainList = tokenCatalogService.distinctChains() + val symbolList = tokenCatalogService.distinctSymbols() return TokenResponse(mapOf("chain" to chainList, "symbol" to symbolList)) } @Autowired lateinit var blockChainXService: BlockChainXService + + @Autowired + lateinit var tokenCatalogService: TokenCatalogService } diff --git a/src/main/kotlin/com/wallet/webapi/controller/WalletController.kt b/src/main/kotlin/com/wallet/api/controller/WalletController.kt similarity index 99% rename from src/main/kotlin/com/wallet/webapi/controller/WalletController.kt rename to src/main/kotlin/com/wallet/api/controller/WalletController.kt index cb1043d..ba40149 100644 --- a/src/main/kotlin/com/wallet/webapi/controller/WalletController.kt +++ b/src/main/kotlin/com/wallet/api/controller/WalletController.kt @@ -1,4 +1,4 @@ -package com.wallet.webapi.controller +package com.wallet.api.controller import com.wallet.biz.domain.dict.TokenResponse import com.wallet.biz.domain.dict.KeyType diff --git a/src/main/kotlin/com/wallet/webapi/scheduler/DynamicTaskScheduler.kt b/src/main/kotlin/com/wallet/api/scheduler/DynamicTaskScheduler.kt similarity index 92% rename from src/main/kotlin/com/wallet/webapi/scheduler/DynamicTaskScheduler.kt rename to src/main/kotlin/com/wallet/api/scheduler/DynamicTaskScheduler.kt index e563681..d2d4b09 100644 --- a/src/main/kotlin/com/wallet/webapi/scheduler/DynamicTaskScheduler.kt +++ b/src/main/kotlin/com/wallet/api/scheduler/DynamicTaskScheduler.kt @@ -1,11 +1,11 @@ -package com.wallet.webapi.scheduler +package com.wallet.api.scheduler import com.wallet.biz.cache.CacheService import com.wallet.biz.dict.SysConfigKey import com.wallet.biz.handler.service.CollectService import com.wallet.biz.handler.service.SendFeeService import com.wallet.biz.handler.service.SynService -import com.wallet.webapi.service.RuntimeConfigService +import com.wallet.api.service.RuntimeConfigService import jakarta.annotation.PostConstruct import org.slf4j.LoggerFactory import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler @@ -29,7 +29,11 @@ class DynamicTaskScheduler( scheduler.setThreadNamePrefix("wallet-scheduler-") scheduler.initialize() register("deposit", SysConfigKey.SCHEDULER_DEPOSIT_SCAN_MS, SysConfigKey.SCHEDULER_DEPOSIT_SCAN_ENABLED) { synService.synDeposit() } - register("sync", SysConfigKey.SCHEDULER_CHAIN_SYNC_MS, SysConfigKey.SCHEDULER_CHAIN_SYNC_ENABLED) { + register( + "sync", + SysConfigKey.SCHEDULER_CHAIN_SYNC_MS, + SysConfigKey.SCHEDULER_CHAIN_SYNC_ENABLED + ) { synService.synETH(); synService.synOMNI(); synService.synTRX(); synService.synImportAddress() } register("sweep", SysConfigKey.SCHEDULER_SWEEP_MS, SysConfigKey.SCHEDULER_SWEEP_ENABLED) { diff --git a/wallet-webapi/src/main/kotlin/com/wallet/webapi/service/RuntimeConfigService.kt b/src/main/kotlin/com/wallet/api/service/RuntimeConfigService.kt similarity index 97% rename from wallet-webapi/src/main/kotlin/com/wallet/webapi/service/RuntimeConfigService.kt rename to src/main/kotlin/com/wallet/api/service/RuntimeConfigService.kt index 7c7b2bf..79ec116 100644 --- a/wallet-webapi/src/main/kotlin/com/wallet/webapi/service/RuntimeConfigService.kt +++ b/src/main/kotlin/com/wallet/api/service/RuntimeConfigService.kt @@ -1,4 +1,4 @@ -package com.wallet.webapi.service +package com.wallet.api.service import com.wallet.entity.domain.* import com.wallet.repository.* diff --git a/src/main/kotlin/com/wallet/biz/cache/impl/CacheServiceImpl.kt b/src/main/kotlin/com/wallet/biz/cache/impl/CacheServiceImpl.kt index ae3a493..ecfeded 100644 --- a/src/main/kotlin/com/wallet/biz/cache/impl/CacheServiceImpl.kt +++ b/src/main/kotlin/com/wallet/biz/cache/impl/CacheServiceImpl.kt @@ -2,7 +2,6 @@ package com.wallet.biz.cache.impl import com.wallet.biz.dict.SysConfigKey import com.wallet.biz.dict.AddressAdminKey -import com.wallet.biz.dict.TokenKey import com.wallet.biz.domain.PageEntity import com.wallet.biz.domain.dict.ErrorCode import com.wallet.biz.domain.exception.BizException @@ -109,21 +108,7 @@ open class CacheServiceImpl : com.wallet.biz.cache.CacheService { @Cacheable("wallet_token") override fun findAllWalletToken(): Map { - val map = walletTokenService.findAll().associateBy { "${it.tokenSymbol}_${it.chainType}" } - TokenKey.values().forEach { - val value = map["${it.tokenSymbol}_${it.chainType}"] - if (value == null) { - val walletToken = Token() - walletToken.chainType = it.chainType - walletToken.tokenSymbol = it.tokenSymbol - walletToken.gasLimit = it.gasLimit - walletToken.tokenAddress = it.tokenAddress - walletToken.minCollect=it.minCollect - walletTokenService.save(walletToken) - } - - } - return map + return walletTokenService.findAll().associateBy { "${it.tokenSymbol}_${it.chainType}" } } override fun getWalletToken(chainType: String, tokenSymbol: String): Token { diff --git a/src/main/kotlin/com/wallet/biz/config/WalletProperties.kt b/src/main/kotlin/com/wallet/biz/config/WalletProperties.kt new file mode 100644 index 0000000..7af064b --- /dev/null +++ b/src/main/kotlin/com/wallet/biz/config/WalletProperties.kt @@ -0,0 +1,27 @@ +package com.wallet.biz.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "wallet") +data class WalletProperties( + var signing: SigningProps = SigningProps(), + var externalSigner: ExternalSignerProps = ExternalSignerProps(), + var tokencore: TokencoreProps = TokencoreProps() +) { + data class SigningProps( + /** Default when wallet_chain_config.signing_backend is null */ + var defaultBackend: String = "TOKENCORE" + ) + + data class ExternalSignerProps( + var baseUrl: String = "", + var connectTimeoutMs: Int = 5000, + var readTimeoutMs: Int = 30000 + ) + + /** Chain id strings passed to tokencore signTransaction (mainnet vs testnet). */ + data class TokencoreProps( + var ethereumChainId: String = "1", + var bitcoinChainId: String = "0" + ) +} diff --git a/src/main/kotlin/com/wallet/biz/dict/SysConfigKey.kt b/src/main/kotlin/com/wallet/biz/dict/SysConfigKey.kt index ac8af0b..857802a 100644 --- a/src/main/kotlin/com/wallet/biz/dict/SysConfigKey.kt +++ b/src/main/kotlin/com/wallet/biz/dict/SysConfigKey.kt @@ -1,6 +1,36 @@ package com.wallet.biz.dict - DEPOSIT_NOTIFY_MODE("post","充值同步模式,支持 post","充值通知"), +/** + * System configuration keys (stored in `config` table). [defaultValue] seeds new installs when missing. + */ +enum class SysConfigKey( + var defaultValue: String, + var description: String, + var group: String +) { + HSM_URL("http://127.0.0.1:8080", "Legacy HSM URL (in-process mode may ignore)", "全局"), + ETH_RPC_URL("http://127.0.0.1:8545", "eth RPC地址", "以太坊"), + OMNI_RPC_URL("http://127.0.0.1:8332", "omni RPC地址", "比特币"), + DASH_RPC_URL("http://127.0.0.1:8332", "dash RPC地址", "达世"), + LTC_RPC_URL("http://127.0.0.1:8332", "ltc RPC地址", "莱特币"), + BCH_RPC_URL("http://127.0.0.1:8332", "bch RPC地址", "比特现金"), + BSV_RPC_URL("http://127.0.0.1:8332", "bsv RPC地址", "比特币SV"), + DOGE_RPC_URL("http://127.0.0.1:8332", "doge RPC地址", "狗狗币"), + EOS_RPC_URL("https://api.eosflare.io", "eos RPC地址", "柚子"), + TRX_API_URL("https://api.trongrid.io", "trx HTTP API地址", "波场"), + ETH_SCAN_BACK("12", "以太坊回扫高度数", "以太坊"), + ETH_GAS_LEVEL("fast", "以太坊手续费等级 fast average safeLow", "以太坊"), + ETH_GAS_FALLBACK_URL("", "Optional HTTP JSON URL for gas price fallback (empty = disabled)", "以太坊"), + BTC_SCAN_BACK("6", "比特币回扫高度数", "比特币"), + BTC_GAS_LEVEL("fastestFee", "比特币手续费等级 fastestFee halfHourFee hourFee", "比特币"), + GAS_PROP("1", "gas使用比例,范围0-1", "全局"), + LOG_LEVEL("1", "0-9 log等级", "全局"), + ERROR_LEVEL("0", "0 展示错误信息 |1 展示堆栈", "全局"), + TRX_SCAN_BACK("12", "波场回扫高度数", "波场"), + DEPOSIT_POST_NOTIFY_SALT("{\"3\":\"2\",\"9\":\"1\"}", "充值同步post数据盐值", "充值通知"), + DEPOSIT_POST_NOTIFY_URL("http://192.168.31.222", "充值同步post数据地址", "充值通知"), + DEPOSIT_NOTIFY_MODE("post", "充值同步模式 post rabbitmq", "充值通知"), + SCHEDULER_DEPOSIT_SCAN_ENABLED("true", "充值扫描任务开关", "任务调度"), SCHEDULER_DEPOSIT_SCAN_MS("15000", "充值扫描任务间隔毫秒", "任务调度"), SCHEDULER_CHAIN_SYNC_ENABLED("true", "区块同步任务开关", "任务调度"), @@ -9,6 +39,7 @@ package com.wallet.biz.dict SCHEDULER_SWEEP_MS("30000", "归集任务间隔毫秒", "任务调度"), SCHEDULER_FEE_SUPPLY_ENABLED("false", "手续费补充任务开关", "任务调度"), SCHEDULER_FEE_SUPPLY_MS("30000", "手续费补充任务间隔毫秒", "任务调度"), + SWEEP_ENABLED("false", "归集开关", "归集配置"), SWEEP_TO_ADDRESS("", "归集地址", "归集配置"), SWEEP_MIN_AMOUNT("0", "最小归集金额", "归集配置"), @@ -18,31 +49,4 @@ package com.wallet.biz.dict FEE_SUPPLY_ENABLED("false", "手续费补充开关", "手续费配置"), FEE_SUPPLY_FROM_ADDRESS("", "手续费补充地址", "手续费配置"), FEE_SUPPLY_MIN_GAS_BALANCE("0", "最小gas余额", "手续费配置"); - - */ -enum class SysConfigKey(var defaultValue:String,var description:String,var group:String) { - - HSM_URL("http://127.0.0.1:10888","hsm 请求地址","全局"), - ETH_RPC_URL("http://127.0.0.1:8545","eth RPC地址","以太坊"), - OMNI_RPC_URL("http://127.0.0.1:8332","omni RPC地址","比特币"), - DASH_RPC_URL("http://127.0.0.1:8332","dash RPC地址","达世"), - LTC_RPC_URL("http://127.0.0.1:8332","ltc RPC地址","莱特币"), - BCH_RPC_URL("http://127.0.0.1:8332","bch RPC地址","比特现金"), - BSV_RPC_URL("http://127.0.0.1:8332","bsv RPC地址","比特币SV"), - DOGE_RPC_URL("http://127.0.0.1:8332","doge RPC地址","狗狗币"), - EOS_RPC_URL("https://api.eosflare.io","eos RPC地址","柚子"), - TRX_API_URL("https://api.trongrid.io","trx HTTP API地址","波场"), - ETH_SCAN_BACK("12","以太坊回扫高度数","以太坊"), - ETH_GAS_LEVEL("fast","以太坊手续费等级 fast average safeLow","以太坊"), - BTC_SCAN_BACK("6","比特币回扫高度数","比特币"), - BTC_GAS_LEVEL("fastestFee","比特币手续费等级 fastestFee halfHourFee hourFee","比特币"), - GAS_PROP("1","gas使用比例,范围0-1","全局"), - LOG_LEVEL("1","0-9 log等级","全局"), - ERROR_LEVEL("0","0 展示错误信息 |1 展示堆栈","全局"), - TRX_SCAN_BACK("12","波场回扫高度数","波场"), - DEPOSIT_POST_NOTIFY_SALT("{\"3\":\"2\",\"9\":\"1\"}","充值同步post数据盐值","充值通知"), - DEPOSIT_POST_NOTIFY_URL("http://192.168.31.222","充值同步post数据地址 逗号分割可以推送多个服务端 例如 http://192.168.31.222,http://192.168.31.223","充值通知"), - DEPOSIT_NOTIFY_MODE("1","充值同步模式 post rabbitmq 逗号分割可以同时推送多个 例如 post,rabbitmq","充值通知"); - } - diff --git a/src/main/kotlin/com/wallet/biz/dict/TokenKey.kt b/src/main/kotlin/com/wallet/biz/dict/TokenKey.kt deleted file mode 100644 index 987004b..0000000 --- a/src/main/kotlin/com/wallet/biz/dict/TokenKey.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.wallet.biz.dict - -import com.wallet.biz.domain.dict.ErrorCode -import com.wallet.biz.domain.exception.BizException -import org.consenlabs.tokencore.wallet.model.ChainType -import java.math.BigDecimal - -/**
 - * Created by pie on 2020/7/24 19: 28.
 - */ -enum class TokenKey( - val tokenSymbol: String, - val chainType: String, - var tokenAddress: String?, - var gasLimit: Long?, - var minCollect:BigDecimal -) { - - BITCOIN_BITCOIN(ChainType.BITCOIN, ChainType.BITCOIN, null, null,BigDecimal.ZERO), - USDT_BITCOIN("USDT", ChainType.BITCOIN, "31", null,BigDecimal.ZERO), - USDT_ETHEREUM("USDT", ChainType.ETHEREUM, "0xdac17f958d2ee523a2206206994597c13d831ec7", 60000L,BigDecimal.ZERO), - ETHEREUM_ETHEREUM(ChainType.ETHEREUM, ChainType.ETHEREUM, null, null,BigDecimal.ZERO), - USDT_TRON("USDT", ChainType.TRON, "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", 5000000,BigDecimal.ZERO), - TRON_TRON(ChainType.TRON, ChainType.TRON, null, null,BigDecimal.ZERO), - DASH_DASH(ChainType.DASH, ChainType.DASH, null, null,BigDecimal.ZERO), - DOGECOIN_DOGECOIN(ChainType.DOGECOIN, ChainType.DOGECOIN, null, null,BigDecimal.ZERO), - LITECOIN_LITECOIN(ChainType.LITECOIN, ChainType.LITECOIN, null, null,BigDecimal.ZERO), - BITCOINSV_BITCOINSV(ChainType.BITCOINSV, ChainType.BITCOINSV, null, null,BigDecimal.ZERO), - BITCOINCASH_BITCOINCASH(ChainType.BITCOINCASH, ChainType.BITCOINCASH, null, null,BigDecimal.ZERO); - - companion object { - - fun getChainList():Set{ - return values().groupBy { it.chainType }.keys - } - - fun getSymbolList():Set{ - return values().groupBy { it.tokenSymbol }.keys - } - } -} diff --git a/src/main/kotlin/com/wallet/biz/domain/po/SignBitcoinPo.kt b/src/main/kotlin/com/wallet/biz/domain/po/SignBitcoinPo.kt index 1f5202a..aea855c 100644 --- a/src/main/kotlin/com/wallet/biz/domain/po/SignBitcoinPo.kt +++ b/src/main/kotlin/com/wallet/biz/domain/po/SignBitcoinPo.kt @@ -9,6 +9,9 @@ import java.util.ArrayList * Created by pie on 2019-03-05 13: 24.
 */ class SignBitcoinPo { + /** Optional tokencore chain id string, e.g. ChainId.BITCOIN_TESTNET; default mainnet in service */ + var chainId: String? = null + var walletId: String? = null var toAddress: String? = null diff --git a/src/main/kotlin/com/wallet/biz/domain/po/SignEthereumPo.kt b/src/main/kotlin/com/wallet/biz/domain/po/SignEthereumPo.kt index 3ed354a..fcf7c6a 100644 --- a/src/main/kotlin/com/wallet/biz/domain/po/SignEthereumPo.kt +++ b/src/main/kotlin/com/wallet/biz/domain/po/SignEthereumPo.kt @@ -8,6 +8,9 @@ import java.math.BigInteger */ class SignEthereumPo{ + /** Optional tokencore chain id; default ETHEREUM_MAINNET in service */ + var chainId: String? = null + var walletId: String? = null var toAddress: String? = null diff --git a/src/main/kotlin/com/wallet/biz/domain/po/SignUsdtCollectPo.kt b/src/main/kotlin/com/wallet/biz/domain/po/SignUsdtCollectPo.kt index dab602e..6db387e 100644 --- a/src/main/kotlin/com/wallet/biz/domain/po/SignUsdtCollectPo.kt +++ b/src/main/kotlin/com/wallet/biz/domain/po/SignUsdtCollectPo.kt @@ -8,6 +8,7 @@ import java.util.ArrayList * Created by pie on 2019-04-16 11: 47.
 */ class SignUsdtCollectPo{ + var chainId: String? = null var walletId: String? = null diff --git a/src/main/kotlin/com/wallet/biz/domain/po/SignUsdtPo.kt b/src/main/kotlin/com/wallet/biz/domain/po/SignUsdtPo.kt index 9cc2695..fb190ba 100644 --- a/src/main/kotlin/com/wallet/biz/domain/po/SignUsdtPo.kt +++ b/src/main/kotlin/com/wallet/biz/domain/po/SignUsdtPo.kt @@ -8,6 +8,7 @@ import java.util.ArrayList * Created by pie on 2019-04-13 16: 08.
 */ class SignUsdtPo{ + var chainId: String? = null var walletId: String? = null diff --git a/src/main/kotlin/com/wallet/biz/handler/service/impl/CollectServiceImpl.kt b/src/main/kotlin/com/wallet/biz/handler/service/impl/CollectServiceImpl.kt index 0267ae7..0e986c8 100644 --- a/src/main/kotlin/com/wallet/biz/handler/service/impl/CollectServiceImpl.kt +++ b/src/main/kotlin/com/wallet/biz/handler/service/impl/CollectServiceImpl.kt @@ -93,6 +93,7 @@ class CollectServiceImpl : CollectService{ val txSignResult = hsmRequest.signUsdtCollectTransaction( + token.chainType, waitCollectAddr, amount, fee.toBigDecimal().divide(BigDecimal.TEN.pow(8)), @@ -173,6 +174,7 @@ class CollectServiceImpl : CollectService{ val realAmount = (amount - fee).toBigDecimal().divide(BigDecimal.TEN.pow(8)) val txSignResult = hsmRequest.signBtcTransaction( + chainType, realAmount, fee.toBigDecimal().divide(BigDecimal.TEN.pow(8)), waitCollectAddr, @@ -251,6 +253,7 @@ class CollectServiceImpl : CollectService{ val realAmount = BigDecimal(balance).subtract(fee).divide(BigDecimal.TEN.pow(18)) val txSignResult = hsmRequest.signEthtransaction( + ChainType.ETHEREUM, nonce, realAmount, BigDecimal(gasPrice), @@ -339,6 +342,7 @@ class CollectServiceImpl : CollectService{ val txSignResult = hsmRequest.signEthtransaction( + ChainType.ETHEREUM, nonce, BigDecimal.ZERO, BigDecimal(gasPrice), diff --git a/src/main/kotlin/com/wallet/biz/handler/service/impl/SendFeeServiceImpl.kt b/src/main/kotlin/com/wallet/biz/handler/service/impl/SendFeeServiceImpl.kt index 0a705ce..46c7e12 100644 --- a/src/main/kotlin/com/wallet/biz/handler/service/impl/SendFeeServiceImpl.kt +++ b/src/main/kotlin/com/wallet/biz/handler/service/impl/SendFeeServiceImpl.kt @@ -79,6 +79,7 @@ class SendFeeServiceImpl : SendFeeService{ val txSignResult = hsmRequest.signEthtransaction( + ChainType.ETHEREUM, sendNonce, reduceAmount, BigDecimal(rpcClient.getGasPrice()), diff --git a/src/main/kotlin/com/wallet/biz/request/HsmRequest.kt b/src/main/kotlin/com/wallet/biz/request/HsmRequest.kt index 6cf018e..8fee193 100644 --- a/src/main/kotlin/com/wallet/biz/request/HsmRequest.kt +++ b/src/main/kotlin/com/wallet/biz/request/HsmRequest.kt @@ -2,6 +2,8 @@ package com.wallet.biz.request import com.wallet.biz.domain.po.* import com.wallet.biz.domain.vo.AddressVo +import com.wallet.biz.signing.ChainSigningService +import org.consenlabs.tokencore.wallet.model.ChainType import org.consenlabs.tokencore.wallet.transaction.BitcoinTransaction import org.consenlabs.tokencore.wallet.transaction.TxSignResult import org.springframework.beans.factory.annotation.Autowired @@ -36,20 +38,36 @@ class HsmRequest { signUsdtPo.fee = fee signUsdtPo.toAddress = toAddress signUsdtPo.walletId = walletId - return hsmXService.signUsdtTransaction(signUsdtPo) + return chainSigningService.signUsdt(ChainType.BITCOIN, signUsdtPo) } - fun signBtcTransaction(amount: BigDecimal, fee: BigDecimal, toAddress: String, utxos: ArrayList, walletId: String): TxSignResult { + fun signBtcTransaction( + chainType: String, + amount: BigDecimal, + fee: BigDecimal, + toAddress: String, + utxos: ArrayList, + walletId: String + ): TxSignResult { val signBitcoinPo = SignBitcoinPo() signBitcoinPo.utxos = utxos signBitcoinPo.amount = amount signBitcoinPo.fee = fee signBitcoinPo.toAddress = toAddress signBitcoinPo.walletId = walletId - return hsmXService.signBitcoinTransaction(signBitcoinPo) + return chainSigningService.signBitcoin(chainType, signBitcoinPo) } - fun signEthtransaction(nonce: Int, amount: BigDecimal, gasPrice: BigDecimal, gasLimit: Long, toAddress: String, walletId: String, data: String?): TxSignResult { + fun signEthtransaction( + chainType: String, + nonce: Int, + amount: BigDecimal, + gasPrice: BigDecimal, + gasLimit: Long, + toAddress: String, + walletId: String, + data: String? + ): TxSignResult { val signEthereumPo = SignEthereumPo() signEthereumPo.walletId = walletId signEthereumPo.amount = amount @@ -58,10 +76,19 @@ class HsmRequest { signEthereumPo.gasPrice = gasPrice signEthereumPo.data = data signEthereumPo.gasLimit = gasLimit - return hsmXService.signEthereumTransaction(signEthereumPo) + return chainSigningService.signEthereum(chainType, signEthereumPo) } - fun signUsdtCollectTransaction(toAddress: String, amount: BigDecimal, fee: BigDecimal, utxos: ArrayList, feeProviderUtxos: ArrayList, walletId: String, feeProviderWalletId: String): TxSignResult { + fun signUsdtCollectTransaction( + chainType: String, + toAddress: String, + amount: BigDecimal, + fee: BigDecimal, + utxos: ArrayList, + feeProviderUtxos: ArrayList, + walletId: String, + feeProviderWalletId: String + ): TxSignResult { val signUsdtCollectPo = SignUsdtCollectPo() signUsdtCollectPo.feeProviderUtxos = feeProviderUtxos signUsdtCollectPo.fee = fee @@ -70,7 +97,7 @@ class HsmRequest { signUsdtCollectPo.toAddress = toAddress signUsdtCollectPo.amount = amount signUsdtCollectPo.utxos = utxos - return hsmXService.signUsdtCollectTransaction(signUsdtCollectPo) + return chainSigningService.signUsdtCollect(chainType, signUsdtCollectPo) } fun checkWallet(walletCode: String): String = hsmXService.getAllWallets().firstOrNull { it.walletCode == walletCode }?.address ?: "" @@ -83,4 +110,7 @@ class HsmRequest { @Autowired lateinit var hsmXService: com.wallet.hsm.xservice.HsmXService + + @Autowired + lateinit var chainSigningService: ChainSigningService } diff --git a/src/main/kotlin/com/wallet/biz/rpc/RpcClient.kt b/src/main/kotlin/com/wallet/biz/rpc/RpcClient.kt index 97e9ccb..aaa2ffc 100644 --- a/src/main/kotlin/com/wallet/biz/rpc/RpcClient.kt +++ b/src/main/kotlin/com/wallet/biz/rpc/RpcClient.kt @@ -3,6 +3,7 @@ package com.wallet.biz.rpc import com.wallet.biz.dict.SysConfigKey import com.wallet.biz.rpc.BitcoinFork.* import com.fasterxml.jackson.databind.JsonNode +import org.consenlabs.tokencore.wallet.model.ChainType import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @@ -28,17 +29,21 @@ class RpcClient { lastGasPrice = gasPrice gasPrice } catch (e: Exception) { - logger.warn("Failed to get gas price from node, falling back to external API: ${e.message}") + logger.warn("Failed to get gas price from node: ${e.message}") + val fallbackUrl = cacheService.getSysConfig(SysConfigKey.ETH_GAS_FALLBACK_URL).trim() + if (fallbackUrl.isBlank()) { + logger.warn("ETH_GAS_FALLBACK_URL empty; using cached gas gwei: $lastGasPrice") + return lastGasPrice + } try { val gasLevel = cacheService.getSysConfig(SysConfigKey.ETH_GAS_LEVEL) val gasProp = cacheService.getSysConfig(SysConfigKey.GAS_PROP).toBigDecimal() - val node = - restTemplate.getForObject("https://ethgasstation.info/json/ethgasAPI.json", JsonNode::class.java) + val node = restTemplate.getForObject(fallbackUrl, JsonNode::class.java) val gasPrice = (node!![gasLevel].decimalValue() * gasProp).toInt() / 10 lastGasPrice = gasPrice gasPrice } catch (e2: Exception) { - logger.warn("Failed to get gas price from external API, using cached value: $lastGasPrice") + logger.warn("Gas price fallback HTTP failed: ${e2.message}; using cached value: $lastGasPrice") lastGasPrice } } @@ -119,6 +124,19 @@ class RpcClient { return TrxApi(url, restTemplate) } + /** UTXO-style chains that use a Bitcoin JSON-RPC compatible node. */ + fun bitcoinStyleRpc(chainType: String): BitcoinJSONRPCClient { + return when (chainType) { + ChainType.BITCOIN -> omniRpc() + ChainType.LITECOIN -> ltcRpc() + ChainType.BITCOINCASH -> bchRpc() + ChainType.BITCOINSV -> bsvRpc() + ChainType.DASH -> dashRpc() + ChainType.DOGECOIN -> dogeRpc() + else -> throw IllegalStateException("No UTXO RPC client for chain: $chainType") + } + } + @Autowired lateinit var cacheService: com.wallet.biz.cache.CacheService diff --git a/src/main/kotlin/com/wallet/biz/signing/ChainSigningService.kt b/src/main/kotlin/com/wallet/biz/signing/ChainSigningService.kt new file mode 100644 index 0000000..25b355c --- /dev/null +++ b/src/main/kotlin/com/wallet/biz/signing/ChainSigningService.kt @@ -0,0 +1,105 @@ +package com.wallet.biz.signing + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.wallet.biz.config.WalletProperties +import com.wallet.biz.domain.dict.ErrorCode +import com.wallet.biz.domain.exception.BizException +import com.wallet.biz.domain.po.SignBitcoinPo +import com.wallet.biz.domain.po.SignEthereumPo +import com.wallet.biz.domain.po.SignUsdtCollectPo +import com.wallet.biz.domain.po.SignUsdtPo +import com.wallet.hsm.xservice.HsmXService +import com.wallet.repository.WalletChainConfigRepository +import org.consenlabs.tokencore.wallet.transaction.TxSignResult +import org.slf4j.LoggerFactory +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate + +/** + * Routes signing to tokencore in-process or HTTP `wallet.external-signer`. + * POST `{baseUrl}/v1/sign` body: `{ "chainType", "kind", "payload" }` where kind matches the Po type. + * Response: `{ "signedTx", "txHash?", "wtxID?" }` or `{ "mode": "fallback" }` to use tokencore. + */ +@Service +class ChainSigningService( + private val hsmXService: HsmXService, + private val chainConfigRepository: WalletChainConfigRepository, + private val walletProperties: WalletProperties, + private val objectMapper: ObjectMapper, + private val restTemplate: RestTemplate +) { + private val log = LoggerFactory.getLogger(javaClass) + + fun signUsdt(chainType: String, po: SignUsdtPo): TxSignResult { + val tc = { hsmXService.signUsdtTransaction(po) } + return if (resolveBackend(chainType) == SigningBackend.TOKENCORE) tc() + else timed(chainType) { postExternal(chainType, "signUsdt", po, tc) } + } + + fun signUsdtCollect(chainType: String, po: SignUsdtCollectPo): TxSignResult { + val tc = { hsmXService.signUsdtCollectTransaction(po) } + return if (resolveBackend(chainType) == SigningBackend.TOKENCORE) tc() + else timed(chainType) { postExternal(chainType, "signUsdtCollect", po, tc) } + } + + fun signBitcoin(chainType: String, po: SignBitcoinPo): TxSignResult { + val tc = { hsmXService.signBitcoinTransaction(po) } + return if (resolveBackend(chainType) == SigningBackend.TOKENCORE) tc() + else timed(chainType) { postExternal(chainType, "signBitcoin", po, tc) } + } + + fun signEthereum(chainType: String, po: SignEthereumPo): TxSignResult { + val tc = { hsmXService.signEthereumTransaction(po) } + return if (resolveBackend(chainType) == SigningBackend.TOKENCORE) tc() + else timed(chainType) { postExternal(chainType, "signEthereum", po, tc) } + } + + private fun timed(chainType: String, block: () -> TxSignResult): TxSignResult { + val started = System.nanoTime() + return try { + block() + } finally { + log.info("external_sign chain={} took_ms={}", chainType, (System.nanoTime() - started) / 1_000_000) + } + } + + private fun resolveBackend(chainType: String): SigningBackend { + val def = SigningBackend.parse(walletProperties.signing.defaultBackend, SigningBackend.TOKENCORE) + val raw = runCatching { chainConfigRepository.findByChain(chainType)?.signingBackend }.getOrNull() + return SigningBackend.parse(raw, def) + } + + private fun postExternal(chainType: String, kind: String, payload: Any, tokencore: () -> TxSignResult): TxSignResult { + val base = walletProperties.externalSigner.baseUrl.trimEnd('/') + if (base.isBlank()) { + throw BizException( + ErrorCode.ERROR_PARAM.code, + "EXTERNAL signing for chain=$chainType but wallet.external-signer.base-url is empty" + ) + } + val body = objectMapper.createObjectNode().apply { + put("chainType", chainType) + put("kind", kind) + set("payload", objectMapper.valueToTree(payload)) + } + val headers = HttpHeaders().apply { contentType = MediaType.APPLICATION_JSON } + val response = restTemplate.postForEntity( + "$base/v1/sign", + HttpEntity(body, headers), + JsonNode::class.java + ).body ?: throw BizException(ErrorCode.ERROR_PARAM.code, "Empty response from external signer") + if ("fallback".equals(response.path("mode").asText(null), ignoreCase = true)) { + log.warn("external signer fallback for chain={} kind={}", chainType, kind) + return tokencore() + } + val signedTx = response.path("signedTx").asText(null) + ?: throw BizException(ErrorCode.ERROR_PARAM.code, "external signer response missing signedTx") + val txHash = response.path("txHash").asText(null) + val wtxId = response.path("wtxID").asText(null) + return if (txHash != null) TxSignResult(wtxId, signedTx, txHash) else TxSignResult(wtxId, signedTx) + } +} diff --git a/src/main/kotlin/com/wallet/biz/signing/SigningBackend.kt b/src/main/kotlin/com/wallet/biz/signing/SigningBackend.kt new file mode 100644 index 0000000..d8df3ed --- /dev/null +++ b/src/main/kotlin/com/wallet/biz/signing/SigningBackend.kt @@ -0,0 +1,16 @@ +package com.wallet.biz.signing + +enum class SigningBackend { + TOKENCORE, + EXTERNAL; + + companion object { + fun parse(raw: String?, defaultBackend: SigningBackend): SigningBackend { + if (raw.isNullOrBlank()) return defaultBackend + return when (raw.uppercase()) { + "EXTERNAL" -> EXTERNAL + else -> TOKENCORE + } + } + } +} diff --git a/src/main/kotlin/com/wallet/biz/token/TokenCatalogService.kt b/src/main/kotlin/com/wallet/biz/token/TokenCatalogService.kt new file mode 100644 index 0000000..d4bf201 --- /dev/null +++ b/src/main/kotlin/com/wallet/biz/token/TokenCatalogService.kt @@ -0,0 +1,16 @@ +package com.wallet.biz.token + +import com.wallet.repository.TokenRepository +import org.springframework.stereotype.Service + +/** Chain and token lists driven by the `token` table (E2). */ +@Service +class TokenCatalogService( + private val tokenRepository: TokenRepository +) { + fun distinctChains(): Set = + tokenRepository.findAll().mapNotNull { it.chainType }.filter { it.isNotBlank() }.toSortedSet() + + fun distinctSymbols(): Set = + tokenRepository.findAll().mapNotNull { it.tokenSymbol }.filter { it.isNotBlank() }.toSortedSet() +} diff --git a/src/main/kotlin/com/wallet/biz/xservice/impl/WalletXServiceImpl.kt b/src/main/kotlin/com/wallet/biz/xservice/impl/WalletXServiceImpl.kt index 2e7b2b3..e64201b 100644 --- a/src/main/kotlin/com/wallet/biz/xservice/impl/WalletXServiceImpl.kt +++ b/src/main/kotlin/com/wallet/biz/xservice/impl/WalletXServiceImpl.kt @@ -638,6 +638,7 @@ open class WalletXServiceImpl : WalletXService, LogService() { log("进行${ChainType.ETHEREUM} 代币${sendPo.symbol} 交易签名") val txSignResult = hsmRequest.signEthtransaction( + ChainType.ETHEREUM, ethNonce.nonce, BigDecimal.ZERO, BigDecimal(gasPrice), @@ -694,6 +695,7 @@ open class WalletXServiceImpl : WalletXService, LogService() { log("开始${ChainType.ETHEREUM}转币 转出nonce为${ethNonce.nonce}") log("正在签名中") val txSignResult = hsmRequest.signEthtransaction( + ChainType.ETHEREUM, ethNonce.nonce, sendPo.amount!!, BigDecimal(gasPrice), @@ -755,6 +757,7 @@ open class WalletXServiceImpl : WalletXService, LogService() { utxos = checkAmount(utxos, totalAmount) log("正在签名中") val txSignResult = hsmRequest.signBtcTransaction( + chainType, sendPo.amount!!, fee.toBigDecimal().divide(BigDecimal.TEN.pow(8)), sendPo.to!!, @@ -783,34 +786,16 @@ open class WalletXServiceImpl : WalletXService, LogService() { override fun getUtxos(chainType: String, address: String): ArrayList { - val symbol: String - val rpc = when (chainType) { - ChainType.BITCOIN -> { - symbol = "btc" - rpcClient.omniRpc() - } - ChainType.LITECOIN -> { - symbol = "ltc" - rpcClient.ltcRpc() - } - ChainType.BITCOINCASH -> { - symbol = "bch" - rpcClient.bchRpc() - } - ChainType.BITCOINSV -> { - symbol = "bsv" - rpcClient.bsvRpc() - } - ChainType.DASH -> { - symbol = "dash" - rpcClient.dashRpc() - } - ChainType.DOGECOIN -> { - symbol = "doge" - rpcClient.dogeRpc() - } + val symbol = when (chainType) { + ChainType.BITCOIN -> "btc" + ChainType.LITECOIN -> "ltc" + ChainType.BITCOINCASH -> "bch" + ChainType.BITCOINSV -> "bsv" + ChainType.DASH -> "dash" + ChainType.DOGECOIN -> "doge" else -> throw BizException(ErrorCode.NO_THIS_CHAIN_TYPE) } + val rpc = rpcClient.bitcoinStyleRpc(chainType) val utxos = ArrayList() rpc.listUnspent(0, Int.MAX_VALUE, address).forEach { if (it.confirmations() > -1) { diff --git a/src/main/kotlin/com/wallet/hsm/xservice/impl/HsmXServiceImpl.kt b/src/main/kotlin/com/wallet/hsm/xservice/impl/HsmXServiceImpl.kt index 5499884..612a3be 100644 --- a/src/main/kotlin/com/wallet/hsm/xservice/impl/HsmXServiceImpl.kt +++ b/src/main/kotlin/com/wallet/hsm/xservice/impl/HsmXServiceImpl.kt @@ -1,5 +1,6 @@ package com.wallet.hsm.xservice.impl +import com.wallet.biz.config.WalletProperties import com.wallet.biz.domain.dict.ErrorCode import com.wallet.biz.domain.dict.KeyType import com.wallet.biz.domain.exception.BizException @@ -57,8 +58,9 @@ class HsmXServiceImpl : HsmXService { signBitcoinPo.fee!!.multiply(BigDecimal.TEN.pow(8)).toLong(), signBitcoinPo.utxos ) + val chainIdStr = signBitcoinPo.chainId ?: walletProperties.tokencore.bitcoinChainId return bitcoinTransaction.signTransaction( - ChainId.BITCOIN_MAINNET.toString(), + chainIdStr, keyStoreProperties.password, wallet ) @@ -74,8 +76,9 @@ class HsmXServiceImpl : HsmXService { signEthereumPo.amount!!.multiply(BigDecimal.TEN.pow(18)).toBigInteger(), signEthereumPo.data ) + val chainIdStr = signEthereumPo.chainId ?: walletProperties.tokencore.ethereumChainId return ethereumTransaction.signTransaction( - ChainId.ETHEREUM_MAINNET.toString(), + chainIdStr, keyStoreProperties.password, wallet ) @@ -103,8 +106,9 @@ class HsmXServiceImpl : HsmXService { signUsdtPo.fee!!.multiply(BigDecimal.TEN.pow(8)).toLong(), signUsdtPo.utxos ) + val chainIdStr = signUsdtPo.chainId ?: walletProperties.tokencore.bitcoinChainId return bitcoinTransaction.signUsdtTransaction( - ChainId.BITCOIN_MAINNET.toString(), + chainIdStr, keyStoreProperties.password, wallet ) @@ -120,8 +124,9 @@ class HsmXServiceImpl : HsmXService { signUsdtCollectPo.fee!!.multiply(BigDecimal.TEN.pow(8)).toLong(), signUsdtCollectPo.utxos ) + val chainIdStr = signUsdtCollectPo.chainId ?: walletProperties.tokencore.bitcoinChainId return bitcoinTransaction.signUsdtCollectTransaction( - ChainId.BITCOIN_MAINNET.toString(), + chainIdStr, keyStoreProperties.password, wallet, feeProviderWallet, signUsdtCollectPo.feeProviderUtxos ) @@ -160,6 +165,18 @@ class HsmXServiceImpl : HsmXService { name = "TRON" path = BIP44Util.TRON_PATH } + ChainType.EOS -> { + name = "EOS" + path = BIP44Util.EOS_PATH + } + ChainType.DOGECOIN -> { + name = "DOGE" + path = BIP44Util.DOGECOIN_MAINNET_PATH + } + ChainType.FILECOIN -> { + name = "FIL" + path = BIP44Util.FILECOIN_PATH + } else -> throw BizException(ErrorCode.NO_THIS_TYPE) } val metadata = Metadata( @@ -253,4 +270,7 @@ class HsmXServiceImpl : HsmXService { @Autowired lateinit var keyStoreProperties: KeyStoreProperties + + @Autowired + lateinit var walletProperties: WalletProperties } diff --git a/src/main/kotlin/com/wallet/webapi/service/RuntimeConfigService.kt b/src/main/kotlin/com/wallet/webapi/service/RuntimeConfigService.kt deleted file mode 100644 index 7c7b2bf..0000000 --- a/src/main/kotlin/com/wallet/webapi/service/RuntimeConfigService.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.wallet.webapi.service - -import com.wallet.entity.domain.* -import com.wallet.repository.* -import org.springframework.stereotype.Service - -@Service -class RuntimeConfigService( - private val schedulerRepo: WalletSchedulerConfigRepository, - private val chainRepo: WalletChainConfigRepository, - private val rpcRepo: WalletRpcConfigRepository, - private val sweepRepo: WalletSweepConfigRepository, - private val withdrawRepo: WalletWithdrawConfigRepository, - private val feeRepo: WalletFeeSupplyConfigRepository, - private val securityRepo: WalletSecurityConfigRepository -) { - fun schedulerDefault(): WalletSchedulerConfig? = schedulerRepo.findByChain("DEFAULT") - fun chain(chain: String): WalletChainConfig? = chainRepo.findByChain(chain) - fun rpc(chain: String): WalletRpcConfig? = rpcRepo.findByChain(chain) - fun sweep(chain: String): WalletSweepConfig? = sweepRepo.findByChain(chain) - fun withdraw(chain: String): WalletWithdrawConfig? = withdrawRepo.findByChain(chain) - fun fee(chain: String): WalletFeeSupplyConfig? = feeRepo.findByChain(chain) - fun security(): WalletSecurityConfig? = securityRepo.findAll().firstOrNull() -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c61411a..c78caf4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,18 +1,65 @@ server: - port: ${SERVER_PORT:10888} + port: ${SERVER_PORT:8080} spring: application: - name: wallet-hsm + name: wallet-app + datasource: + url: ${DB_URL:jdbc:mysql://127.0.0.1:3306/wallet_db?allowMultiQueries=true&useSSL=false&characterEncoding=UTF-8&autoReconnect=true} + username: ${DB_USERNAME:root} + password: ${DB_PASSWORD:} + hikari: + maximum-pool-size: ${DB_POOL_MAX:20} + minimum-idle: ${DB_POOL_MIN:2} + jpa: + hibernate: + ddl-auto: none + show-sql: false + open-in-view: false + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + jdbc: + batch_size: 50 + order_inserts: true + order_updates: true + cache: + type: caffeine keystore: dir: ${KEYSTORE_DIR:/data/keystores} - password: ${KEYSTORE_PASSWORD:} + password: ${WALLET_KEYSTORE_PASSWORD:${KEYSTORE_PASSWORD:}} eosDepositAddress: ${EOS_DEPOSIT_ADDRESS:} +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics + endpoint: + health: + show-details: when_authorized + metrics: + tags: + application: ${spring.application.name} + +wallet: + signing: + # TOKENCORE | EXTERNAL — per-chain override in wallet_chain_config.signing_backend when column exists; see WalletChainProperties + default-backend: TOKENCORE + tokencore: + # Ethereum chain id for signing (1 = mainnet, 11155111 = Sepolia, etc.) + ethereum-chain-id: ${ETH_SIGN_CHAIN_ID:1} + # Bitcoin chain id for signing (0 = mainnet, 1 = testnet) + bitcoin-chain-id: ${BTC_SIGN_CHAIN_ID:0} + external-signer: + base-url: ${EXTERNAL_SIGNER_BASE_URL:} + connect-timeout-ms: ${EXTERNAL_SIGNER_CONNECT_TIMEOUT_MS:5000} + read-timeout-ms: ${EXTERNAL_SIGNER_READ_TIMEOUT_MS:30000} + logging: level: root: INFO com.wallet: ${LOG_LEVEL:INFO} file: - name: logs/wallet-hsm.log + name: logs/wallet-app.log diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index 7195813..ad584e5 --- a/start.sh +++ b/start.sh @@ -1,11 +1,9 @@ -#!/bin/bash -service docker start -docker start mysql5.7 -cd /home/java-wallet/xxl-job-admin -nohup java -Xmx1G -jar xxl-job-admin-2.2.0.jar & -cd /home/java-wallet/hsm -nohup java -Xmx1G -jar wallet-hsm-3.0.0.jar & -cd /home/java-wallet/webapi -nohup java -Xmx1G -jar wallet-webapi-3.0.0.jar & -cd /home/java-wallet/task -nohup java -Xmx1G -jar wallet-task-3.0.0.jar & \ No newline at end of file +#!/usr/bin/env bash +# Run the single Spring Boot jar built with: ./gradlew bootJar +set -euo pipefail +JAR=$(ls build/libs/*.jar 2>/dev/null | head -1) +if [[ -z "${JAR}" ]]; then + echo "No jar in build/libs — run ./gradlew bootJar first" >&2 + exit 1 +fi +exec java -XX:+UseContainerSupport -Xmx1G -jar "${JAR}" "$@" diff --git a/wallet-webapi/src/main/kotlin/com/wallet/webapi/scheduler/DynamicTaskScheduler.kt b/wallet-webapi/src/main/kotlin/com/wallet/webapi/scheduler/DynamicTaskScheduler.kt deleted file mode 100644 index e563681..0000000 --- a/wallet-webapi/src/main/kotlin/com/wallet/webapi/scheduler/DynamicTaskScheduler.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.wallet.webapi.scheduler - -import com.wallet.biz.cache.CacheService -import com.wallet.biz.dict.SysConfigKey -import com.wallet.biz.handler.service.CollectService -import com.wallet.biz.handler.service.SendFeeService -import com.wallet.biz.handler.service.SynService -import com.wallet.webapi.service.RuntimeConfigService -import jakarta.annotation.PostConstruct -import org.slf4j.LoggerFactory -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler -import org.springframework.stereotype.Component -import java.time.Duration - -@Component -class DynamicTaskScheduler( - private val cacheService: CacheService, - private val synService: SynService, - private val collectService: CollectService, - private val sendFeeService: SendFeeService, - private val runtimeConfigService: RuntimeConfigService -) { - private val log = LoggerFactory.getLogger(javaClass) - private val scheduler = ThreadPoolTaskScheduler() - - @PostConstruct - fun start() { - scheduler.poolSize = 4 - scheduler.setThreadNamePrefix("wallet-scheduler-") - scheduler.initialize() - register("deposit", SysConfigKey.SCHEDULER_DEPOSIT_SCAN_MS, SysConfigKey.SCHEDULER_DEPOSIT_SCAN_ENABLED) { synService.synDeposit() } - register("sync", SysConfigKey.SCHEDULER_CHAIN_SYNC_MS, SysConfigKey.SCHEDULER_CHAIN_SYNC_ENABLED) { - synService.synETH(); synService.synOMNI(); synService.synTRX(); synService.synImportAddress() - } - register("sweep", SysConfigKey.SCHEDULER_SWEEP_MS, SysConfigKey.SCHEDULER_SWEEP_ENABLED) { - collectService.collectETH(); collectService.collectOMNI(); collectService.collectTRX(); collectService.collectTRC(); collectService.collectERC() - } - register("fee", SysConfigKey.SCHEDULER_FEE_SUPPLY_MS, SysConfigKey.SCHEDULER_FEE_SUPPLY_ENABLED) { - sendFeeService.sendFeeETH(); sendFeeService.sendFeeTRX() - } - } - - private fun register(name: String, intervalKey: SysConfigKey, enabledKey: SysConfigKey, task: () -> Unit) { - scheduler.scheduleWithFixedDelay({ - val enabled = readEnabled(name, enabledKey) - if (!enabled) return@scheduleWithFixedDelay - runCatching(task).onFailure { log.error("dynamic task {} failed: {}", name, it.message) } - }, Duration.ofMillis(readInterval(name, intervalKey))) - } - - private fun readEnabled(name: String, fallbackKey: SysConfigKey): Boolean { - val cfg = runtimeConfigService.schedulerDefault() - return when (name) { - "deposit" -> cfg?.depositScanEnabled - "sweep" -> cfg?.sweepEnabled - "fee" -> cfg?.feeSupplyEnabled - else -> null - } ?: (cacheService.getSysConfig(fallbackKey).lowercase() == "true") - } - - private fun readInterval(name: String, fallbackKey: SysConfigKey): Long { - val cfg = runtimeConfigService.schedulerDefault() - return when (name) { - "deposit" -> cfg?.depositScanIntervalMs - "sweep" -> cfg?.sweepIntervalMs - "fee" -> cfg?.feeSupplyIntervalMs - else -> null - } ?: (cacheService.getSysConfig(fallbackKey).toLongOrNull() ?: fallbackKey.defaultValue.toLong()) - } -}