diff --git a/Assets/SequenceSDK/Indexer/Tests/MockHttpHandler.cs b/Assets/SequenceSDK/Indexer/Tests/MockHttpHandler.cs index de11a112..4eca004f 100644 --- a/Assets/SequenceSDK/Indexer/Tests/MockHttpHandler.cs +++ b/Assets/SequenceSDK/Indexer/Tests/MockHttpHandler.cs @@ -27,7 +27,12 @@ public Task HttpPost(string chainID, string endPoint, object args, int r return Task.FromResult(_response); } - public void HttpStream(string chainID, string endPoint, object args, WebRPCStreamOptions options, int retries = 0) + public Task HttpPost(string url, object args) + { + throw new NotImplementedException(); + } + + public void HttpStream(string chainID, string endPoint, object args, WebRPCStreamOptions options) { throw new NotImplementedException(); } diff --git a/Assets/SequenceSDK/Indexer/Tests/PriceFeedTests.cs b/Assets/SequenceSDK/Indexer/Tests/PriceFeedTests.cs new file mode 100644 index 00000000..a6f5af72 --- /dev/null +++ b/Assets/SequenceSDK/Indexer/Tests/PriceFeedTests.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Sequence.Indexer.Tests +{ + public class PriceFeedTests + { + [Test] + public async Task TestGetCoinPrices() + { + Address polygonUSDC = new Address("0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"); + Address arbitrumNovaUSDC = new Address("0x750ba8b76187092B0D1E87E28daaf484d1b5273b"); + PriceFeed.Token polygonUSDCtoken = new PriceFeed.Token(Chain.Polygon, polygonUSDC); + PriceFeed.Token arbitrumNovaUSDCtoken = new PriceFeed.Token(Chain.ArbitrumNova, arbitrumNovaUSDC); + PriceFeed priceFeed = new PriceFeed(); + + TokenPrice[] prices = await priceFeed.GetCoinPrices(polygonUSDCtoken, arbitrumNovaUSDCtoken); + + Assert.NotNull(prices); + Assert.AreEqual(2, prices.Length); + Assert.AreEqual(polygonUSDC, prices[0].token.Contract); + Assert.AreEqual(arbitrumNovaUSDC, prices[1].token.Contract); + Assert.AreEqual(1, Math.Round(prices[0].price.value)); + Assert.AreEqual(1, Math.Round(prices[1].price.value)); + } + } +} \ No newline at end of file diff --git a/Assets/SequenceSDK/Indexer/Tests/PriceFeedTests.cs.meta b/Assets/SequenceSDK/Indexer/Tests/PriceFeedTests.cs.meta new file mode 100644 index 00000000..cf1499fb --- /dev/null +++ b/Assets/SequenceSDK/Indexer/Tests/PriceFeedTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a783705decc74a9fb8aaebb5f54c85ec +timeCreated: 1740605949 \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesArgs.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesArgs.cs new file mode 100644 index 00000000..ff307407 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesArgs.cs @@ -0,0 +1,16 @@ +using System; +using UnityEngine.Scripting; + +namespace Sequence +{ + [Serializable] + internal class GetTokenPricesArgs + { + public PriceFeed.Token[] tokens; + + public GetTokenPricesArgs(PriceFeed.Token[] tokens) + { + this.tokens = tokens; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesArgs.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesArgs.cs.meta new file mode 100644 index 00000000..e7c09294 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesArgs.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6d29d944f6e546438b2829b7909f1412 +timeCreated: 1740604904 \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesReturn.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesReturn.cs new file mode 100644 index 00000000..48d4e31a --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesReturn.cs @@ -0,0 +1,17 @@ +using System; +using UnityEngine.Scripting; + +namespace Sequence +{ + [Serializable] + internal class GetTokenPricesReturn + { + public TokenPrice[] tokenPrices; + + [Preserve] + public GetTokenPricesReturn(TokenPrice[] tokenPrices) + { + this.tokenPrices = tokenPrices; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesReturn.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesReturn.cs.meta new file mode 100644 index 00000000..0667a18e --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/GetTokenPricesReturn.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fda0d852ef824f6897530515610db324 +timeCreated: 1740605005 \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/Price.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/Price.cs new file mode 100644 index 00000000..6d3b098c --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/Price.cs @@ -0,0 +1,17 @@ +using System; + +namespace Sequence +{ + [Serializable] + public class Price + { + public decimal value; + public string currency; + + public Price(decimal value, string currency) + { + this.value = value; + this.currency = currency; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/Price.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/Price.cs.meta new file mode 100644 index 00000000..dd3e271f --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/Price.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e32f6920a9174987bf34cd16f0874cc5 +timeCreated: 1740604715 \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/TokenPrice.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/TokenPrice.cs new file mode 100644 index 00000000..9dbb2781 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/TokenPrice.cs @@ -0,0 +1,29 @@ +using System; +using UnityEngine.Scripting; + +namespace Sequence +{ + [Serializable] + public class TokenPrice + { + public PriceFeed.Token token; + public Price price; + public Price price24hChange; + public Price floorPrice; + public Price buyPrice; + public Price sellPrice; + public DateTime updatedAt; + + [Preserve] + public TokenPrice(PriceFeed.Token token, Price price, Price price24hChange, Price floorPrice, Price buyPrice, Price sellPrice, DateTime updatedAt) + { + this.token = token; + this.price = price; + this.price24hChange = price24hChange; + this.floorPrice = floorPrice; + this.buyPrice = buyPrice; + this.sellPrice = sellPrice; + this.updatedAt = updatedAt; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/TokenPrice.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/TokenPrice.cs.meta new file mode 100644 index 00000000..59861c3f --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/DataTypes/TokenPrice.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 81ee70447b764204bbce63d33e1b4ee6 +timeCreated: 1740604458 \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/HttpHandler.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/HttpHandler.cs index c3be84ab..2e365731 100644 --- a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/HttpHandler.cs +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/HttpHandler.cs @@ -3,6 +3,7 @@ using System.IO; using System.Net; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using Sequence.Utils; @@ -106,7 +107,65 @@ public async Task HttpPost(string chainID, string endPoint, object args, return ""; } - public async void HttpStream(string chainID, string endPoint, object args, WebRPCStreamOptions options, int retries = 0) + public async Task HttpPost(string url, object args) + { + string requestJson = JsonConvert.SerializeObject(args, serializerSettings); + using var req = UnityWebRequest.Put(url, requestJson); + req.SetRequestHeader("Content-Type", "application/json"); + req.SetRequestHeader("Accept", "application/json"); + req.SetRequestHeader("X-Access-Key", _builderApiKey); + req.method = UnityWebRequest.kHttpVerbPOST; + req.timeout = 10; + + string curlRequest = + $"curl -X POST -H \"Content-Type: application/json\" -H \"Accept: application/json\" -H \"X-Access-Key: {req.GetRequestHeader("X-Access-Key")}\" -d '{requestJson}' {url}"; + + try + { + await req.SendWebRequest(); + if (req.responseCode < 200 || req.responseCode > 299 || req.error != null || + req.result == UnityWebRequest.Result.ConnectionError || + req.result == UnityWebRequest.Result.ProtocolError) + { + throw new Exception("Failed to make request, non-200 status code " + req.responseCode + + " with error: " + req.error + "\nCurl-equivalent request: " + curlRequest); + } + + byte[] results = req.downloadHandler.data; + var responseJson = Encoding.UTF8.GetString(results); + try + { + T result = JsonConvert.DeserializeObject(responseJson); + return result; + } + catch (Exception e) + { + throw new Exception( + $"Error unmarshalling response from {url}: {e.Message} | given: {responseJson}"); + } + } + catch (HttpRequestException e) + { + throw new HttpRequestException("HTTP Request failed: " + e.Message + "\nCurl-equivalent request: " + curlRequest); + } + catch (FormatException e) + { + throw new FormatException("Invalid URL format: " + e.Message + "\nCurl-equivalent request: " + curlRequest); + } + catch (FileLoadException e) + { + throw new FileLoadException("File load exception: " + e.Message + "\nCurl-equivalent request: " + curlRequest); + } + catch (Exception e) { + throw new Exception("An unexpected error occurred: " + e.Message + "\nCurl-equivalent request: " + curlRequest); + } + finally + { + req.Dispose(); + } + } + + public async void HttpStream(string chainID, string endPoint, object args, WebRPCStreamOptions options) { var requestJson = JsonConvert.SerializeObject(args, serializerSettings); using var req = UnityWebRequest.Put(Url(chainID, endPoint), requestJson); diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/IHttpHandler.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/IHttpHandler.cs index b718b0e3..2dcd7eb2 100644 --- a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/IHttpHandler.cs +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/IHttpHandler.cs @@ -9,7 +9,8 @@ public interface IHttpHandler /// /// public Task HttpPost(string chainID, string endPoint, object args, int retries = 0); - public void HttpStream(string chainID, string endPoint, object args, WebRPCStreamOptions options, int retries = 0); + public Task HttpPost(string url, object args); + public void HttpStream(string chainID, string endPoint, object args, WebRPCStreamOptions options); public void AbortStreams(); } } \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/PriceFeed.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/PriceFeed.cs new file mode 100644 index 00000000..b4d7fdbc --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/PriceFeed.cs @@ -0,0 +1,87 @@ +using System; +using System.Numerics; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Sequence.Config; +using Sequence.Utils; +using UnityEngine.Scripting; + +namespace Sequence +{ + public class PriceFeed + { + [Serializable] + [JsonConverter(typeof(TokenConverter))] + public class Token + { + public Chain Chain; + public Address Contract; + + public Token(Chain chain, Address contract) + { + Chain = chain; + Contract = contract; + } + + [Preserve] + [JsonConstructor] + public Token(BigInteger chainId, string contractAddress) + { + Chain = ChainDictionaries.ChainById[chainId.ToString()]; + Contract = new Address(contractAddress); + } + } + + private class TokenConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Token value, JsonSerializer serializer) + { + var jsonObject = new JObject + { + ["chainId"] = ulong.Parse(ChainDictionaries.ChainIdOf[value.Chain]), + ["contractAddress"] = value.Contract.ToString(), + }; + jsonObject.WriteTo(writer); + } + + public override Token ReadJson(JsonReader reader, Type objectType, Token existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var jsonObject = JObject.Load(reader); + + BigInteger chainId = jsonObject["chainId"]?.Value() ?? 0; + string contractAddress = jsonObject["contractAddress"]?.Value(); + + return new Token(chainId, contractAddress); + } + } + + private const string _devUrl = "https://dev-api.sequence.app/rpc/API"; + private const string _prodUrl = "https://api.sequence.app/rpc/API"; + + private IHttpHandler _httpHandler; + private string _baseUrl; + + public PriceFeed(IHttpHandler httpHandler = null) + { + _httpHandler = httpHandler; + if (_httpHandler == null) + { + _httpHandler = new HttpHandler(SequenceConfig.GetConfig(SequenceService.Stack).BuilderAPIKey); + } + +#if SEQUENCE_DEV_STACK || SEQUENCE_DEV + _baseUrl = _devUrl; +#else + _baseUrl = _prodUrl; +#endif + } + + public async Task GetCoinPrices(params Token[] tokens) + { + GetTokenPricesArgs args = new GetTokenPricesArgs(tokens); + GetTokenPricesReturn response = await _httpHandler.HttpPost(_baseUrl.AppendTrailingSlashIfNeeded() + "GetCoinPrices", args); + return response.tokenPrices; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/PriceFeed.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/PriceFeed.cs.meta new file mode 100644 index 00000000..70045f39 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Indexer/PriceFeed.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a9e95ec0e9da4bc9b0b62ed5efc7efe2 +timeCreated: 1740604074 \ No newline at end of file diff --git a/Packages/Sequence-Unity/package.json b/Packages/Sequence-Unity/package.json index fb1a1647..04b64ff9 100644 --- a/Packages/Sequence-Unity/package.json +++ b/Packages/Sequence-Unity/package.json @@ -1,6 +1,6 @@ { "name": "xyz.0xsequence.waas-unity", - "version": "3.19.3", + "version": "3.20.0", "displayName": "Sequence Embedded Wallet SDK", "description": "A Unity SDK for the Sequence WaaS API", "unity": "2021.3",