4
Подписаться
1271
Подписчики

Практика упаковки Web3 Tron и доступ к SunSwap DEX на базе платформы FMZ

Создано: 2025-03-21 15:17:05, Обновлено: 2025-03-28 09:52:32
comments   0
hits   715

[TOC]

Предисловие

В сфере блокчейн-транзакций децентрализованные биржи (DEX) обеспечивают надежный способ обмена активами, а SunSwap, как представитель DEX в экосистеме Tron, поддерживает взаимодействие смарт-контрактов на основе Web3. Платформа FMZ (FMZ Quantitative) предоставляет мощную структуру количественной торговли, которая позволяет разработчикам интегрировать несколько торговых стратегий.

Платформа FMZ уже поддерживает инкапсуляцию объектов обмена Web3 Ethereum (ETH), а недавно была добавлена ​​поддержка Web3 TRON, инкапсулирующая объекты обмена цепочки TRON. Так же, как и к UniSwap DEX, можно получить доступ к SunSwap DEX.

В этой статье мы рассмотрим, как инкапсулировать интерфейс Web3 на основе количественной платформы FMZ и подключиться к SunSwap DEX для транзакций. Мы познакомим вас с методом взаимодействия Web3 сети Tron, вызовом смарт-контракта SunSwap и покажем на примерах кода, как запрашивать ликвидность, выполнять транзакции и получать результаты транзакций, чтобы построить эффективную количественную торговую стратегию.

заявление

Все коды и проекты, представленные в этой статье, были протестированы и предназначены для обучения и общения. Однако даже после тестирования их все равно может потребоваться скорректировать с учетом реальных потребностей. Если вам необходимо применить его в реальной торговле, обязательно оцените, оптимизируйте и модифицируйте его самостоятельно, чтобы убедиться, что он соответствует вашим потребностям.

Подготовка

  • Настройте объект обмена Web3 TRON платформы FMZ

В предыдущей статье「Расширение FMZ Quantitative Web3: добавление поддержки Tron и расширение возможностей транзакций в цепочке」В этой статье мы представилиTron gRPC Мы изучили общие методы настройки узлов и то, как настроить объект обмена Web3 TRON на FMZ. В этой статье не будет повторяться это содержание. Что касается адреса узла, мы будем напрямую использовать узел, официально предоставленный TRON.

  • Развертывание хоста

https://www.bilibili.com/video/BV1PC4y1F7TN/

В отличие от бирж CEX, здесь нет никаких ограничений, о которых стоило бы беспокоиться. Вы можете получить доступ к официальному узлу, развернув хост локально.

  • SunSwap DEX

https://sun.io/

Откройте в браузере страницу SunSwap, вы сможете подключиться к кошельку и удобно просматривать данные.

  • tronscan

https://tronscan.org/

Необходимые инструменты для просмотра и анализа данных.

  • base58

Адреса в TRON закодированы по алгоритму base58, что отличается от Ethereum. Иногда для просмотра и сравнения данных требуется некоторое преобразование. В FMZ вы можете напрямую использовать адреса, закодированные в base58. Некоторые смарт-контракты возвращают данные в кодировке HEX, что требует некоторого преобразования. Для преобразования форматов адресов реализовано несколько простых функций:

Ниже приведены основные функции преобразования. Из-за ограниченного места я не буду публиковать все коды. Полный код находится в библиотеке шаблонов «Tron SunSwap V3 Trading Library», указанной в конце статьи.

  function ethAddressToTron(ethAddress) {
      if (!/^0x[0-9a-fA-F]{40}$/.test(ethAddress)) {
          throw "Invalid Ethereum address"
      }

      const ethAddressBytes = new Uint8Array(20)
      for (let i = 0; i < 20; i++) {
          ethAddressBytes[i] = parseInt(ethAddress.substr(2 + i * 2, 2), 16)
      }

      const tronAddressBytes = new Uint8Array(21)
      tronAddressBytes[0] = 0x41
      tronAddressBytes.set(ethAddressBytes, 1)

      const hash1 = Encode("sha256", "hex", "hex", uint8ArrayToHex(tronAddressBytes))
      const hash2 = Encode("sha256", "hex", "hex", hash1)
      const checksum = hash2.slice(0, 8)

      const fullAddress = new Uint8Array(25)
      fullAddress.set(tronAddressBytes, 0)
      fullAddress.set(hexToUint8Array(checksum), 21)

      return base58Encode(fullAddress)
  }
  • Пакетный вызов

Поскольку иногда необходимо считывать данные из нескольких методов одновременно, вызов их по одному отнимает много времени и сил. Таким образом, пакетная вызывающая функция инкапсулируется:

  function multicall(e, multicallContractAddress, data, outTypes) {
      let ret = e.IO("api", multicallContractAddress, "aggregate", data)
      if (!ret || !ret["returnData"] || !Array.isArray(ret["returnData"])) {
          Log("invalid ret:", ret)
          return null
      }

      let arrRet = ret["returnData"]
      if (outTypes.length != arrRet.length) {
          Log("Arrays have unequal lengths:", arrRet, outTypes)
          return null
      }

      let outData = []
      for (let i in arrRet) {
          outData.push(e.IO("decode", outTypes[i], arrRet[i]))
      }

      return outData
  }

Библиотека торговли Tron SunSwap V3

Дизайн торговой библиотеки SunSwap V3 создан по образцу шаблона торговой библиотеки UniSwap платформы FMZ. Поскольку код шаблона SunSwap довольно длинный, я не буду его здесь публиковать, а в основном объясню реализованные функции.

Создать объект транзакции SunSwap

let e = $.CreateSunSwapEx()                 // 默认使用exchange即exchanges[0]初始化SunSwap交易对象。
// let e = $.CreateSunSwapEx(exchanges[1])  // 使用次交易所对象初始化SunSwap交易对象。

Этот шаблон имеет только одну функцию интерфейса, а именно:$.CreateSunSwapEx()Используется для создания объектов транзакций SunSwap. После создания объекта вы можете вызвать метод этого объекта, чтобы выполнить некоторые функциональные вызовы, такие как: транзакции погашения, запрос цен пула, получение всей информации о пуле V3, залог, распаковка и т. д.

