跳到内容

Web3 DEX Starter

受众:开发者 摘要:PancakeSwap V2 Router 报价、池子储备只读查询、用户侧加减池报价 / calldata 构建和流动性订单追踪。Token 元数据缓存和地址本见 web3-token.md

何时使用

场景建议
Token 链上报价(默认 pancake-v2,降级 pancake-v2-poolWeb3.price(...)
DEX 池子只读(储备 / 流动性 / 手续费后输出 / 价格影响)Web3.dexPool(...)
用户侧加池子 / 减池子报价与 calldata 构建Web3DexClient#quoteLiquidity(...)
用户侧加池子 / 减池子订单追踪Core Web3 /api/v1/money/web3/liquidity/orders/**
低频常用链上地址收口web3-token.md 的「地址本与元数据缓存」
Token 静态元数据缓存(name / symbol / decimalsweb3-token.md 的「地址本与元数据缓存」

代币价格查询

Web3PriceClient 是 Web3 价格查询入口。默认 Provider 为 pancake-v2,底层通过 PancakeSwap V2 Router 的 getAmountsOut(uint256,address[]) 做只读报价,不发起 swap、授权、签名或广播。

内置 Provider:

Provider实现说明
pancake-v2pancake-v2通过 Router getAmountsOut 查询指定数量可兑换多少报价 Token,适合常规报价
pancake-v2-poolpancake-v2-pool通过 Factory getPair 找池子,再读取 Pair token0/token1/getReserves 计算池子中间价,适合 Router 或报价服务不可用时兜底

当请求没有显式指定 providerName 时,客户端会先调用 default-provider,失败后按 fallback-providers 顺序降级。显式指定 providerName 表示调用者要精确使用某个 Provider,不会自动降级。

java
@RequiredArgsConstructor
public class TokenPriceService {

    private final Web3PriceClient priceClient;

    public Web3TokenPrice price(String tokenAddress) {
        return priceClient.price(Web3TokenPriceRequest.builder()
                .chainName("bsc")
                .tokenAddress(tokenAddress)
                .tokenSymbol("TOKEN")
                .amountRaw(new BigInteger("1000000000000000000"))
                .build());
    }

}

返回值中的 amountInRaw / amountOutRaw 是链上最小单位,amountIn / amountOut 会按精度换算成人类可读数量,price 表示 1 个基础 Token 可兑换多少报价 Token。价格缓存、业务展示和后台管理都应该以 chainName + tokenAddress 作为唯一键;仅使用合约地址不够,因为不同链上可能存在相同地址。

Money Web3 模块提供只读后台接口,方便运维页或调试页按链查询报价:

http
POST /api/v1/money/web3/chains/{chainName}/token-prices

请求示例:

json
{
  "providerName": "pancake-v2-pool",
  "tokenAddress": "0x0000000000000000000000000000000000000001",
  "quoteTokenAddress": "0x55d398326f99059fF775485246999027B3197955",
  "pairAddress": null,
  "tokenSymbol": "TOKEN",
  "quoteTokenSymbol": "USDT",
  "tokenDecimals": 18,
  "quoteTokenDecimals": 18,
  "amountRaw": 1000000000000000000
}

providerName 为空时使用默认 Provider;显式传入 pancake-v2-pool 时会走池子储备量报价,并在返回值中包含 pairAddress / pairAddressUrl。该接口只读 RPC,不签名、不授权、不广播,也不写入业务订单或流水。

配置示例:

yaml
spring:
  open:
    web3:
      price:
        enabled: true
        default-provider: pancake-v2
        fallback-providers:
          - pancake-v2-pool
        providers:
          pancake-v2:
            enabled: true
            provider: pancake-v2
            chain-name: bsc
            router-address: 0x10ED43C718714eb63d5aA57B78B54704E256024E
            quote-token-address: 0x55d398326f99059fF775485246999027B3197955
            quote-token-symbol: USDT
            quote-token-decimals: 18
            base-token-decimals: 18
            default-amount-raw: 1000000000000000000
            fee-bps: 25
            default-slippage-bps: 50
          pancake-v2-pool:
            enabled: true
            provider: pancake-v2-pool
            chain-name: bsc
            factory-address: 0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73
            # 为空时通过 factory-address 自动查找 token/quote 对应 Pair。
            pair-address:
            quote-token-address: 0x55d398326f99059fF775485246999027B3197955
            quote-token-symbol: USDT
            quote-token-decimals: 18
            base-token-decimals: 18
            default-amount-raw: 1000000000000000000
            fee-bps: 25
            default-slippage-bps: 50
      dex:
        enabled: true
        default-provider: pancake-v2-pool

配置优先级同样遵循 Config starter,可在数据库配置中维护 spring.open.web3.price.*spring.open.web3.dex.*。生产环境要确保 chain-name 对应链已启用,且 RPC 节点可访问 PancakeSwap V2 Router、Factory、Pair 和目标 Token 合约。

DEX 池子只读查询

Web3DexClient 是 DEX 只读入口。第一阶段内置 pancake-v2-pool Provider,复用 spring.open.web3.price.providers.pancake-v2-pool 的链、Factory、Pair、报价 Token、手续费和默认滑点配置。

它只做只读 RPC:

  • 通过 Factory getPair(token, quoteToken) 自动查找 Pair,或使用请求中的 pairAddress
  • 读取 Pair token0token1getReserves
  • 读取 Pair 作为 LP Token 的 totalSupply;传入 walletAddress 后额外读取 balanceOf(walletAddress),传入或默认 Router 作为 spenderAddress 后读取 allowance(walletAddress, spenderAddress)
  • 计算基础 Token 储备、报价 Token 储备、按报价 Token 估算的池子总流动性。
  • 基于 V2 恒定乘积公式、fee-bpsdefault-slippage-bps 给出 amountOutminimumAmountOutpriceImpactBps 提示。

它不发起 swap、不授权、不签名、不广播。priceImpactBps 是给运维和风控参考的只读提示,不等于最终成交保障;真正下单仍应由业务侧结合 DEX Router、交易截止时间、滑点和风控规则处理。

Money Web3 模块提供只读后台接口:

http
POST /api/v1/money/web3/chains/{chainName}/dex/pools/info

请求示例:

json
{
  "providerName": "pancake-v2-pool",
  "tokenAddress": "0x0000000000000000000000000000000000000001",
  "quoteTokenAddress": "0x55d398326f99059fF775485246999027B3197955",
  "pairAddress": null,
  "walletAddress": "0x0000000000000000000000000000000000000004",
  "spenderAddress": null,
  "tokenSymbol": "TOKEN",
  "quoteTokenSymbol": "USDT",
  "tokenDecimals": 18,
  "quoteTokenDecimals": 18,
  "amountRaw": 1000000000000000000,
  "slippageBps": 50
}

常用返回字段:

字段说明
tokenReserve / quoteReserve池子中基础 Token 和报价 Token 的人类可读储备量
lpTokenAddressLP Token 地址;Pancake V2 Pair 本身就是 LP Token
lpTotalSupplyLP Token 总发行量
lpBalance请求钱包的 LP Token 持仓;未传 walletAddress 时为空
lpAllowance请求钱包对授权目标的 LP Token 授权额度;未传钱包或授权目标时为空
lpApproved钱包是否已对授权目标授权 LP Token;未传钱包或授权目标时为空
estimatedLiquidityInQuoteToken以报价 Token 估算的池子总流动性,约等于 quoteReserve * 2
price由储备量计算的中间价
amountOut按 V2 公式和 fee-bps 估算的输出数量
midAmountOut不考虑手续费和池子深度影响的中间价输出数量
minimumAmountOutslippageBps 计算的最小输出提示
priceImpactBps手续费和池子深度共同造成的输出差异,单位 bps

流动性加减池报价与订单 contract

Core Web3 已提供用户侧 Pancake V2 创建池子、加池子 / 减池子报价和订单追踪接口。该 contract 面向自托管钱包:平台只计算比例、滑点、授权和交易参数,不签名、不广播、不托管普通用户私钥;用户完成钱包广播后再把 txHash 绑定到订单,由服务端同步 receipt 并解析 Mint / Burn 结果。

App 钱包中心和 PC 用户中心已接入该 contract。App 可从钱包首页进入兑换页,PC 在 /account/balance 的链上资产区域展示流动性表单;两端都支持创建池子、加池子 / 减池子报价、创建订单、填写钱包广播后的交易哈希和手动同步链上结果。前端只消费后端返回的订单与报价,不伪造本地流动性订单。

报价支持 ERC-20 / ERC-20、native coin / ERC-20 和 ERC-20 / native coin 三种组合。native coin 场景必须提供 Wrapped Native Token 地址,Pair 查询和 reserves 计算走 wrapped native,交易 calldata 走 Pancake V2 Router 的 addLiquidityETH / removeLiquidityETH;native 侧不生成 ERC-20 授权,add-quote 会返回 transactionValueRaw 作为钱包交易的 value

用户侧入口:

http
POST /api/v1/money/web3/liquidity/add-quote
POST /api/v1/money/web3/liquidity/create-quote
POST /api/v1/money/web3/liquidity/remove-quote
POST /api/v1/money/web3/liquidity/orders
GET  /api/v1/money/web3/liquidity/orders
GET  /api/v1/money/web3/liquidity/orders/{orderNo}
POST /api/v1/money/web3/liquidity/orders/{orderNo}/tx
POST /api/v1/money/web3/liquidity/orders/{orderNo}/sync

add-quote 请求示例:

json
{
  "chainName": "bsc",
  "providerName": "pancake-v2-pool",
  "tokenAAddress": "0x0000000000000000000000000000000000000001",
  "tokenBAddress": "0x55d398326f99059fF775485246999027B3197955",
  "tokenANative": false,
  "tokenBNative": false,
  "wrappedNativeAddress": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
  "pairAddress": null,
  "walletAddress": "0x0000000000000000000000000000000000000004",
  "recipientAddress": "0x0000000000000000000000000000000000000004",
  "routerAddress": null,
  "amountADesiredRaw": 1000000000000000000,
  "amountBDesiredRaw": 30000000000000000000,
  "slippageBps": 100,
  "deadlineSeconds": 900
}

create-quote 使用同一请求结构,但 pairAddress 通常为空;Provider 会通过 Factory 查询 Pair 是否存在。Pair 不存在时返回首次流动性 quote,交易 calldata 仍走 Router addLiquidity,由兼容 Router 隐式创建 Pair 并注入首笔流动性;首次流动性由用户两侧数量决定初始价格,因此返回显著风险提示,并标记为非官方池。

remove-quote 使用同一请求结构,但传入 liquidityRaw 表示要移除的 LP Token 最小单位数量,不传 amountADesiredRaw / amountBDesiredRaw

native coin 请求可把对应 token 地址填为 native0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 或 wrapped native 地址,同时把对应 tokenANative / tokenBNative 置为 true。只允许一侧为 native;两侧 native 会被拒绝。

Admin 官方池入口:

http
POST /api/v1/admin/web3/liquidity/create-quote
POST /api/v1/admin/web3/liquidity/add-quote
POST /api/v1/admin/web3/liquidity/remove-quote
GET  /api/v1/admin/web3/liquidity/orders
GET  /api/v1/admin/web3/liquidity/orders/{orderNo}
POST /api/v1/admin/web3/liquidity/orders/{orderNo}/sync

Admin 官方池报价会在调用 Pancake V2 Provider 前读取当前启用的 DEX 配置。后台可以通过 money_web3_swap_dex_config.metadata_json 配置官方池 token / pair 白名单和单笔限额;未配置白名单时不额外限制 token 组合,配置后只允许命中的 pair 或无序 token 对。native coin 报价会先映射到 DEX 配置或请求中的 wrapped native 地址再匹配白名单。

json
{
  "officialPool": {
    "allowedPairs": [
      {
        "tokenA": "0x0000000000000000000000000000000000000001",
        "tokenB": "0x55d398326f99059fF775485246999027B3197955"
      },
      "0x0000000000000000000000000000000000000005"
    ],
    "singleAmountLimitRaw": "1000000000000000000000",
    "maxAmountADesiredRaw": "500000000000000000000",
    "maxAmountBDesiredRaw": "1000000000000000000000",
    "maxLiquidityRaw": "200000000000000000000"
  }
}

singleAmountLimitRaw 会作为 tokenA、tokenB 和 LP 的默认单笔上限;单侧配置 maxAmountADesiredRawmaxAmountBDesiredRawmaxLiquidityRaw 时优先生效。metadata_json 不是合法 JSON 或限额不是整数时,官方池报价会失败;用户侧普通流动性报价不读取该官方池规则。

常用返回字段:

字段说明
tokenAAmountRaw / tokenBAmountRaw按 Pair reserves 修正后的实际加池投入,或减池预计取回数量
tokenAMinRaw / tokenBMinRawslippageBps 计算的最小投入 / 到账数量
liquidityRaw加池预计获得或减池消耗的 LP Token 数量
tokenANative / tokenBNative当前报价是否把该侧视为 native coin
wrappedNativeAddressnative coin 对应的 Wrapped Native Token 地址,用于 Pair 查询和 reserves 计算
transactionValueRawnative coin 加池时钱包交易需要携带的原生币 value;ERC-20 / ERC-20 或减池时为 0
pairExistsFactory 或显式 Pair 是否已存在
createPairRequired创建池子报价是否需要新建 Pair
firstLiquidity是否属于首次流动性注入,首次注入会决定初始价格
nonOfficialPool是否为用户侧非官方池操作,前端必须展示风险提示
warnings风险提示 key,例如 liquidity-create-pair-requiredliquidity-first-price-set-by-userliquidity-non-official-poolliquidity-native-coin-uses-wrapped-native-pair
tokenAAllowanceRaw / tokenBAllowanceRaw / lpAllowanceRaw钱包当前授权额度
needsTokenAApproval / needsTokenBApproval / needsLpApproval是否需要先执行授权交易
approveTokenACalldata / approveTokenBCalldata / approveLpCalldataERC-20 approve(spender, amount) calldata
transactionToAddress / transactionCalldataaddLiquidity / removeLiquidity / addLiquidityETH / removeLiquidityETH 的目标地址与 calldata
routerAddressUrl / pairAddressUrl / token*AddressUrl按链浏览器配置生成的地址链接

当前实现复用 pancake-v2-pool Provider 的 Factory / Router / Pair 配置。Provider 会通过 Factory 自动查找 Pair,读取 token0token1getReserves 和 LP totalSupply,再按 tokenA/tokenB 请求顺序映射 reserves。传入 walletAddress 时会读取 Token 或 LP allowance;传入 recipientAddress 或可由 walletAddress 推导时才返回交易 calldata。

/orders 会基于同一报价创建订单快照,保存 action、Pair / Router、Token、报价数量、授权与交易 calldata、风险评估、当前登录用户和 traceId。action 支持 addremovecreatecreate 成功回执按 Mint 事件解析。订单初始状态按报价结果进入 pending-approvalpending-signature;人工复核阻断时进入 manual-review

用户完成钱包签名并广播后,前端把交易哈希提交到 /orders/{orderNo}/tx。系统会校验订单归属、终态、人工复核状态和 txHash 唯一性;同一订单已绑定 txHash 时只允许同哈希幂等同步,不允许换绑。提交后系统立即尝试读取交易回执:未出块时订单进入 confirming,回执失败或 revert 时进入 failed 并记录失败原因;成功回执必须解析到 Pancake V2 MintBurn 事件,系统会保存 receiptStatusblockNumbergasUsed、实际 tokenA / tokenB 数量、LP Token 数量和事件 JSON,订单进入 succeeded。弱网或 RPC 延迟时可调用 /orders/{orderNo}/sync 重新同步,GET /orders/{orderNo} 用于订单历史和详情展示。

Swap 报价与订单 contract

Core Web3 已提供 Swap 领域 contract,用于把后台 DEX 配置、路由策略、报价结果和用户订单快照串起来。该 contract 面向自托管钱包:平台生成报价、交易参数和订单状态,真实授权、签名和广播仍由用户钱包完成。

核心表:

说明
money_web3_swap_dex_configchainName + dexCode 保存 Pancake V2 兼容 DEX Router / Factory / WNATIVE / 默认报价 Token / 滑点 / deadline / 手续费配置
money_web3_swap_route_strategychainName + strategyCode 保存默认 DEX、基础路由 Token、exact-in / exact-out、最大跳数、最大滑点和路径策略
money_web3_swap_order保存用户 Swap 订单快照、报价快照、交易请求、风险评估、订单状态、txHash、回执状态、区块高度、gasUsed、真实成交数量、Swap 事件和 traceId

用户侧报价入口:

http
POST /api/v1/money/web3/swap/quote

用户侧订单入口:

http
POST /api/v1/money/web3/swap/orders

用户侧链上追踪入口:

http
POST /api/v1/money/web3/swap/orders/{orderNo}/tx
POST /api/v1/money/web3/swap/orders/{orderNo}/sync
GET  /api/v1/money/web3/swap/orders/{orderNo}

/quote 会结合启用的 DEX 配置和默认路由策略补齐 providerNameslippageBpsdeadlineSecondsmaxHops 和基础路由 Token,并在调用底层 Pancake V2 兼容报价前读取 Token 列表安全快照。已入目录 Token 会继承禁用、黑名单、风险标签和元数据完整性判断;未入目录 ERC-20 会先通过现有 metadata 预览读取 name / symbol / decimals,解析失败时以 token-unresolved 阻断报价,解析成功时以 token-custom 进入 watch 风险。DEX 报价返回后,系统会合并 DEX 价格影响等风险与 Token 风险,统一输出 securityAssessmentminimumAmountOutpriceImpactBps 等字段。/orders 需要登录,会基于同一报价创建 pending-approvalpending-signaturemanual-review 状态订单,并保存输入 / 输出 Token、amount、路径、报价和风险快照。

用户完成钱包签名并广播后,前端把链上交易哈希提交到 /orders/{orderNo}/tx。系统会校验订单归属、终态、人工复核状态和 txHash 唯一性;同一订单已绑定 txHash 时只允许同哈希幂等同步,不允许换绑。提交后系统立即尝试读取交易回执:未出块时订单进入 confirming,回执失败或 revert 时进入 failed 并记录失败原因;成功回执必须解析到 Pancake V2 Swap 事件,系统会保存 receiptStatusblockNumbergasUsedactualAmountInRawactualAmountOutRaw 和事件 JSON,订单进入 succeeded。弱网或 RPC 延迟时可调用 /orders/{orderNo}/sync 重新同步,GET /orders/{orderNo} 用于订单历史和详情展示。

Admin 运维入口已提供 DEX 配置、路由策略、Swap 订单查询 / 同步和 DEX 暂停 / 恢复能力:

http
GET    /api/v1/admin/web3/swap/dex-configs
POST   /api/v1/admin/web3/swap/dex-configs
PUT    /api/v1/admin/web3/swap/dex-configs/{id}
POST   /api/v1/admin/web3/swap/dex-configs/{id}/pause
POST   /api/v1/admin/web3/swap/dex-configs/{id}/resume
GET    /api/v1/admin/web3/swap/route-strategies
POST   /api/v1/admin/web3/swap/route-strategies
PUT    /api/v1/admin/web3/swap/route-strategies/{id}
GET    /api/v1/admin/web3/swap/orders
GET    /api/v1/admin/web3/swap/orders/{orderNo}
POST   /api/v1/admin/web3/swap/orders/{orderNo}/sync

Admin 前端入口位于“Web3 运维 / Token 行情”页面中的 DEX 配置、路由策略和 Swap 订单页签。当前 contract 仍不替代 App / PC 真实用户入口;上线时用户侧入口也必须闭合后再开放生产 Swap。

用户侧安全体验入口

App / PC 钱包兑换页会先读取 Web3 用户体验元数据,再展示 Swap、后续 Bridge 和组合路由共用的安全基线。

http
GET /api/v1/money/web3/security/user-experience

该接口需要登录,返回内容包括:

字段说明
defaultMode当前固定为 preset-first,表示默认展示后台预设链和 Token
walletExecutionMode当前固定为 self-custody,表示平台只生成报价和交易参数,真实交易由用户钱包签名
advancedTokenInputEnabled是否允许用户进入高级自定义 Token 合约地址输入
customTokenImportRequiresConfirmation自定义 Token 是否必须经过解析和风险确认
quoteRequiredBeforeTransaction是否必须先报价再生成交易
chains已启用链列表的用户侧选项
presetTokens已启用 Token 列表的用户侧默认选项
riskWarnings预设资产、自定义 Token、自托管签名和跨链托管资金池等统一风险提示

前端必须优先展示 chains / presetTokens,自定义 ERC-20 合约地址只作为高级入口;后续 Swap / Bridge / Swap + Bridge + Swap 组合路由不得绕过 Token 解析、黑名单、风险标签、流动性、价格影响、滑点、deadline、钱包签名和人工复核等校验。

相关

Released under the Apache License 2.0.