diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cf9b2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +config.yaml \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..c5e0fd6 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,6 @@ +## Alpha-Beta 0.0.0v +This is the initial version of the project. However, some progress has already been made, including: +- Research on STonFi contracts on the TON blockchain. +- Research on UniSwap contracts on Ethereum. +- Development of an initial version of the API server (which will need to be rewritten). +Some mathematical aspects may need to be reviewed, particularly due to the challenges of working with integer operations, which has been a bit of a pain point. Future versions are planned to include support for additional protocols, possibly starting with one on Solana and later adding support for Tron to increase diversity. diff --git a/LICENSE b/LICENSE index dbf37f2..f85d953 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 JZethar +Copyright (c) 2024 Yulian Volianskyi aka jzethar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index cf0635c..fd951e7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,87 @@ -# DexPriceAPI +# Dex Price API An API to retrieve prices directly from DEXs. + +## What is it? +This project was initiated as an Open Source token price API. Its primary goal is to provide free access to token prices across various DEXs (Decentralized Exchanges). The API is designed to be particularly useful for companies whose products rely on token prices, such as wallets and other Web3 applications. All prices accessible through this API are publicly available and stored within DEX contracts on public blockchains. + +## Supported protocols +Now are supported only 2 protocols: +- [STonFi](https://ston.fi/) +- [UniSwap](https://uniswap.org) + +## Supported chains +- Ethereum +- TON + +## Run docker +To run docker compose just: + +`docker compose up --build -d` + +Before using docker compose be sure that you have your ip correct in your config file. Also check ports and config path. + +## Config file +Config file now contains only 2 params: +- ip +- port + + +## Examples +On request: +```sh +curl --location 'http://localhost:15001/ton' \ +--header 'Content-Type: application/json' \ +--data '{ + "method": "TONDex.GetPoolPrice", + "params": [ + { + "pool":"EQD8TJ8xEWB1SpnRE4d89YO3jl0W0EiBnNS4IBaHaUmdfizE" + } + ], + "id": "1" +}' +``` +You will se the response: +```json +{ + "result": { + "token0": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", + "token1": "0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c", + "price_of_token0": "149599453", + "price_of_token1": "6684516415", + "decimals": "9" + }, + "error": null, + "id": "1" +} +``` +And the second type of request: +```sh +curl --location 'http://localhost:15001/ton' \ +--header 'Content-Type: application/json' \ +--data '{ + "method": "TONDex.GetTokensPrices", + "params": [ + { + "token0" : "EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT", + "token1" : "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" + } + ], + "id": "1" +}' +``` +Response: +```json +{ + "result": { + "token0": "0:2f956143c461769579baef2e32cc2d7bc18283f40d20bb03e432cd603ac33ffc", + "token1": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", + "price_of_token0": "12590087", + "price_of_token1": "79427567228", + "decimals": "9" + }, + "error": null, + "id": "1" +} +``` +Be aware that prices are changing all the time and this is only example. \ No newline at end of file diff --git a/common/tokenPrices.go b/common/tokenPrices.go new file mode 100644 index 0000000..f2d2544 --- /dev/null +++ b/common/tokenPrices.go @@ -0,0 +1,13 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package common + +type TokenPrice struct { + Token0 string `json:"token0,omitempty"` + Token1 string `json:"token1,omitempty"` + PriceT0 string `json:"price_of_token0,omitempty"` + PriceT1 string `json:"price_of_token1,omitempty"` + Decimals string `json:"decimals,omitempty"` +} diff --git a/common/uniSlot0.go b/common/uniSlot0.go new file mode 100644 index 0000000..c15eb6f --- /dev/null +++ b/common/uniSlot0.go @@ -0,0 +1,63 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package common + +import ( + "math/big" + "strconv" +) + +// Golang doesn’t check for overflows implicitly and +// so this may lead to unexpected results when a number +// larger than 64 bits are stored in a int64. +type UniSlot0 struct { + SqrtPriceX96 big.Int // uint160 + tick int32 + observationIndex uint16 + observationCardinality uint16 + observationCardinalityNext uint16 + feeProtocol uint8 + unlocked bool +} + +func (uns *UniSlot0) hex2int(data string, bitsize int) int64 { + result, _ := strconv.ParseInt(data, 16, bitsize) + return result +} + +func (uns *UniSlot0) Unmarshal(data string) error { + data = data[2:] + + SqrtPriceX96 := data[:64] + uns.SqrtPriceX96.SetString(SqrtPriceX96, 16) + data = data[64:] + + tick := data[:64] + uns.tick = int32(uns.hex2int(tick, 32)) + data = data[64:] + + observationIndex := data[:64] + uns.observationIndex = uint16(uns.hex2int(observationIndex, 16)) + data = data[64:] + + observationCardinality := data[:64] + uns.observationCardinality = uint16(uns.hex2int(observationCardinality, 16)) + data = data[64:] + + observationCardinalityNext := data[:64] + uns.observationCardinalityNext = uint16(uns.hex2int(observationCardinalityNext, 16)) + data = data[64:] + + feeProtocol := data[:64] + uns.feeProtocol = uint8(uns.hex2int(feeProtocol, 8)) + data = data[64:] + + if string(data[len(data)-1]) == "1" { + uns.unlocked = true + } else { + uns.unlocked = false + } + return nil +} diff --git a/config_example.yaml b/config_example.yaml new file mode 100644 index 0000000..9b11ba5 --- /dev/null +++ b/config_example.yaml @@ -0,0 +1,3 @@ +server: + ip: 0.0.0.0 + port: 15001 \ No newline at end of file diff --git a/dex_prices.Dockerfile b/dex_prices.Dockerfile new file mode 100644 index 0000000..f1d5cfd --- /dev/null +++ b/dex_prices.Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.21 AS builder +WORKDIR /app + +COPY . . +RUN go mod download +RUN go mod tidy + +WORKDIR /app/server +RUN CGO_ENABLED=0 GOOS=linux go build -o main + +FROM alpine:latest +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ +COPY --from=builder /app/server/main . diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..13b4cda --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,43 @@ +version: "3.7" + +x-logging: &logging + logging: + driver: json-file + options: + max-size: 5m + max-file: "2" + +volumes: + dex_prices: + driver: local + driver_opts: + type: none + device: /var/lib/dex_prices # TODO rename before git + o: bind + + +services: + dex_api: + build: + context: . + dockerfile: dex_prices.Dockerfile + restart: on-failure + stop_grace_period: 1m + deploy: + resources: + limits: + memory: 2G + cpus: "2" + <<: *logging + ports: + - 127.0.0.1:15001:15001 # API + # TODO: add swagger + networks: + - dex_prices_net + volumes: + - dex_prices:/home/dex_prices + - ./config.yaml:/home/dex_prices/config.yaml + command: ["./main", "-c", "/home/dex_prices/"] + +networks: + dex_prices_net: diff --git a/eth/uniswap/univ3.go b/eth/uniswap/univ3.go new file mode 100644 index 0000000..1627368 --- /dev/null +++ b/eth/uniswap/univ3.go @@ -0,0 +1,208 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package eth + +import ( + "errors" + "fmt" + "log" + "math" + "math/big" + "strconv" + + ecommon "dexprices.io/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rpc" + "github.com/holiman/uint256" +) + +type ContractParams struct { + To string `json:"to,omitempty"` + Data string `json:"data,omitempty"` +} + +const ( + two96Str = "79228162514264337593543950336" + ethCall = "eth_call" + slot0Str = "slot0()" + depthStr = "1000000000000" + uniV3FactoryContract = "0x1F98431c8aD98523631AE4a59f267346ea31F984" + depth = 9 +) + +// P = y / x (Price of X in terms of Y) +// sqrtPriceX96 = sqrt(P) * 2^96 +// 1eth = 1 / P = 1 / (sqrtPriceX96 / 2^96)^2 -- then calculate the 10^18 / 10^6 (for usdc) +// X = token0 +// Y = token1 + +type UniV3 struct { + Version string + client *rpc.Client + Error string +} + +func (uni *UniV3) Init(rpcProvider string) error { + var err error + uni.client, err = rpc.DialHTTP(rpcProvider) + if err != nil { + return errors.New("could not connect to the node") + } + defer uni.client.Close() + return nil +} + +func (uni *UniV3) GetPrice(token0 string, token1 string, block string, tokenInfo *ecommon.TokenPrice) error { + pool, err := uni.getPool(token0, token1, block) + if err != nil { + return err + } + if err := uni.GetPoolPrice(pool, block, tokenInfo); err != nil { + return err + } + return nil +} + +func (uni *UniV3) GetPoolPrice(poolContract string, block string, tokenInfo *ecommon.TokenPrice) error { + var uniSlot0 ecommon.UniSlot0 + var token0, token1 string + var err error + slot0 := []byte(slot0Str) + hashSlot0 := crypto.Keccak256Hash(slot0) // don't forget to take 10 symbols, because 0x + + var result string + req := ContractParams{poolContract, hashSlot0.String()[:10]} + if err := uni.client.Call(&result, ethCall, req, block); err != nil { + return err + } + + uniSlot0.Unmarshal(result) + token0, token1, err = uni.getTokensFromPool(poolContract, block) // evn if we have tokens set -- we need to ask a contract 4 order + if err != nil { + return err + } + tokenInfo.Token0 = token0 + tokenInfo.Token1 = token1 + + decimals0, err := uni.getTokenDecimals(tokenInfo.Token0, block) + if err != nil { + return err + } + decimals1, err := uni.getTokenDecimals(tokenInfo.Token1, block) + if err != nil { + return err + } + + uni.getPrices(tokenInfo, decimals0, decimals1, uniSlot0) + return nil +} + +func (uni *UniV3) getPool(token0 string, token1 string, block string) (string, error) { + var pool string + token0 = token0[2:] + token1 = token1[2:] + token0Padded := fmt.Sprintf("%064s", token0) + token1Padded := fmt.Sprintf("%064s", token1) + feePadded := fmt.Sprintf("%064s", strconv.FormatInt(3000, 16)) + hashDecimals0 := crypto.Keccak256Hash([]byte("getPool(address,address,uint24)")) + hashRequest := hashDecimals0.String()[:10] + token0Padded + token1Padded + feePadded + + reqPool := ContractParams{uniV3FactoryContract, hashRequest} + if err := uni.client.Call(&pool, ethCall, reqPool, block); err != nil { + return "", err + } + + return "0x" + pool[26:], nil +} + +// OK +func (uni *UniV3) getTokenDecimals(token string, block string) (uint8, error) { + var decimalsUint string + decimals := []byte("decimals()") + hashDecimals0 := crypto.Keccak256Hash(decimals) // don't forget to take 10 symbols, because 0x + + reqDecimals := ContractParams{token, hashDecimals0.String()[:10]} + if err := uni.client.Call(&decimalsUint, ethCall, reqDecimals, block); err != nil { + return 0, err + } + + decimalsUint = decimalsUint[2:] + return uint8(uni.hex2int(decimalsUint, 8)), nil +} + +// OK +func (uni *UniV3) getTokensFromPool(pool string, block string) (string, string, error) { + var resultToken0 string + var resultToken1 string + token0 := []byte("token0()") + token1 := []byte("token1()") + hashToken0 := crypto.Keccak256Hash(token0) // don't forget to take 10 symbols, because 0x + hashToken1 := crypto.Keccak256Hash(token1) // don't forget to take 10 symbols, because 0x + + reqToken0 := ContractParams{pool, hashToken0.String()[:10]} + if err := uni.client.Call(&resultToken0, ethCall, reqToken0, block); err != nil { + return "0x", "0x", err + } + + reqToken1 := ContractParams{pool, hashToken1.String()[:10]} + if err := uni.client.Call(&resultToken1, ethCall, reqToken1, block); err != nil { + return "0x", "0x", err + } + + return "0x" + resultToken0[26:], "0x" + resultToken1[26:], nil +} + +// OK +func (uni UniV3) hex2int(data string, bitsize int) int64 { + result, _ := strconv.ParseInt(data, 16, bitsize) + return result +} + +func (uni *UniV3) getPrices(tokenInfo *ecommon.TokenPrice, decimals0 uint8, decimals1 uint8, uniSlot0 ecommon.UniSlot0) { + var X uint256.Int + var P uint256.Int + var depthStr uint256.Int + var d0d1 uint64 + var newDepth uint64 + var divSqrtTwo96 uint256.Int + two96BI, _ := new(big.Int).SetString(two96Str, 10) + two96, _ := uint256.FromBig(two96BI) + depthStr.Exp(uint256.NewInt(10), uint256.NewInt(depth)) + // WE NEED TO CMP two96 and sqrtPriceX96 + // WE NEED DEPTH due to the dividing 1 to (A) we have to provide depth that gonna be 10^9 (I think it should be enough) to calculate properly it + // But + // Maybe even it could be set by user + // BUT now let use 10^9 + // SHIT + + // ((10^dp / (SqrtPriceX96 / two96))^2) * 10^(d1-d0) + log.Print("SqrtPriceX96: " + uniSlot0.SqrtPriceX96.String()) + log.Print("Dec0: " + strconv.FormatUint(uint64(decimals0), 10)) + log.Print("Dec1: " + strconv.FormatUint(uint64(decimals1), 10)) + if len(uniSlot0.SqrtPriceX96.String())-len(two96Str) < 8 { + diff := int64(len(uniSlot0.SqrtPriceX96.String()) - len(two96Str)) + uniSlot0.SqrtPriceX96.Mul(&uniSlot0.SqrtPriceX96, + new(big.Int).Exp(big.NewInt(10), big.NewInt(diff), nil)) + newDepth = uint64(int64(depth) + diff) + depthStr.Exp(uint256.NewInt(10), uint256.NewInt(newDepth)) + } + if int8(decimals1)-int8(decimals0) < 0 { + d0d1 = uint64(math.Abs(float64(decimals1) - float64(decimals0))) + } // TODO errors d0d1 is not initialized + divSqrtTwo96.Div(uint256.MustFromBig(&uniSlot0.SqrtPriceX96), two96) + X.Mul( + X.Exp(X.Div(&depthStr, + &divSqrtTwo96), uint256.NewInt(2)), + new(uint256.Int).Exp(uint256.NewInt(10), uint256.NewInt(d0d1))) + log.Print(X.String()) + + // (SqrtPriceX96 / two96) ^ 2 + P.Mul(P.Exp(&divSqrtTwo96, uint256.NewInt(2)), + new(uint256.Int).Exp(uint256.NewInt(10), uint256.NewInt(uint64((newDepth+newDepth)-d0d1)))) + // AFTER ALL OPERATIONS we have depth ^ 2 or 10^18 be AWARE!!! + tokenInfo.Decimals = strconv.FormatInt(int64(newDepth+newDepth), 10) + tokenInfo.PriceT1 = X.String() + tokenInfo.PriceT0 = P.String() +} diff --git a/eth/uniswap/univ3_test.go b/eth/uniswap/univ3_test.go new file mode 100644 index 0000000..9344103 --- /dev/null +++ b/eth/uniswap/univ3_test.go @@ -0,0 +1,73 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package eth + +import ( + "strings" + "testing" + + ecommon "dexprices.io/common" +) + +func TestGetPoolPrice(t *testing.T) { + var uni UniV3 + uni.Init("https://eth.llamarpc.com") + got := uni.GetPoolPrice("0x99ac8cA7087fA4A2A1FB6357269965A2014ABc35", "latest", &ecommon.TokenPrice{}) + + if got != nil { + t.Errorf("got %q, wanted nil", got) + } +} + +func TestGetTokensFromPool(t *testing.T) { + var uni UniV3 + uni.Init("https://eth.llamarpc.com") + t0, t1, err := uni.getTokensFromPool("0x99ac8cA7087fA4A2A1FB6357269965A2014ABc35", "latest") + + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + if t0 != "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" { + t.Errorf("got %q, wanted 0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", t0) + } + if t1 != "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" { + t.Errorf("got %q, wanted ", t1) + } +} + +func TestGetTokenDecimals(t *testing.T) { + var uni UniV3 + uni.Init("https://eth.llamarpc.com") + d0, err := uni.getTokenDecimals("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "latest") + + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + if d0 != 8 { + t.Errorf("got %d, wanted 8", d0) + } +} + +func TestHex2Int(t *testing.T) { + var uni UniV3 + d0 := uni.hex2int("0000000000000000000000000000000000000000000000000000000000000008", 8) + + if d0 != 8 { + t.Errorf("got %d, wanted 8", d0) + } +} + +func TestGetPool(t *testing.T) { + var uni UniV3 + uni.Init("https://eth.llamarpc.com") + pool, err := uni.getPool("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "latest") + + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + if pool != strings.ToLower("0x99ac8cA7087fA4A2A1FB6357269965A2014ABc35") { + t.Errorf("got %q, wanted 0x99ac8cA7087fA4A2A1FB6357269965A2014ABc35", pool) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..16dd9ff --- /dev/null +++ b/go.mod @@ -0,0 +1,50 @@ +module dexprices.io + +go 1.21 + +toolchain go1.22.5 + +require ( + github.com/ethereum/go-ethereum v1.14.7 + github.com/gorilla/mux v1.8.1 + github.com/gorilla/rpc v1.2.1 +) + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/holiman/uint256 v1.3.0 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/snksoft/crc v1.1.0 // indirect + github.com/spf13/viper v1.19.0 + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tonkeeper/tongo v1.9.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..085711c --- /dev/null +++ b/go.sum @@ -0,0 +1,116 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= +github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= +github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.14.7 h1:EHpv3dE8evQmpVEQ/Ne2ahB06n2mQptdwqaMNhAT29g= +github.com/ethereum/go-ethereum v1.14.7/go.mod h1:Mq0biU2jbdmKSZoqOj29017ygFrMnB5/Rifwp980W4o= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k= +github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/holiman/uint256 v1.3.0 h1:4wdcm/tnd0xXdu7iS3ruNvxkWwrb4aeBQv19ayYn8F4= +github.com/holiman/uint256 v1.3.0/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae h1:7smdlrfdcZic4VfsGKD2ulWL804a4GVphr4s7WZxGiY= +github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/snksoft/crc v1.1.0 h1:HkLdI4taFlgGGG1KvsWMpz78PkOC9TkPVpTV/cuWn48= +github.com/snksoft/crc v1.1.0/go.mod h1:5/gUOsgAm7OmIhb6WJzw7w5g2zfJi4FrHYgGPdshE+A= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tonkeeper/tongo v1.9.0 h1:yWPc13byc341mnKOBbPkBzGs9GxGdZ+ugMIBg+Q2pNk= +github.com/tonkeeper/tongo v1.9.0/go.mod h1:MjgIgAytFarjCoVjMLjYEtpZNN1f2G/pnZhKjr28cWs= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..713b803 --- /dev/null +++ b/server/server.go @@ -0,0 +1,46 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package main + +import ( + "flag" + "fmt" + "net/http" + + servers "dexprices.io/server/servers" + "github.com/gorilla/mux" + "github.com/gorilla/rpc" + "github.com/gorilla/rpc/json" + "github.com/spf13/viper" +) + +// TODO interfaces +// + +func main() { + var config string + var server_ip, server_port string + + flag.StringVar(&config, "c", "/home/dexprices", "Enter your config") + flag.Parse() + viper.SetConfigType("yaml") + viper.AddConfigPath(config) + err := viper.ReadInConfig() + if err != nil { + panic(fmt.Errorf("fatal error config file: %w", err)) + } + server_ip = viper.GetString("server.ip") + server_port = viper.GetString("server.port") + s := rpc.NewServer() + s.RegisterCodec(json.NewCodec(), "application/json") + s.RegisterService(new(servers.Dex), "") + t := rpc.NewServer() + t.RegisterCodec(json.NewCodec(), "application/json") + t.RegisterService(new(servers.TONDex), "") + r := mux.NewRouter() + r.Handle("/dex", s) + r.Handle("/ton", t) + http.ListenAndServe(server_ip+":"+server_port, r) +} diff --git a/server/servers/args.go b/server/servers/args.go new file mode 100644 index 0000000..5a6a455 --- /dev/null +++ b/server/servers/args.go @@ -0,0 +1,13 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package servers + +// Args holds arguments passed to JSON RPC service +type Args struct { + Pool string `json:"pool,omitempty"` + Token0 string `json:"token0,omitempty"` + Token1 string `json:"token1,omitempty"` + Block string `json:"block,omitempty"` +} diff --git a/server/servers/sTonFiTVM.go b/server/servers/sTonFiTVM.go new file mode 100644 index 0000000..99fbd5c --- /dev/null +++ b/server/servers/sTonFiTVM.go @@ -0,0 +1,40 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package servers + +import ( + "net/http" + + common "dexprices.io/common" + sTonFi "dexprices.io/ton/sTonFi" +) + +type TONDex struct{} + +func (t *TONDex) GetPoolPrice(r *http.Request, args *Args, reply *common.TokenPrice) error { + var sTonFi sTonFi.STonFi + if err := sTonFi.Init(""); err != nil { + r.Response.StatusCode = 404 + return err + } + if err := sTonFi.GetPoolPrice(args.Pool, "latest", reply); err != nil { + // r.Response.StatusCode = 200 + return err + } + return nil +} + +func (t *TONDex) GetTokensPrices(r *http.Request, args *Args, reply *common.TokenPrice) error { + var sTonFi sTonFi.STonFi + if err := sTonFi.Init(""); err != nil { + r.Response.StatusCode = 404 + return err + } + if err := sTonFi.GetPrice(args.Token0, args.Token1, "reply", reply); err != nil { + // r.Response.StatusCode = 200 + return err + } + return nil +} diff --git a/server/servers/uniEVM.go b/server/servers/uniEVM.go new file mode 100644 index 0000000..e379448 --- /dev/null +++ b/server/servers/uniEVM.go @@ -0,0 +1,40 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package servers + +import ( + "net/http" + + common "dexprices.io/common" + uniSwap "dexprices.io/eth/uniswap" +) + +type Dex struct{} + +func (t *Dex) GetPoolPrice(r *http.Request, args *Args, reply *common.TokenPrice) error { + var uniV3 uniSwap.UniV3 + if err := uniV3.Init("https://eth.llamarpc.com"); err != nil { + r.Response.StatusCode = 404 + return err + } + if err := uniV3.GetPoolPrice(args.Pool, "latest", reply); err != nil { + // r.Response.StatusCode = 200 + return err + } + return nil +} + +func (t *Dex) GetTokensPrices(r *http.Request, args *Args, reply *common.TokenPrice) error { + var uniV3 uniSwap.UniV3 + if err := uniV3.Init("https://eth.llamarpc.com"); err != nil { + r.Response.StatusCode = 404 + return err + } + if err := uniV3.GetPrice(args.Token0, args.Token1, "latest", reply); err != nil { + // r.Response.StatusCode = 200 + return err + } + return nil +} diff --git a/ton/sTonFi/sTonFi.go b/ton/sTonFi/sTonFi.go new file mode 100644 index 0000000..6a184dd --- /dev/null +++ b/ton/sTonFi/sTonFi.go @@ -0,0 +1,327 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package ton + +import ( + "context" + "errors" + "fmt" + "log" + "math" + "math/big" + "strconv" + + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/contract/jetton" + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + + dcommon "dexprices.io/common" +) + +const ( + depthStr = "1000000000000" + depth = 9 +) + +type GetExpectedOutputs struct { + Out uint64 + Protocol_fee_out uint64 + Ref_fee_out uint64 +} + +type GetPoolDataOutput struct { + Reserve0 uint64 + Reserve1 uint64 + Token0_address tlb.MsgAddress + Token1_address tlb.MsgAddress + Lp_fee uint64 + Protocol_fee uint64 + Ref_fee uint64 + Protocol_fee_address tlb.MsgAddress + Collected_token0_protocol_fee uint64 + Collected_token1_protocol_fee uint64 +} + +type GetWalletDataOutput struct { + Balance tlb.Int257 + Owner tlb.MsgAddress + Jetton tlb.MsgAddress + JettonWalletCode tlb.Any +} + +type GetPoolOutput struct { + Pool tlb.MsgAddress +} + +type STonFi struct { + Version string + client *liteapi.Client + Error string +} + +var MainnetRouter = ton.MustParseAccountID("EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt") + +// TODO do we need another connectors??? + +func (uni *STonFi) Init(rpcProvider string) error { + var err error + uni.client, err = liteapi.NewClientWithDefaultMainnet() + if err != nil { + fmt.Printf("Unable to create tongo client: %v", err) + } + return nil +} + +/* +TODO + 1. Get metadata of each token + 2. Get `get_expected_outputs` 4 each wallet in account with decimals + 3. Count with decimals + 4. Get pool by addresses abi/get_methods.go:2061 +*/ + +func (sTonFi *STonFi) GetPrice(token0 string, token1 string, block string, tokenInfo *dcommon.TokenPrice) error { + pool, err := sTonFi.getPool(token0, token1) + if err != nil { + return err + } + poolStr, err := ton.AccountIDFromTlb(pool) + if err != nil { + return err + } + err = sTonFi.GetPoolPrice(poolStr.ToRaw(), block, tokenInfo) + if err != nil { + return err + } + return nil +} + +func (sTonFi *STonFi) GetPoolPrice(poolContract string, block string, tokenInfo *dcommon.TokenPrice) error { + accountId := tongo.MustParseAddress(poolContract) + // Get data from Pool + rs0, rs1, ta0, ta1, err := sTonFi.getPoolData(accountId) + if err != nil { + return err + } + ta0ID, err := ton.AccountIDFromTlb(ta0) + if err != nil { + return err + } + ta1ID, err := ton.AccountIDFromTlb(ta1) + if err != nil { + return err + } + // Get token decimals + d0, err := sTonFi.getTokenDecimals(ta0) + if err != nil { + return err + } + d1, err := sTonFi.getTokenDecimals(ta1) + if err != nil { + return err + } + // Count prices + err = sTonFi.getPrices(uint8(d0), uint8(d1), rs0, rs1, tokenInfo) + if err != nil { + return err + } + // Get tokens masters + /* + Unfortunately in STon.fi they keep only wallets that provides liquidity for pools + So for knowing what is a real token is -- we need to ask these wallets for masters + */ + ta0Master, err := sTonFi.getJettonMaster(*ta0ID) + if err != nil { + return err + } + ta1Master, err := sTonFi.getJettonMaster(*ta1ID) + if err != nil { + return err + } + ta0MasterID, err := ton.AccountIDFromTlb(ta0Master) + if err != nil { + return err + } + ta1MasterID, err := ton.AccountIDFromTlb(ta1Master) + if err != nil { + return err + } + tokenInfo.Token0 = ta0MasterID.String() + tokenInfo.Token1 = ta1MasterID.String() + return nil +} + +/* + In a case dc0 < dc1: + 1. d0d1 = dc0 - dc1 + 2. nx = x * 10^d0d1 + 3. P0 = nx / y = (nx * 10^d) / (y * 10^d) => A = (nx * 10^d) / y => P0 = A / 10^d + 4. P1 = y / nx = (y * 10^d) / (nx * 10^d) = (y * 10^d) / (x * 10^(d+d0d1)) => B = (y * 10^d) / (x) => P1 = B / 10^(d+d0d1) + +Perhaps not the best way to count, maybe we need to make decimals flow +*/ +func (uni *STonFi) getPrices(decimals0 uint8, decimals1 uint8, rs0, rs1 big.Int, tokenPrice *dcommon.TokenPrice) error { + var nrs0, nrs1 big.Int + var p0, p1 big.Int + d0d1 := int64(math.Abs(float64(decimals1) - float64(decimals0))) + if decimals0 < decimals1 { + nrs0.Mul(&rs0, big.NewInt(1).Exp(big.NewInt(10), big.NewInt(depth+d0d1), big.NewInt(0))) + nrs1.Mul(&rs1, big.NewInt(1).Exp(big.NewInt(10), big.NewInt(depth-d0d1), big.NewInt(0))) + } else { + nrs0.Mul(&rs0, big.NewInt(1).Exp(big.NewInt(10), big.NewInt(depth-d0d1), big.NewInt(0))) + nrs1.Mul(&rs1, big.NewInt(1).Exp(big.NewInt(10), big.NewInt(depth+d0d1), big.NewInt(0))) + } + log.Print(rs0.String()) + log.Print(rs1.String()) + p1.Div(&nrs0, &rs1) + p0.Div(&nrs1, &rs0) + log.Print(p0.String()) + log.Print(p1.String()) + tokenPrice.PriceT0 = p0.String() + tokenPrice.PriceT1 = p1.String() + tokenPrice.Decimals = strconv.FormatInt(9, 10) + return nil +} + +/* + TODO: + 1. Find permalink on github + 2. Deal with uint or bigInt + + Source: https://tonscan.org/address/EQD8TJ8xEWB1SpnRE4d89YO3jl0W0EiBnNS4IBaHaUmdfizE#source + In pool/get.func + + ``` + (int, int, slice, slice, int, int, int, slice, int, int) get_pool_data() method_id { + load_storage(); + return ( + storage::reserve0, + storage::reserve1, + storage::token0_address, + storage::token1_address, + storage::lp_fee, + storage::protocol_fee, + storage::ref_fee, + storage::protocol_fee_address, + storage::collected_token0_protocol_fee, + storage::collected_token1_protocol_fee + ); + } + ``` +*/ + +func (uni *STonFi) getPoolData(pool ton.Address) (big.Int, big.Int, tlb.MsgAddress, tlb.MsgAddress, error) { + stack := tlb.VmStack{} + rc, vmStack, err := uni.client.RunSmcMethod(context.Background(), pool.ID, "get_pool_data", stack) + if err != nil { + log.Print("Rc: " + strconv.FormatUint(uint64(rc), 10)) + return *big.NewInt(0), *big.NewInt(0), tlb.MsgAddress{}, tlb.MsgAddress{}, err + } + if len(vmStack) != 10 || + vmStack[0].SumType != "VmStkTinyInt" || + vmStack[1].SumType != "VmStkTinyInt" || + vmStack[2].SumType != "VmStkSlice" || + vmStack[3].SumType != "VmStkSlice" { + return *big.NewInt(0), *big.NewInt(0), tlb.MsgAddress{}, tlb.MsgAddress{}, errors.New("stack corrupted") + } + var result GetPoolDataOutput + _ = vmStack.Unmarshal(&result) + return *big.NewInt(int64(result.Reserve0)), *big.NewInt(int64(result.Reserve1)), result.Token0_address, result.Token1_address, nil +} + +// Do golang has overloading? +func (uni *STonFi) getPool(master0 string, master1 string) (tlb.MsgAddress, error) { + t0 := tongo.MustParseAddress(master0) + j0 := jetton.New(t0.ID, uni.client) + token0S, err := j0.GetJettonWallet(context.Background(), MainnetRouter) + if err != nil { + return tlb.MsgAddress{}, nil + } + + t1 := tongo.MustParseAddress(master1) + j1 := jetton.New(t1.ID, uni.client) + token1S, err := j1.GetJettonWallet(context.Background(), MainnetRouter) + if err != nil { + return tlb.MsgAddress{}, nil + } + + stack := tlb.VmStack{} + val0, err := tlb.TlbStructToVmCellSlice(token0S.ToMsgAddress()) + if err != nil { + return tlb.MsgAddress{}, err + } + stack.Put(val0) + val1, err := tlb.TlbStructToVmCellSlice(token1S.ToMsgAddress()) + if err != nil { + return tlb.MsgAddress{}, err + } + stack.Put(val1) + rc, vmStack, err := uni.client.RunSmcMethod(context.Background(), MainnetRouter, "get_pool_address", stack) + if err != nil { + log.Print("Rc: " + strconv.FormatUint(uint64(rc), 10)) + return tlb.MsgAddress{}, err + } + if len(vmStack) != 1 || vmStack[0].SumType != "VmStkSlice" { + return tlb.MsgAddress{}, errors.New("stack corrupted") + } + var result GetPoolOutput + _ = vmStack.Unmarshal(&result) + + return result.Pool, nil +} + +func (uni *STonFi) getTokenDecimals(token tlb.MsgAddress) (int, error) { + tokenTLB, err := ton.AccountIDFromTlb(token) + if err != nil { + return 0, err + } + j := jetton.New(*tokenTLB, uni.client) + d, err := j.GetDecimals(context.Background()) + if err != nil { + // log.Fatalf("Get decimals error: %v", err) + master, err := uni.getJettonMaster(*tokenTLB) + if err != nil { + return 0, err + } + tokenTLB, err := ton.AccountIDFromTlb(master) + if err != nil { + return 0, err + } + j := jetton.New(*tokenTLB, uni.client) + d, err := j.GetDecimals(context.Background()) + if err != nil { + if err.Error() == "only onchain jetton data supported" { + return 9, nil // TODO offchain support + } + return 0, err + } + return d, nil + } + return d, nil +} + +func (uni *STonFi) getJettonMaster(jettonWallet ton.AccountID) (tlb.MsgAddress, error) { + errCode, stack, err := uni.client.RunSmcMethod(context.Background(), jettonWallet, "get_wallet_data", tlb.VmStack{}) + if err != nil { + return tlb.MsgAddress{}, err + } + if errCode == 0xFFFFFF00 { // contract not init + return tlb.MsgAddress{}, nil + } + if errCode != 0 && errCode != 1 { + return tlb.MsgAddress{}, fmt.Errorf("method execution failed with code: %v", errCode) + } + if len(stack) != 4 || (stack[0].SumType != "VmStkTinyInt" && stack[0].SumType != "VmStkInt") || + stack[1].SumType != "VmStkSlice" || + stack[2].SumType != "VmStkSlice" || + stack[3].SumType != "VmStkCell" { + return tlb.MsgAddress{}, fmt.Errorf("invalid stack") + } + var result GetWalletDataOutput + _ = stack.Unmarshal(&result) + return result.Jetton, nil +} diff --git a/ton/sTonFi/sTonFi_test.go b/ton/sTonFi/sTonFi_test.go new file mode 100644 index 0000000..f9f0ffc --- /dev/null +++ b/ton/sTonFi/sTonFi_test.go @@ -0,0 +1,125 @@ +// Copyright 2024, Yulian Volianskyi aka jzethar, All rights reserved. +// This code is a part of DexPriceAPI project +// See the LICENSE file + +package ton + +import ( + "math/big" + "testing" + + common "dexprices.io/common" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/ton" +) + +/* +For tests: EQD8TJ8xEWB1SpnRE4d89YO3jl0W0EiBnNS4IBaHaUmdfizE USTD/pTON pool +*/ +func TestGetPoolPrice(t *testing.T) { + var sTonFi STonFi + sTonFi.Init("") + got := sTonFi.GetPoolPrice("EQD8TJ8xEWB1SpnRE4d89YO3jl0W0EiBnNS4IBaHaUmdfizE", "", &common.TokenPrice{}) + + if got != nil { + t.Errorf("got %q, wanted nil", got) + } +} + +func TestGetPoolData(t *testing.T) { + var sTonFi STonFi + sTonFi.Init("") + accountId := tongo.MustParseAddress("EQD8TJ8xEWB1SpnRE4d89YO3jl0W0EiBnNS4IBaHaUmdfizE") + _, _, ta0, ta1, err := sTonFi.getPoolData(accountId) + + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + t0, err := ton.AccountIDFromTlb(ta0) + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + t1, err := ton.AccountIDFromTlb(ta1) + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + if t0.ToRaw() != "0:4eec921b9d4d56a0d94676016d49c26b39b1d7901a7e153e66033f146a0886f4" { + t.Errorf("got %q, wanted nil", t0.Address) + } + if t1.ToRaw() != "0:1150b518b2626ad51899f98887f8824b70065456455f7fe2813f012699a4061f" { + t.Errorf("got %q, wanted nil", t1.Address) + } +} + +func TestGetTokenDecimals(t *testing.T) { + var sTonFi STonFi + sTonFi.Init("") + accountId := tongo.MustParseAddress("EQD8TJ8xEWB1SpnRE4d89YO3jl0W0EiBnNS4IBaHaUmdfizE") + _, _, ta0, ta1, err := sTonFi.getPoolData(accountId) + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + d0, err := sTonFi.getTokenDecimals(ta0) + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + if d0 != 6 { + t.Errorf("got %q, wanted nil", d0) + } + d1, err := sTonFi.getTokenDecimals(ta1) + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + if d1 != 9 { + t.Errorf("got %q, wanted nil", d1) + } +} + +func TestGetJettonMaster(t *testing.T) { + var sTonFi STonFi + sTonFi.Init("") + accountId := tongo.MustParseAddress("EQARULUYsmJq1RiZ-YiH-IJLcAZUVkVff-KBPwEmmaQGH6aC") + master, err := sTonFi.getJettonMaster(accountId.ID) + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + ms, err := ton.AccountIDFromTlb(master) + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + if ms.ToRaw() != "0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c" { + t.Errorf("got %q, wanted nil", ms.Address) + } +} + +func TestGetPool(t *testing.T) { + var sTonFi STonFi + sTonFi.Init("") + pool, err := sTonFi.getPool("EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", "EQCM3B12QK1e4yZSf8GtBRT0aLMNyEsBc_DhVfRRtOEffLez") + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + ms, err := ton.AccountIDFromTlb(pool) + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + if ms.ToRaw() != "0:fc4c9f311160754a99d113877cf583b78e5d16d048819cd4b820168769499d7e" { + t.Errorf("got %q, wanted nil", ms.Address) + } +} + +func TestGetPrices(t *testing.T) { + var sTonFi STonFi + var tokenPrice common.TokenPrice + sTonFi.Init("") + err := sTonFi.getPrices(6, 9, *big.NewInt(150681859999009), *big.NewInt(22133954922754892), &tokenPrice) + if err != nil { + t.Errorf("got %q, wanted nil", err) + } + if tokenPrice.PriceT0 != "146891967" { + t.Errorf("got %q, wanted 146891967771704", tokenPrice.PriceT0) + } + if tokenPrice.PriceT1 != "6807724174" { + t.Errorf("got %q, wanted 6807724174", tokenPrice.PriceT1) + } +}