GetMarkets

Тестовый код «Tron SunSwap V3 Trading Library» находится в шаблонеmain()Функция была записана и не будет здесь повторяться.

    let markets = e.GetMarkets()
    let idx = 0
    let tbl = {"type": "table", "title": "test GetMarkets", "cols": ["Index", "PoolAddress", "Symbol", "BaseAsset", "QuoteAsset", "BaseName", "QuoteName", "BaseDecimals", "QuoteDecimals", "BaseAddress", "QuoteAddress"], "rows": []}
    for (let currency in markets) {
        let arrMarket = markets[currency]
        for (let market of arrMarket) {
            tbl["rows"].push([idx, market["PoolAddress"], market["Symbol"], market["BaseAsset"], market["QuoteAsset"], market["BaseName"], market["QuoteName"], market["BaseDecimals"], market["QuoteDecimals"], market["BaseAddress"], market["QuoteAddress"]])    
            idx++
        }
    }
    LogStatus("`" + JSON.stringify(tbl) + "`")

функцияGetMarkets()Используется для получения актуальной информации обо всех пулах обмена пула SunSwap V3, кэшируется в переменных объектов транзакций SunSwap и используется для запроса точности, адреса и другой информации во время других операций.

Этот тестовый код отобразит информацию обо всех пулах V3 после запуска:

Практика упаковки Web3 Tron и доступ к SunSwap DEX на базе платформы FMZ

Некоторые адреса контрактов, такие как «фишинговые контракты» и «недействительные контракты», которые были замечены до сих пор, были удалены из кода.

GetTicker

    let ticker1 = e.GetTicker("WTRX_USDT")
    Log("symbol:", "WTRX_USDT", ", ticker:", ticker1)
    let ticker2 = e.GetTicker("iCMX_USDT", "TLVDozYEBgeaJXH7oKBsougzEJKNrogFun")  // iCMX_USDT
    Log("symbol:", "iCMX_USDT", ", ticker:", ticker2)

ДоступноGetTicker()Функция запрашивает цену определенного биржевого пула и может указать адрес конкретного пула. Обратите внимание, что некоторые пулы имеют очень низкую ликвидность, а цены приведены только для справки.

