In the field of blockchain tradings, decentralized exchanges (DEX) provide a trustless way to exchange assets, and SunSwap, as a representative DEX in the Tron ecosystem, supports smart contract interactions based on Web3. We FMZ platform provides a powerful quantitative trading framework that enables developers to integrate multiple trading strategies.
The FMZ platform already supports the encapsulation of Web3 Ethereum (ETH) exchange objects, and added support for Web3 TRON recently, encapsulating the exchange objects of the TRON chain. Just like accessing UniSwap DEX, access to SunSwap DEX is also possible.
This article will explore how to encapsulate the Web3 interface based on the FMZ Quant platform and access SunSwap DEX for trading. We will introduce the Web3 interaction method of the Tron network, the SunSwap smart contract call, and show how to query liquidity, execute transactions, and obtain transaction results through code examples, so as to build an efficient quantitative trading strategy.
All codes and designs shared in this article have been tested and are intended for learning and communication. However, even after testing, these contents may still need to be adjusted according to actual needs. If you want to apply them to live trading, please be sure to evaluate, optimize and modify them yourself to ensure that they meet your needs.
In the previous article FMZ Quant Web3 Expansion: Tron Support Added, Expand On-chain Tradings, we have introduced the common methods of Tron gRPC nodes and learned how to configure Web3 TRON exchange objects on FMZ. This article will not repeat these contents. As for the node address, we will use the node provided by TRON official directly.
https://youtu.be/xpfaimePdHc?si=wVASaGguaaxNwqkS
Unlike CEX exchanges, there is no wall to worry about. You can access the official node by deploying a docker locally.
Open the front-end page of SunSwap in the browser, you can connect to the wallet and observe the data conveniently.
Essential tools for viewing and analyzing data.
The address on TRON is base58 encoded, which is different from Ethereum. Sometimes, in order to view and compare data, some conversion is required. On FMZ, you can use the base58 encoded address directly. Some smart contracts return data in HEX encoding, which requires some conversion. Several functions are simply implemented to convert the address format:
The following are the main conversion functions. Due to limited space, I will not post all the codes. The complete code is in the “Tron SunSwap V3 Trading Library” template library disclosed at the end of the article.
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)
}
Because sometimes you need to read data from multiple methods at once, calling them one by one is time-consuming and laborious. So a packaged calling function is encapsulated:
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
}
The design of SunSwap V3 trading library is modeled after the UniSwap trading library template of the FMZ platform. Since the SunSwap template code is long, I will not post it here, and will mainly explain the functions implemented.
let e = $.CreateSunSwapEx() // By default, the exchange exchanges[0] is used to initialize the SunSwap trading object.
// let e = $.CreateSunSwapEx(exchanges[1]) // Initialize the SunSwap trading object using the secondary exchange object.
This template has only one interface function, namely: $.CreateSunSwapEx()
, which is used to create a SunSwap trading object. After the object is created, you can call the method of this object to perform some function calls, such as: exchange transactions, query pool prices, obtain all V3 pool information, pledge, decompression, etc.
The test codes of “Tron SunSwap V3 Trading Library” are all recorded in the main()
function of the template and will not be repeated here.
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) + "`")
The function GetMarkets()
is used to obtain the relevant information of all exchange pools in the SunSwap V3 pool, cached in the variables of the SunSwap trading object, and used to query accuracy, address and other information during other operations.
After running the test code, the information of all V3 pools will be displayed:
The code has removed some of the contract addresses currently seen, such as “phishing contracts” and “invalid contracts”.
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)
You can use the GetTicker()
function to query the price of a certain exchange pool, and you can specify the address of a specific pool. Note that some pools have poor liquidity and the price is only for reference.
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))
This function can be used for subsequent function encapsulation. For example, in the example, two balanceOf
method calls can be encoded and the data can be requested once.
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))
This is an important function, which encapsulates SunSwap’s intelligent routing service. Given tokenInSymbol and tokenOutSymbol, it can return possible exchange paths, the number of output tokens and other information.
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) + "`")
Since there is no good mechanism to traverse TRC20 tokens, with the help of tronscan’s function, this third-party interface is used to query account asset information.
// 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)
The most important function is this exchange. When executing the exchange, the exchange path will be formulated according to the smart routing first, and then the exchange method will be called to exchange the token. It should be noted that if it is a TRX package/unpackage exchange, it is only executed using the WTRX contract method.
We perform the following tests:
let ret = e.Swap("TRX_USDT", 5, "sell")
Log(ret)
There are some other functions such as “staking” and “unstaking”. You can check them in the main()
function in the template code, so I won’t go into details.
You can also expand and add functions such as adding liquidity and removing liquidity.
This template does not have any interaction design and interface parameters. It only implements the basic function of accessing SunSwap DEX. More functions can be optimized and added later.
”`javascript 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", // Attention: Fishing contract
"TXioUZK8ju3R54KvzSqPc6Ufq5wkxFqpq9": "U S D T", // Attention: Fishing contract
"TU41FWQQT8hZ6t72gVEjtSGn1Dshbevi7g": "CNY Coin", // Attention: Invalid
"TYB7oXNq4VyuiH5BHb4os8rTh7Ca8pxqSx": "BULL", // Attention: Fishing contract
"TYJxozCVMUiHg3TbQp9PKeuXdTsX9ugJz9": "SHISHA", // Attention: Not circulated
"TF9io9LGyjuK3uTpr73pAaQ5m9scxd9xvr": "TestToken18", // Attention: Test trading pairs
"TK8E3sFhBt3EB6gTT6d6co8RMB6DFUnNwE": "TestToken6" // Attention: Test trading pairs
}
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("By default, the exchange configuration is used")
}
sunSwapEx.e = e
// tron
let ret = e.IO("api", "tron", "GetNodeInfo")
if (!ret) {
throw "The current Web3 tron exchange object may be misconfigured"
} else {
Log("node information:", 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":"observationCardinalityNext","internalType":"uint16","type":"uint16"}],"name":"increaseObservationCardinalityNext","stateMutability":"nonpayable","type":"function"},{"outputs":[],"inputs":[{"name":"sqrtPriceX96","internalType":"uint160","type":"uint160"}],"name":"initialize","stateMutability":"nonpayable","type":"function"},{"outputs":[{"name":"amount0","internalType":"uint256","type":"uint256"},{"name":"amount1","internalType":"uint256","type":"uint256"}],"inputs":[{"name":"recipient","internalType":"address","type":"address"},{"name":"tickLower","internalType":"int24","type":"int24"},{"name":"tickUpper","internalType":"int24","type":"int24"},{"name":"amount","internalType":"uint128","type":"uint128"},{"name":"data","internalType":"bytes","type":"bytes"}],"name":"mint","stateMutability":"nonpayable","type":"function"},{"outputs":[{"name":"amount0","internalType":"uint128","type":"uint128"},{"name":"amount1","internalType":"uint128","type":"uint128"}],"inputs":[{"name":"recipient","internalType":"address","type":"address"},{"name":"tickLower","internalType":"int24","type":"int24"},{"name":"tickUpper","internalType":"int24","type":"int24"},{"name":"amount0Requested","internalType":"uint128","type":"uint128"},{"name":"amount1Requested","internalType":"uint128","type":"uint128"}],"name":"collect","stateMutability":"nonpayable","type":"function"},{"outputs":[{"name":"amount0","internalType":"uint256","type":"uint256"},{"name":"amount1","internalType":"uint256","type":"uint256"}],"inputs":[{"name":"tickLower","internalType":"int24","type":"int24"},{"name":"tickUpper","internalType":"int24","type":"int24"},{"name":"amount","internalType":"uint128","type":"uint128"}],"name":"burn","stateMutability":"nonpayable","type":"function"},{"outputs":[{"name":"amount0","internalType":"int256","type":"int256"},{"name":"amount1","internalType":"int256","type":"int256"}],"inputs":[{"name":"recipient","internalType":"address","type":"address"},{"name":"zeroForOne","internalType":"bool","type":"bool"},{"name":"amountSpecified","internalType":"int256","type":"int256"},{"name":"sqrtPriceLimitX96","internalType":"uint160","type":"uint160"},{"name":"data","internalType":"bytes","type":"bytes"}],"name":"swap","stateMutability":"nonpayable","type":"function"},{"outputs":[],"inputs":[{"name":"recipient","internalType":"address","type":"address"},{"name":"amount0","internalType":"uint256","type":"uint256"},{"name":"amount1","internalType":"uint256","type":"uint256"},{"name":"data","internalType":"bytes","type":"bytes"}],"name":"flash","stateMutability":"nonpayable","type":"function"},{"outputs":[],"inputs":[{"name":"feeProtocol0","internalType":"uint8","type":"uint8"},{"name":"feeProtocol1","internalType":"uint8","type":"uint8"}],"name":"setFeeProtocol","stateMutability":"nonpayable","type":"function"},{"outputs":[{"name":"amount0","internalType":"uint128","type":"uint128"},{"name":"amount1","internalType":"uint128","type":"uint128"}],"inputs":[{"name":"recipient","internalType":"address","type":"address"},{"name":"amount0Requested","internalType":"uint128","type":"uint128"},{"name":"amount1Requested","internalType":"uint128","type":"uint128"}],"name":"collectProtocol","stateMutability":"nonpayable","type":"function"}]`
sunSwapEx.poolABI = poolABI
let multicallContr