multicall

    let data = []
    let walletAddress = exchange.IO("address")
    data.push(["TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "0x" + exchange.IO("encode", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "balanceOf", walletAddress)])    
    data.push(["TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "0x" + exchange.IO("encode", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "balanceOf", walletAddress)])    
    Log(data)
    let ret = multicall(exchange, "TGXuuKAb4bnrn137u39EKbYzKNXvdCes98", data, ["uint256", "uint256"])
    Log(ret, toAmount(ret[0], 6))

Эту функцию можно использовать для последующей инкапсуляции функций. Например, в примере вы можете поместитьbalanceOfВызов метода кодируется, и данные запрашиваются один раз.

GetSwapOutAmount

    let retBuy = e.GetSwapOutAmount("WTRX_USDT", 1, "buy")     // 1 quote -> base
    Log("WTRX_USDT", "buy outAmount:", retBuy["outAmount"], ", ask:", (1 / retBuy["outAmount"]))
    let retSell = e.GetSwapOutAmount("WTRX_USDT", 1, "sell")   // 1 base -> quote
    Log("WTRX_USDT", "sell outAmount:", retSell["outAmount"], ", bid:", (retSell["outAmount"] / 1))

Это относительно важная функция, которая включает в себя интеллектуальную службу маршрутизации SunSwap. Учитывая tokenInSymbol и tokenOutSymbol, он может возвращать возможные пути обмена, количество выходных токенов и другую информацию.

Практика упаковки Web3 Tron и доступ к SunSwap DEX на базе платформы FMZ

GetAssets

    let assets = e.GetAssets()
    let tbl = {"type": "table", "title": "test GetAssets", "cols": ["Name", "Address", "Decimals", "Balance"], "rows": []}   
    for (let asset of assets) {
        tbl["rows"].push([asset.Currency, asset.Address, asset.Decimals, asset.Amount])
    }
    LogStatus("`" + JSON.stringify(tbl) + "`")

Поскольку не существует хорошего механизма обхода токенов TRC20, с помощью обхода Tronscan этот сторонний интерфейс используется для запроса информации об активах аккаунта.

Практика упаковки Web3 Tron и доступ к SunSwap DEX на базе платформы FMZ

Практика упаковки Web3 Tron и доступ к SunSwap DEX на базе платформы FMZ

Swap

    // 6 USDT -> ? WTRX
    let ret = e.Swap("WTRX_USDT", 6, "buy")
    Log(ret)

    // 10 TRX -> ? WTRX
    // let ret = e.Swap("TRX_WTRX", 10, "sell")
    // Log(ret)

Самая важная функция — этот обмен. При выполнении обмена путь обмена будет сформирован в соответствии с интеллектуальной маршрутизацией, а затем будет вызван метод обмена для обмена токенами. Обратите внимание, что если это обмен упаковкой/распаковкой TRX, он выполняется только с использованием метода контракта WTRX.

Есть и другие функции, которые можно добавить в код шаблона.main()Функция, в подробности вдаваться не буду.

Код шаблона

Этот шаблон не имеет никакихИнтерактивный дизайниПараметры интерфейса, он реализует только базовую функцию доступа к SunSwap DEX, а в будущем могут быть оптимизированы и добавлены дополнительные функции.

”`js function isEmptyObject(obj) { return Object.keys(obj).length === 0 }

function computePoolPrice(decimals0, decimals1, sqrtPriceX96) { [decimals0, decimals1, sqrtPriceX96] = [decimals0, decimals1, sqrtPriceX96].map(BigInt) const TWO = BigInt(2) const TEN = BigInt(10) const SIX_TENTH = BigInt(1000000) const Q192 = (TWO ** BigInt(96)) ** TWO return (Number((sqrtPriceX96 ** TWO * TEN ** decimals0 * SIX_TENTH) / (Q192 * TEN ** decimals1)) / Number(SIX_TENTH)) }

function multicall(e, multicallContractAddress, data, outTypes) { let ret = e.IO(“api”, multicallContractAddress, “aggregate”, data) if (!ret || !ret[“returnData”] || !Array.isArray(ret[“returnData”])) { Log(“invalid ret:”, ret) return null }

let arrRet = ret["returnData"]
if (outTypes.length != arrRet.length) {
    Log("Arrays have unequal lengths:", arrRet, outTypes)
    return null
}

let outData = []
for (let i in arrRet) {
    outData.push(e.IO("decode", outTypes[i], arrRet[i]))
}

return outData

}

function toAmount(s, decimals) { return Number((BigDecimal(BigInt(s)) / BigDecimal(Math.pow(10, decimals))).toString()) }

function toInnerAmount(n, decimals) { return (BigDecimal(n) * BigDecimal(Math.pow(10, decimals))).toFixed(0) }

function hexToUint8Array(hex) { if (hex.length % 2 !== 0) { throw new Error(“Invalid hex string length”) } return Uint8Array.from( hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)) ) }

function uint8ArrayToHex(array) { return Array.from(array).map(byte => byte.toString(16).padStart(2, “0”)).join(“”) }

function ethAddressToTron(ethAddress) { if (!/^0x[0-9a-fA-F]{40}$/.test(ethAddress)) { throw “Invalid Ethereum address” }

const ethAddressBytes = new Uint8Array(20)
for (let i = 0; i < 20; i++) {
    ethAddressBytes[i] = parseInt(ethAddress.substr(2 + i * 2, 2), 16)
}

const tronAddressBytes = new Uint8Array(21)
tronAddressBytes[0] = 0x41
tronAddressBytes.set(ethAddressBytes, 1)

const hash1 = Encode("sha256", "hex", "hex", uint8ArrayToHex(tronAddressBytes))
const hash2 = Encode("sha256", "hex", "hex", hash1)
const checksum = hash2.slice(0, 8)

const fullAddress = new Uint8Array(25)
fullAddress.set(tronAddressBytes, 0)
fullAddress.set(hexToUint8Array(checksum), 21)

return base58Encode(fullAddress)

}

function base58Encode(buffer) { const base58Alphabet = “123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz” let num = BigInt(“0x” + Array.from(buffer).map(b => b.toString(16).padStart(2, “0”)).join(“”)) let encoded = “” while (num > 0) { let remainder = num % 58n num = num / 58n encoded = base58Alphabet[Number(remainder)] + encoded } for (let byte of buffer) { if (byte === 0) { encoded = base58Alphabet[0] + encoded } else { break } }

return encoded

}

function base58ToHex(base58Str) { const ALPHABET = “123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz”

var num = BigInt(0)
for (var char of base58Str) {
    var digit = BigInt(ALPHABET.indexOf(char))
    if (digit === BigInt(-1)) throw new Error("Invalid Base58 character: " + char)
    num = num * BigInt(58) + digit
}

var hex = num.toString(16)    
if (hex.length % 2 !== 0) {
    hex = "0" + hex
}

return "0x" + hex

}

$.CreateSunSwapEx = function(e) { let sunSwapEx = {}

sunSwapEx.registerABI = function(address, codeABI) {
    let e = sunSwapEx.e

    if (typeof(address) == "undefined" || typeof(codeABI) == "undefined") {
        throw "need address, codeABI"
    }

    return e.IO("abi", address, codeABI)
}

sunSwapEx.getCurrencyInfo = function(token0Address, token1Address) {
    let e = sunSwapEx.e

    let arrTokenAddress = [token0Address, token1Address]
    let tokenInfoCallData = []
    let tokenInfoRetType = []
    for (let tokenAddress of arrTokenAddress) {
        tokenInfoCallData.push([tokenAddress, "0x" + e.IO("encode", tokenAddress, "symbol")])
        tokenInfoRetType.push("string")
        tokenInfoCallData.push([tokenAddress, "0x" + e.IO("encode", tokenAddress, "name")])
        tokenInfoRetType.push("string")
        tokenInfoCallData.push([tokenAddress, "0x" + e.IO("encode", tokenAddress, "decimals")])
        tokenInfoRetType.push("uint8")
    }
    let currencyInfo = _C(multicall, e, multicallContractAddress, tokenInfoCallData, tokenInfoRetType)
    if (currencyInfo.length != 6) {
        Log("invalid currency Info:", currencyInfo)
        return null
    }
    let ret = {
        "token0Symbol": currencyInfo[0],
        "token0Name": currencyInfo[1],
        "token0Decimals": currencyInfo[2],
        "token1Symbol": currencyInfo[3],
        "token1Name": currencyInfo[4],
        "token1Decimals": currencyInfo[5]
    }

    return ret
}

sunSwapEx.waitMined = function(tx) {
    let e = sunSwapEx.e        
    let i = 0
    let maxLoop = 10

    while (true) {
        Sleep(3000)

        let info = e.IO("api", "tron", "GetTransactionInfoByID", tx)
        if (info && info.receipt && typeof(info.receipt.result) == "number") {
            Log("GetTransactionInfoByID:", info)
            if (info.receipt.result == 1) {
                return true
            } else {                
                return false 
            }            
        }

        Log("Transaction not yet mined", tx)
        if (i > maxLoop) {
            break
        }
        i++
    }

    Log(`Transaction: ${tx} not found`)
    return false 
}

sunSwapEx.getTokenInfo = function(tokenSymbol) {
    let markets = sunSwapEx.markets
    if (!markets || isEmptyObject(markets)) {
        markets = sunSwapEx.GetMarkets()
    }

    if (tokenSymbol == "TRX") {
        return {"tokenSymbol": tokenSymbol, "tokenName": tokenSymbol, "tokenDecimals": 6, "tokenAddress": "--"}
    }

    for (let currency in markets) {
        for (let market of markets[currency]) {
            if (market["BaseAsset"] == tokenSymbol) {
                return {"tokenSymbol": market["BaseAsset"], "tokenName": market["BaseName"], "tokenDecimals": market["BaseDecimals"], "tokenAddress": market["BaseAddress"]}
            } else if (market["QuoteAsset"] == tokenSymbol) {
                return {"tokenSymbol": market["QuoteAsset"], "tokenName": market["QuoteName"], "tokenDecimals": market["QuoteDecimals"], "tokenAddress": market["QuoteAddress"]}
            }
        }
    }

    Log("not found token symbol:", tokenSymbol)
    return null
}

sunSwapEx.dealStakeTRX = function(methodName, resourceName, amount) {
    let e = sunSwapEx.e

    let walletAddress = e.IO("address")
    let balance = toInnerAmount(amount, 6)
    let resourceCode = -1

    if (resourceName == "BANDWIDTH") {
        resourceCode = 0
    } else if (resourceName == "ENERGY") {
        resourceCode = 1
    } else if (resourceName == "TRON_POWER") {
        resourceCode = 2
    } else {
        Log("not support resourceName:", resourceName)
        return null 
    }

    return e.IO("api", "tron", methodName, walletAddress, resourceCode, balance)
}

sunSwapEx.GetMarkets = function() {
    // sunswap v3 pool

    let e = sunSwapEx.e

    if (!sunSwapEx.markets) {
        sunSwapEx.markets = {}
    }

    let markets = sunSwapEx.markets
    if (!isEmptyObject(markets)) {
        return markets
    }

    let factoryV3Address = sunSwapEx.factoryV3Address

    // allPoolsLength
    let poolIdx = e.IO("api", factoryV3Address, "allPoolsLength")
    if (!poolIdx) {
        Log("invalid poolIdx:", poolIdx)
        return null 
    }
    Log("allPoolsLength:", poolIdx)

    // multicallContractAddress
    let multicallContractAddress = sunSwapEx.multicallContractAddress

    // get All pool address
    let retPools = []
    let getPoolsData = []
    let retPoolsType = []
    for (let i = 0; i < poolIdx; i++) {
        getPoolsData.push([factoryV3Address, "0x" + e.IO("encode", factoryV3Address, "allPools", String(i))])
        retPoolsType.push("address")

        if (getPoolsData.length > 30 || i == poolIdx - 1) {
            let arr = _C(multicall, e, multicallContractAddress, getPoolsData, retPoolsType)

            retPools.push(...arr)
            getPoolsData = []
            retPoolsType = []
        } 
    }

    // allPools
    let poolABI = sunSwapEx.poolABI
    for (let i in retPools) {
        // poolAddress
        let poolAddress = ethAddressToTron(retPools[i])

        // register pool ABI
        sunSwapEx.registerABI(poolAddress, poolABI)

        // get token address of the pool
        let tokenCallData = [[poolAddress, "0x" + e.IO("encode", poolAddress, "token0")], [poolAddress, "0x" + e.IO("encode", poolAddress, "token1")]]
        let tokenRetType = ["address", "address"]
        let arrTokenAddress = _C(multicall, e, multicallContractAddress, tokenCallData, tokenRetType)

        let token0Address = ethAddressToTron(arrTokenAddress[0])
        let token1Address = ethAddressToTron(arrTokenAddress[1])

        // symbol , name , decimals
        let currencyInfo = sunSwapEx.getCurrencyInfo(token0Address, token1Address)
        if (!currencyInfo) {
            return null
        }

        let token0Symbol = currencyInfo["token0Symbol"]
        let token0Name = currencyInfo["token0Name"]
        let token0Decimals = currencyInfo["token0Decimals"]
        let token1Symbol = currencyInfo["token1Symbol"]
        let token1Name = currencyInfo["token1Name"]
        let token1Decimals = currencyInfo["token1Decimals"]

        // Alias
        let mapAlias = {
            "TJWm3jWaJeCdyRckEXfopsJBvZi6wXVK2p": "PAPA",
            "TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn": "USDDOLD",
            "TTiwtPv4MFHMoCpGDBEF85qQjD2v4e99ck": "HOUSDOLD",
            "TRFe3hT5oYhjSZ6f3ji5FJ7YCfrkWnHRvh": "ETHB"
        }
        if (typeof(mapAlias[token0Address]) != "undefined") {
            token0Symbol = mapAlias[token0Address]
        }
        if (typeof(mapAlias[token1Address]) != "undefined") {
            token1Symbol = mapAlias[token1Address]
        }

        // bad
        let mapBad = {
            "TMEmRKPob42vhkqEGopCnfBPixN5XTkdUc": "U S D T",      // 注意:钓鱼合约
            "TXioUZK8ju3R54KvzSqPc6Ufq5wkxFqpq9": "U S D T",      // 注意:钓鱼合约
            "TU41FWQQT8hZ6t72gVEjtSGn1Dshbevi7g": "CNY Coin",     // 注意:无效
            "TYB7oXNq4VyuiH5BHb4os8rTh7Ca8pxqSx": "BULL",         // 注意:钓鱼合约
            "TYJxozCVMUiHg3TbQp9PKeuXdTsX9ugJz9": "SHISHA",       // 注意:不流通
            "TF9io9LGyjuK3uTpr73pAaQ5m9scxd9xvr": "TestToken18",  // 注意:测试交易对
            "TK8E3sFhBt3EB6gTT6d6co8RMB6DFUnNwE": "TestToken6"    // 注意:测试交易对
        }
        if (typeof(mapBad[token0Address]) != "undefined" || typeof(mapBad[token1Address]) != "undefined") {
            continue
        }

        // market
        let currency = token0Symbol + "_" + token1Symbol
        let market = {
            "Symbol": currency,
            "BaseAsset": token0Symbol,
            "QuoteAsset": token1Symbol,
            "BaseName": token0Name,
            "QuoteName": token1Name,
            "BaseDecimals": token0Decimals,
            "QuoteDecimals": token1Decimals,
            "BaseAddress": token0Address,
            "QuoteAddress": token1Address,
            "PoolAddress": poolAddress,
            "PoolIndex": i
        }

        if (!Array.isArray(markets[currency])) {
            markets[currency] = []
        }
        markets[currency].push(market)
        LogStatus(_D(), "get markets, length:", Object.keys(markets).length)

        Sleep(200)
    }

    _G("sunswap_markets", markets)
    sunSwapEx.markets = markets

    return markets
}

sunSwapEx.GetTicker = function(symbol, targetPoolAddress) {
    let e = sunSwapEx.e

    let markets = sunSwapEx.markets
    if (!markets || isEmptyObject(markets)) {
        markets = sunSwapEx.GetMarkets()
    }

    let arrPoolAddress = []
    let arrToken0Decimals = []
    let arrToken1Decimals = []
    for (let currency in markets) {        
        if (!Array.isArray(markets[currency]) || markets[currency].length == 0) {
            continue 
        }

        if (currency == symbol) {
            for (let ele of markets[currency]) {
                if (typeof(targetPoolAddress) != "undefined" && ele["PoolAddress"] != targetPoolAddress) {
                    continue 
                }
                arrPoolAddress.push(ele["PoolAddress"])
                arrToken0Decimals.push(ele["BaseDecimals"])
                arrToken1Decimals.push(ele["QuoteDecimals"])
            }
        }
    }

    if (arrPoolAddress.length == 0) {
        Log(`${symbol} and ${targetPoolAddress} not found`)
        sunSwapEx.markets = {}
        return null
    }

    let arrPrice = []
    let slot0CallData = []
    let slot0RetType = []
    for (let i in arrPoolAddress) {
        let poolAddress = arrPoolAddress[i]
        let poolABI = sunSwapEx.poolABI
        e.IO("abi", poolAddress, poolABI)
        slot0CallData.push([poolAddress, "0x" + e.IO("encode", poolAddress, "slot0")])
        slot0RetType.push("(uint160,int24,uint16,uint16,uint16,uint8,bool)")
    }
    let multicallContractAddress = sunSwapEx.multicallContractAddress
    let arrSlot0 = _C(multicall, e, multicallContractAddress, slot0CallData, slot0RetType)

    for (let i in arrSlot0) {
        let slot0 = arrSlot0[i]
        let token0Decimals = arrToken0Decimals[i]
        let token1Decimals = arrToken1Decimals[i]
        let sqrtPriceX96 = slot0["Field1"]
        let poolAddress = arrPoolAddress[i]
        let price = computePoolPrice(token0Decimals, token1Decimals, sqrtPriceX96)

        arrPrice.push({"price": price, "poolAddress": poolAddress})
    }

    return arrPrice
}

sunSwapEx.GetSwapOutAmount = function(symbol, amountIn, type) {
    let arrCurrency = symbol.split("_")
    if (arrCurrency.length != 2) {
        Log("invalid symbol:", symbol)
        return null
    }

    let baseCurrencyInfo = sunSwapEx.getTokenInfo(arrCurrency[0])
    let quoteCurrencyInfo = sunSwapEx.getTokenInfo(arrCurrency[1])
    if (!baseCurrencyInfo || !quoteCurrencyInfo) {
        return null 
    }

    let baseSymbol = baseCurrencyInfo["tokenSymbol"]
    let baseAddress = baseCurrencyInfo["tokenAddress"]
    let baseDecimals = baseCurrencyInfo["tokenDecimals"]
    let quoteSymbol = quoteCurrencyInfo["tokenSymbol"]
    let quoteAddress = quoteCurrencyInfo["tokenAddress"]
    let quoteDecimals = quoteCurrencyInfo["tokenDecimals"]

    // black hole address
    if (baseSymbol == "TRX") {
        baseAddress = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"
    }
    if (quoteSymbol == "TRX") {
        quoteAddress = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"
    }

    let query = null
    if (type == "buy") {
        let amount = toInnerAmount(amountIn, quoteDecimals)
        query = `?fromToken=${quoteAddress}&toToken=${baseAddress}&amountIn=${amount}&typeList=WTRX,SUNSWAP_V1,SUNSWAP_V2,SUNSWAP_V3`
    } else if (type == "sell") {
        let amount = toInnerAmount(amountIn, baseDecimals)
        query = `?fromToken=${baseAddress}&toToken=${quoteAddress}&amountIn=${amount}&typeList=WTRX,SUNSWAP_V1,SUNSWAP_V2,SUNSWAP_V3`
    } else {
        Log("not support type:", type)
        return null 
    }

    try {
        let ret = JSON.parse(HttpQuery("https://rot.endjgfsv.link/swap/router" + query))
        Log("https://rot.endjgfsv.link/swap/router" + query, "GetSwapOutAmount ret:", ret)

        if (!ret || ret["message"] != "SUCCESS") {
            Log("invalid data:", ret)
            return null
        }

        let outAmount = null
        let best = null 
        let info = ret["data"]
        for (let ele of info) {
            if (!outAmount) {
                outAmount = parseFloat(ele["amountOut"])
                best = ele
            } else {
                let amount = parseFloat(ele["amountOut"])
                if (amount > outAmount) {
                    outAmount = amount
                    best = ele
                }
            }
        }

        if (!outAmount || !best) {
            Log("info:", info)
            return null 
        }

        return {"info": info, "outAmount": outAmount, "best": best, "baseCurrencyInfo": baseCurrencyInfo, "quoteCurrencyInfo": quoteCurrencyInfo}
    } catch(e) {
        Log("e.name:", e.name, "e.stack:", e.stack, "e.message:", e.message)
        return null
    }
}

sunSwapEx.GetAccount = function() {
    let e = sunSwapEx.e

    let walletAddress = e.IO("address")
    let account = e.IO("api", "tron", "GetAccount", walletAddress)
    if (!account) {
        Log("get account failed, account:", account)
        return null 
    }

    let trxBalance = toAmount(account["balance"], 6)

    let resource = e.IO("api", "tron", "GetAccountResource", walletAddress)
    if (!resource) {
        Log("get resource failed, resource:", resource)
        return null 
    }

    let energyLimit = resource["EnergyLimit"] ? resource["EnergyLimit"] : 0
    let freeNetLimit = resource["freeNetLimit"] ? resource["freeNetLimit"] : 0

    return {"Info": {"GetAccount": account, "GetAccountResource": resource}, "TRX": trxBalance, "Bandwidth": freeNetLimit, "Energy": energyLimit}
}

sunSwapEx.GetAssets = function() {
    let e = sunSwapEx.e
    let walletAddress = e.IO("address")

    try {
        let ret = JSON.parse(HttpQuery("https://apilist.tronscanapi.com/api/account/token_asset_overview?address=" + walletAddress))

        let assets = []
        for (let info of ret["data"]) {
            let decimals = parseInt(info["tokenDecimal"])
            let asset = {
                "Currency": info["tokenAbbr"] == "trx" ? "TRX" : info["tokenAbbr"],
                "Decimals": decimals,
                "Amount": toAmount(info["balance"], decimals),
                "Address": info["tokenId"],
            }

            assets.push(asset)
        }

        return assets
    } catch(e) {
        Log("e.name:", e.name, "e.stack:", e.stack, "e.message:", e.message)
        return null
    }
}

sunSwapEx.StakeTRX = function(resourceName, frozenBalance) {
    return sunSwapEx.dealStakeTRX("FreezeBalanceV2", resourceName, frozenBalance)
}

sunSwapEx.UnStakeTRX = function(resourceName, unfreezeBalance) {
    return sunSwapEx.dealStakeTRX("UnfreezeBalanceV2", resourceName, unfreezeBalance)
}

sunSwapEx.WrappedTRX = function(actionType, amountIn) {
    let e = sunSwapEx.e

    let tx = null
    if (actionType == "deposit") {
        let amount = toInnerAmount(amountIn, 6)
        tx = e.IO("api", "TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR", "deposit", amount, {gasLimit: toInnerAmount(sunSwapEx.feeLimit, 6)})
    } else if (actionType == "withdraw") {
        tx = e.IO("api", "TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR", "withdraw(uint256)", toInnerAmount(amountIn, 6), {gasLimit: toInnerAmount(sunSwapEx.feeLimit, 6)})
    } else {
        Log("not support actionType:", actionType)
        return false 
    }

    if (tx) {
        Log("tx: ", tx)
        let txRet = sunSwapEx.waitMined(tx)
        if (!txRet) {
            Log(actionType, "failed")
            return false
        } else {
            Log(actionType, "success")
            return true
        }
    } else {
        Log("trans error")
        return false
    }
}

sunSwapEx.Swap = function(symbol, amountIn, type) {
    let e = sunSwapEx.e
    let smartRouterAddress = sunSwapEx.smartRouterAddress

    if (symbol == "TRX_WTRX" || symbol == "WTRX_TRX") {
        let actionType = null 
        if (type == "buy") {
            actionType = symbol == "TRX_WTRX" ? "withdraw" : "deposit"
        } else if (type == "sell") {
            actionType = symbol == "TRX_WTRX" ? "deposit" : "withdraw"
        } else {
            Log("invalid type:", type)
            return false 
        }

        return sunSwapEx.WrappedTRX(actionType, amountIn)
    }

    let swapInfo = sunSwapEx.GetSwapOutAmount(symbol, amountIn, type)
    if (!swapInfo || !swapInfo["best"]) {
        Log("invalid swapInfo:", swapInfo)
        return false
    }

    let outAmount = swapInfo["outAmount"]
    let best = swapInfo["best"]

    // path
    let path = best["tokens"]
    if (!path || path.length < 2) {
        Log("invalid path:", path)
        return false 
    }

    // poolVersions and versionsLen
    let poolVersions = [] 
    let versionsLen = []
    for (var v of best["poolVersions"]) {
        if (poolVersions.length == 0) {
            poolVersions.push(v)
            versionsLen.push(2)
        } else {
            if (poolVersions[poolVersions.length - 1] == v) {
                versionsLen[versionsLen.length - 1] += 1
            } else {
                poolVersions.push(v)
                versionsLen.push(1)
            }
        }
    }

    // fees 
    let poolFees = best["poolFees"]

    // get decimals , token name 
    let token0Decimals = swapInfo["baseCurrencyInfo"]["tokenDecimals"]
    let token1Decimals = swapInfo["quoteCurrencyInfo"]["tokenDecimals"]

    let tokenInName = type == "buy" ? swapInfo["quoteCurrencyInfo"]["tokenSymbol"] : swapInfo["baseCurrencyInfo"]["tokenSymbol"]
    let tokenOutName = type == "buy" ? swapInfo["baseCurrencyInfo"]["tokenSymbol"] : swapInfo["quoteCurrencyInfo"]["tokenSymbol"]
    let tokenInAddress = type == "buy" ? swapInfo["quoteCurrencyInfo"]["tokenAddress"] : swapInfo["baseCurrencyInfo"]["tokenAddress"]
    let tokenOutAddress = type == "buy" ? swapInfo["baseCurrencyInfo"]["tokenAddress"] : swapInfo["quoteCurrencyInfo"]["tokenAddress"]
    let tokenInDecimals = type == "buy" ? token1Decimals : token0Decimals

    // calc amount
    let amount = null 
    let minAmount = null 
    if (type == "buy") {
        amount = toInnerAmount(amountIn, token1Decimals)
        minAmount = toInnerAmount(outAmount * 0.99, token0Decimals)
    } else if (type == "sell") {
        amount = toInnerAmount(amountIn, token0Decimals)
        minAmount = toInnerAmount(outAmount * 0.99, token1Decimals)
    } else {
        Log("invalid type:", type)
        return false 
    }

    // wallet address 
    let walletAddress = e.IO("address")

    // expired timestamp
    let expiredTS = parseInt((new Date().getTime() + 1000 * 60 * 5) / 1000)

    // [amount of the token to be swapped, minimum acceptable amount of the token obtained from the swap, address to receive the token obtained from the swap, deadline]
    let data = [String(amount), String(minAmount), walletAddress, expiredTS]

    // allowance
    if (tokenInName != "TRX" && !(tokenInName == "WTRX" && tokenOutName == "TRX")) {
        let allowanceAmount = e.IO("api", tokenInAddress, "allowance", walletAddress, smartRouterAddress)
        let realAmount = toAmount(allowanceAmount, tokenInDecimals)
        if (realAmount < amountIn) {
            // approve
            Log("realAmount is", realAmount, "too small, try to approve large amount")
            let txApprove = e.IO("api", tokenInAddress, "approve(address,uint256)", smartRouterAddress, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", {gasLimit: toInnerAmount(sunSwapEx.feeLimit, 6)})
            if (!txApprove) {
                throw "approve error"
            }

            let txRet = sunSwapEx.waitMined(txApprove)
            if (!txRet) {
                Log("approve failed")
                return false 
            } else {
                Log("approve success")
            }
        } else {
            Log("allowance", realAmount, "no need to approve")
        }
    }

    // swap
    Log(`path:${path}, poolVersions:${poolVersions}, versionsLen:${versionsLen}, poolFees:${poolFees}, data:${data}`)
    Log("best swap:", best)
    let tx = e.IO("api", smartRouterAddress, "swapExactInput(address[],string[],uint256[],uint24[],(uint256,uint256,address,uint256))", tokenInName == "TRX" ? toInnerAmount(amountIn, 6) : 0, path, poolVersions, versionsLen, poolFees, data, {gasLimit: toInnerAmount(sunSwapEx.feeLimit, 6)})
    if (tx) {
        Log("tx: ", tx)
        let txRet = sunSwapEx.waitMined(tx)
        if (!txRet) {
            Log("swap", tokenInName, "to", tokenOutName, "failed")
            return false
        } else {
            Log("swap", tokenInName, "to", tokenOutName, "success")
            return true
        }            
    } else {
        Log("trans error")
        return false
    }
}

sunSwapEx.SendTRX = function(to, amount) {
    let e = sunSwapEx.e
    return e.IO("api", "tron", "send", to, toInnerAmount(amount, 6))
}

sunSwapEx.SetFeeLimit = function(feeLimit) {
    sunSwapEx.feeLimit = feeLimit
    Log("SetFeeLimit, feeLimit:", sunSwapEx.feeLimit, ", toInnerAmount:", toInnerAmount(sunSwapEx.feeLimit, 6))        
}

// init
if (typeof(e) == "undefined") {
    e = exchange
    Log("默认使用exchange配置")
}
sunSwapEx.e = e

// tron
let ret = e.IO("api", "tron", "GetNodeInfo")
if (!ret) {
    throw "当前Web3 tron交易所对象可能配置有误"
} else {
    Log("node节点信息:", ret)
}

// feeLimit
sunSwapEx.feeLimit = 50

// register abi
let factoryV3Address = "TThJt8zaJzJMhCEScH7zWKnp5buVZqys9x"
sunSwapEx.factoryV3Address = factoryV3Address

let poolABI = `[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"indexed":true,"name":"owner","internalType":"address","type":"address"},{"indexed":true,"name":"tickLower","internalType":"int24","type":"int24"},{"indexed":true,"name":"tickUpper","internalType":"int24","type":"int24"},{"indexed":false,"name":"amount","internalType":"uint128","type":"uint128"},{"indexed":false,"name":"amount0","internalType":"uint256","type":"uint256"},{"indexed":false,"name":"amount1","internalType":"uint256","type":"uint256"}],"name":"Burn","anonymous":false,"type":"event"},{"inputs":[{"indexed":true,"name":"owner","internalType":"address","type":"address"},{"indexed":false,"name":"recipient","internalType":"address","type":"address"},{"indexed":true,"name":"tickLower","internalType":"int24","type":"int24"},{"indexed":true,"name":"tickUpper","internalType":"int24","type":"int24"},{"indexed":false,"name":"amount0","internalType":"uint128","type":"uint128"},{"indexed":false,"name":"amount1","internalType":"uint128","type":"uint128"}],"name":"Collect","anonymous":false,"type":"event"},{"inputs":[{"indexed":true,"name":"sender","internalType":"address","type":"address"},{"indexed":true,"name":"recipient","internalType":"address","type":"address"},{"indexed":false,"name":"amount0","internalType":"uint128","type":"uint128"},{"indexed":false,"name":"amount1","internalType":"uint128","type":"uint128"}],"name":"CollectProtocol","anonymous":false,"type":"event"},{"inputs":[{"indexed":true,"name":"sender","internalType":"address","type":"address"},{"indexed":true,"name":"recipient","internalType":"address","type":"address"},{"indexed":false,"name":"amount0","internalType":"uint256","type":"uint256"},{"indexed":false,"name":"amount1","internalType":"uint256","type":"uint256"},{"indexed":false,"name":"paid0","internalType":"uint256","type":"uint256"},{"indexed":false,"name":"paid1","internalType":"uint256","type":"uint256"}],"name":"Flash","anonymous":false,"type":"event"},{"inputs":[{"indexed":false,"name":"observationCardinalityNextOld","internalType":"uint16","type":"uint16"},{"indexed":false,"name":"observationCardinalityNextNew","internalType":"uint16","type":"uint16"}],"name":"IncreaseObservationCardinalityNext","anonymous":false,"type":"event"},{"inputs":[{"indexed":false,"name":"sqrtPriceX96","internalType":"uint160","type":"uint160"},{"indexed":false,"name":"tick","internalType":"int24","type":"int24"}],"name":"Initialize","anonymous":false,"type":"event"},{"inputs":[{"indexed":false,"name":"sender","internalType":"address","type":"address"},{"indexed":true,"name":"owner","internalType":"address","type":"address"},{"indexed":true,"name":"tickLower","internalType":"int24","type":"int24"},{"indexed":true,"name":"tickUpper","internalType":"int24","type":"int24"},{"indexed":false,"name":"amount","internalType":"uint128","type":"uint128"},{"indexed":false,"name":"amount0","internalType":"uint256","type":"uint256"},{"indexed":false,"name":"amount1","internalType":"uint256","type":"uint256"}],"name":"Mint","anonymous":false,"type":"event"},{"inputs":[{"indexed":false,"name":"feeProtocol0Old","internalType":"uint8","type":"uint8"},{"indexed":false,"name":"feeProtocol1Old","internalType":"uint8","type":"uint8"},{"indexed":false,"name":"feeProtocol0New","internalType":"uint8","type":"uint8"},{"indexed":false,"name":"feeProtocol1New","internalType":"uint8","type":"uint8"}],"name":"SetFeeProtocol","anonymous":false,"type":"event"},{"inputs":[{"indexed":true,"name":"sender","internalType":"address","type":"address"},{"indexed":true,"name":"recipient","internalType":"address","type":"address"},{"indexed":false,"name":"amount0","internalType":"int256","type":"int256"},{"indexed":false,"name":"amount1","internalType":"int256","type":"int256"},{"indexed":false,"name":"sqrtPriceX96","internalType":"uint160","type":"uint160"},{"indexed":false,"name":"liquidity","internalType":"uint128","type":"uint128"},{"indexed":false,"name":"tick","internalType":"int24","type":"int24"}],"name":"Swap","anonymous":false,"type":"event"},{"outputs":[{"name":"","internalType":"address","type":"address"}],"inputs":[],"name":"factory","stateMutability":"view","type":"function"},{"outputs":[{"name":"","internalType":"uint24","type":"uint24"}],"inputs":[],"name":"fee","stateMutability":"view","type":"function"},{"outputs":[{"name":"","internalType":"uint256","type":"uint256"}],"inputs":[],"name":"feeGrowthGlobal0X128","stateMutability":"view","type":"function"},{"outputs":[{"name":"","internalType":"uint256","type":"uint256"}],"inputs":[],"name":"feeGrowthGlobal1X128","stateMutability":"view","type":"function"},{"outputs":[{"name":"","internalType":"uint128","type":"uint128"}],"inputs":[],"name":"liquidity","stateMutability":"view","type":"function"},{"outputs":[{"name":"","internalType":"uint128","type":"uint128"}],"inputs":[],"name":"maxLiquidityPerTick","stateMutability":"view","type":"function"},{"outputs":[{"name":"blockTimestamp","internalType":"uint32","type":"uint32"},{"name":"tickCumulative","internalType":"int56","type":"int56"},{"name":"secondsPerLiquidityCumulativeX128","internalType":"uint160","type":"uint160"},{"name":"initialized","internalType":"bool","type":"bool"}],"inputs":[{"name":"","internalType":"uint256","type":"uint256"}],"name":"observations","stateMutability":"view","type":"function"},{"outputs":[{"name":"liquidity","internalType":"uint128","type":"uint128"},{"name":"feeGrowthInside0LastX128","internalType":"uint256","type":"uint256"},{"name":"feeGrowthInside1LastX128","internalType":"uint256","type":"uint256"},{"name":"tokensOwed0","internalType":"uint128","type":"uint128"},{"name":"tokensOwed1","internalType":"uint128","type":"uint128"}],"inputs":[{"name":"","internalType":"bytes32","type":"bytes32"}],"name":"positions","stateMutability":"view","type":"function"},{"outputs":[{"name":"token0","internalType":"uint128","type":"uint128"},{"name":"token1","internalType":"uint128","type":"uint128"}],"inputs":[],"name":"protocolFees","stateMutability":"view","type":"function"},{"outputs":[{"name":"sqrtPriceX96","internalType":"uint160","type":"uint160"},{"name":"tick","internalType":"int24","type":"int24"},{"name":"observationIndex","internalType":"uint16","type":"uint16"},{"name":"observationCardinality","internalType":"uint16","type":"uint16"},{"name":"observationCardinalityNext","internalType":"uint16","type":"uint16"},{"name":"feeProtocol","internalType":"uint8","type":"uint8"},{"name":"unlocked","internalType":"bool","type":"bool"}],"inputs":[],"name":"slot0","stateMutability":"view","type":"function"},{"outputs":[{"name":"","internalType":"uint256","type":"uint256"}],"inputs":[{"name":"","internalType":"int16","type":"int16"}],"name":"tickBitmap","stateMutability":"view","type":"function"},{"outputs":[{"name":"","internalType":"int24","type":"int24"}],"inputs":[],"name":"tickSpacing","stateMutability":"view","type":"function"},{"outputs":[{"name":"liquidityGross","internalType":"uint128","type":"uint128"},{"name":"liquidityNet","internalType":"int128","type":"int128"},{"name":"feeGrowthOutside0X128","internalType":"uint256","type":"uint256"},{"name":"feeGrowthOutside1X128","internalType":"uint256","type":"uint256"},{"name":"tickCumulativeOutside","internalType":"int56","type":"int56"},{"name":"secondsPerLiquidityOutsideX128","internalType":"uint160","type":"uint160"},{"name":"secondsOutside","internalType":"uint32","type":"uint32"},{"name":"initialized","internalType":"bool","type":"bool"}],"inputs":[{"name":"","internalType":"int24","type":"int24"}],"name":"ticks","stateMutability":"view","type":"function"},{"outputs":[{"name":"","internalType":"address","type":"address"}],"inputs":[],"name":"token0","stateMutability":"view","type":"function"},{"outputs":[{"name":"","internalType":"address","type":"address"}],"inputs":[],"name":"token1","stateMutability":"view","type":"function"},{"outputs":[{"name":"tickCumulativeInside","internalType":"int56","type":"int56"},{"name":"secondsPerLiquidityInsideX128","internalType":"uint160","type":"uint160"},{"name":"secondsInside","internalType":"uint32","type":"uint32"}],"inputs":[{"name":"tickLower","internalType":"int24","type":"int24"},{"name":"tickUpper","internalType":"int24","type":"int24"}],"name":"snapshotCumulativesInside","stateMutability":"view","type":"function"},{"outputs":[{"name":"tickCumulatives","internalType":"int56[]","type":"int56[]"},{"name":"secondsPerLiquidityCumulativeX128s","internalType":"uint160[]","type":"uint160[]"}],"inputs":[{"name":"secondsAgos","internalType":"uint32[]","type":"uint32[]"}],"name":"observe","stateMutability":"view","type":"function"},{"outputs":[],"inputs":[{"name":"obse