diff --git a/Assets/SequenceSDK/Marketplace/CurrencySwapTests.cs b/Assets/SequenceSDK/Marketplace/CurrencySwapTests.cs new file mode 100644 index 000000000..4e28bc690 --- /dev/null +++ b/Assets/SequenceSDK/Marketplace/CurrencySwapTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Numerics; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Sequence.Marketplace +{ + public class CurrencySwapTests + { + private const Chain _chain = Chain.ArbitrumOne; + private const string USDC = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; + private const string USDCe = "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"; + + [Test] + public async Task GetSwapPriceTest() + { + CurrencySwap currencySwap = new CurrencySwap(_chain); + string amount = "1000"; + + SwapPrice swapPrice = await currencySwap.GetSwapPrice(new Address(USDC), new Address(USDCe), amount); + + Assert.IsNotNull(swapPrice); + Assert.AreEqual(USDCe.ToLower(), swapPrice.currencyAddress.Value.ToLower()); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapPrice.price)); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapPrice.maxPrice)); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapPrice.transactionValue)); + Assert.GreaterOrEqual(BigInteger.Parse(swapPrice.transactionValue), BigInteger.Zero); + } + + [Test] + public async Task GetSwapPricesTest() + { + CurrencySwap currencySwap = new CurrencySwap(_chain); + string amount = "1000"; + + SwapPrice[] swapPrices = await currencySwap.GetSwapPrices(new Address("0xe8db071f698aBA1d60babaE8e08F5cBc28782108"), new Address(USDC), amount); + + Assert.IsNotNull(swapPrices); + Assert.Greater(swapPrices.Length, 0); + foreach (SwapPrice swapPrice in swapPrices) + { + Assert.IsNotNull(swapPrice); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapPrice.price)); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapPrice.maxPrice)); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapPrice.transactionValue)); + Assert.GreaterOrEqual(BigInteger.Parse(swapPrice.transactionValue), BigInteger.Zero); + } + } + + [Test] + public async Task GetSwapQuoteTest() + { + CurrencySwap currencySwap = new CurrencySwap(_chain); + string amount = "1000"; + ChainIndexer indexer = new ChainIndexer(_chain); + Address userWallet = new Address("0xe8db071f698aBA1d60babaE8e08F5cBc28782108"); + + SwapQuote swapQuote = await currencySwap.GetSwapQuote(userWallet, new Address(USDC), new Address(USDCe), amount, true); + GetTokenBalancesReturn balancesReturn = + await indexer.GetTokenBalances(new GetTokenBalancesArgs(userWallet, USDCe)); + TokenBalance[] balances = balancesReturn.balances; + Assert.Greater(balances.Length, 0); + TokenBalance wethBalance = null; + foreach (var balance in balances) + { + if (balance.contractAddress == USDCe) + { + wethBalance = balance; + break; + } + } + Assert.IsNotNull(wethBalance); + BigInteger wethBalanceAmount = wethBalance.balance; + + Assert.IsNotNull(swapQuote); + Assert.AreEqual(USDCe.ToLower(), swapQuote.currencyAddress.Value.ToLower()); + Assert.AreEqual(wethBalanceAmount, BigInteger.Parse(swapQuote.currencyBalance)); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapQuote.price)); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapQuote.maxPrice)); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapQuote.transactionData)); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapQuote.transactionValue)); + Assert.GreaterOrEqual(BigInteger.Parse(swapQuote.transactionValue), BigInteger.Zero); + Assert.IsFalse(string.IsNullOrWhiteSpace(swapQuote.approveData)); + } + + [Test] + public async Task GetSwapQuoteTest_InsufficientBalance() + { + CurrencySwap currencySwap = new CurrencySwap(_chain); + string amount = "1000"; + ChainIndexer indexer = new ChainIndexer(_chain); + Address userWallet = new Address("0xc683a014955b75F5ECF991d4502427c8fa1Aa249"); + + try + { + SwapQuote swapQuote = await currencySwap.GetSwapQuote(userWallet, new Address(USDC), new Address(USDCe), amount, true); + Assert.Fail("Exception expected but none was encountered"); + } + catch (Exception e) + { + Assert.IsTrue(e.Message.Contains("Insufficient balance")); + } + } + + [Test] + public async Task GetSwapQuoteTest_FailedToFetchPrice() + { + CurrencySwap currencySwap = new CurrencySwap(_chain); + string amount = "1000000000000000000000000000000000000"; + ChainIndexer indexer = new ChainIndexer(_chain); + Address userWallet = new Address("0xc683a014955b75F5ECF991d4502427c8fa1Aa249"); + + try + { + SwapQuote swapQuote = await currencySwap.GetSwapQuote(userWallet, new Address(USDC), new Address(USDCe), amount, true); + Assert.Fail("Exception expected but none was encountered"); + } + catch (Exception e) + { + Assert.IsTrue(e.Message.Contains("Error fetching swap price")); + } + } + } +} \ No newline at end of file diff --git a/Assets/SequenceSDK/Marketplace/CurrencySwapTests.cs.meta b/Assets/SequenceSDK/Marketplace/CurrencySwapTests.cs.meta new file mode 100644 index 000000000..c80236aaa --- /dev/null +++ b/Assets/SequenceSDK/Marketplace/CurrencySwapTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f38ecba261f54d90a4c7915d3def746 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SequenceSDK/Marketplace/SequenceMarketPlaceTests.asmdef b/Assets/SequenceSDK/Marketplace/SequenceMarketPlaceTests.asmdef index 67b77c72d..9dc95f979 100644 --- a/Assets/SequenceSDK/Marketplace/SequenceMarketPlaceTests.asmdef +++ b/Assets/SequenceSDK/Marketplace/SequenceMarketPlaceTests.asmdef @@ -4,8 +4,16 @@ "references": [ "GUID:0acc523941302664db1f4e527237feb3", "GUID:27619889b8ba8c24980f49ee34dbb44a", - "GUID:19b9eb7db56cc47349571a4fbb0dd677", - "GUID:f7fd4ba36aabd1d499450c174865e70b" + "GUID:f7fd4ba36aabd1d499450c174865e70b", + "SequenceIntegrations", + "SequenceUtils", + "SequenceMarketplace", + "GUID:403077141e1554429a890cbc129df651", + "SequenceEmbeddedWallet", + "GUID:040286810a82b46ed9acd6d70bfbbfd4", + "GUID:f78a27d6a73d94c4baf04337e0add4dc", + "GUID:68e161619428e430bba22d7d0a9548ab", + "GUID:0da6d172d18a54e4389d0dce1e1ffdf2" ], "includePlatforms": [ "Editor" diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Ethereum/Address/Address.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Ethereum/Address/Address.cs index c5f1b0a72..3c58abb74 100644 --- a/Packages/Sequence-Unity/Sequence/SequenceSDK/Ethereum/Address/Address.cs +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Ethereum/Address/Address.cs @@ -1,7 +1,10 @@ using System; +using Newtonsoft.Json; using Sequence.Utils; namespace Sequence { + + [JsonConverter(typeof(AddressJsonConverter))] public class Address { public readonly string Value; @@ -40,4 +43,43 @@ public override bool Equals(object obj) return this.Value == address.Value; } } + + public class AddressJsonConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is Address address) + { + writer.WriteValue(address.Value); + } + else + { + throw new JsonSerializationException("Expected Address object."); + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + string addressString = (string)reader.Value; + + try + { + return new Address(addressString); + } + catch (ArgumentOutOfRangeException) + { + throw new JsonSerializationException("Invalid address format."); + } + } + + throw new JsonSerializationException("Expected a string value."); + } + + public override bool CanConvert(Type objectType) + { + return true; + } + } } \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/CurrencySwap.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/CurrencySwap.cs new file mode 100644 index 000000000..cec164832 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/CurrencySwap.cs @@ -0,0 +1,161 @@ +using System; +using System.Numerics; +using System.Threading.Tasks; +using Sequence.Utils; + +namespace Sequence.Marketplace +{ + public class CurrencySwap : ISwap + { + private Chain _chain; + private IHttpClient _client; + private const string BaseUrl = "https://api.sequence.app/rpc/API"; + private IIndexer _indexer; + + public CurrencySwap(Chain chain, IHttpClient client = null) + { + _chain = chain; + if (client == null) + { + client = new HttpClient(); + } + _client = client; + _indexer = new ChainIndexer(_chain); + } + + public event Action OnSwapPriceReturn; + public event Action OnSwapPriceError; + + public async Task GetSwapPrice(Address buyCurrency, Address sellCurrency, string buyAmount, + uint slippagePercent = ISwap.DefaultSlippagePercentage) + { + GetSwapPriceRequest args = new GetSwapPriceRequest(buyCurrency, sellCurrency, buyAmount, _chain, + slippagePercent); + string url = BaseUrl.AppendTrailingSlashIfNeeded() + "GetSwapPrice"; + try + { + GetSwapPriceResponse response = + await _client.SendRequest(url, args); + OnSwapPriceReturn?.Invoke(response.swapPrice); + return response.swapPrice; + } + catch (Exception e) + { + string error = + $"Error fetching swap price for {buyCurrency} and {sellCurrency} with {nameof(buyAmount)} {buyAmount}: {e.Message}"; + OnSwapPriceError?.Invoke(error); + throw new Exception(error); + } + } + + public event Action OnSwapPricesReturn; + public event Action OnSwapPricesError; + + public async Task GetSwapPrices(Address userWallet, Address buyCurrency, string buyAmount, + uint slippagePercentage = ISwap.DefaultSlippagePercentage) + { + GetSwapPricesRequest args = new GetSwapPricesRequest(userWallet, buyCurrency, buyAmount, _chain, + slippagePercentage); + string url = BaseUrl.AppendTrailingSlashIfNeeded() + "GetSwapPrices"; + try + { + GetSwapPricesResponse response = + await _client.SendRequest(url, args); + OnSwapPricesReturn?.Invoke(response.swapPrices); + return response.swapPrices; + } + catch (Exception e) + { + string error = + $"Error fetching swap prices for {buyCurrency} with {nameof(buyAmount)} {buyAmount}: {e.Message}"; + OnSwapPricesError?.Invoke(error); + throw new Exception(error); + } + } + + public event Action OnSwapQuoteReturn; + public event Action OnSwapQuoteError; + + public async Task GetSwapQuote(Address userWallet, Address buyCurrency, Address sellCurrency, + string buyAmount, bool includeApprove, + uint slippagePercentage = ISwap.DefaultSlippagePercentage) + { + try + { + await AssertWeHaveSufficientBalance(userWallet, buyCurrency, sellCurrency, buyAmount, + slippagePercentage); + } + catch (Exception e) + { + string error = $"Error fetching swap quote for buying {buyAmount} of {buyCurrency} with {sellCurrency}: {e.Message}"; + OnSwapQuoteError?.Invoke(error); + throw new Exception(error); + } + + GetSwapQuoteRequest args = new GetSwapQuoteRequest(userWallet, buyCurrency, sellCurrency, buyAmount, _chain, + slippagePercentage, includeApprove); + string url = BaseUrl.AppendTrailingSlashIfNeeded() + "GetSwapQuote"; + try + { + GetSwapQuoteResponse response = + await _client.SendRequest(url, args); + if (response.swapQuote == null) + { + string error = $"Error fetching swap quote for buying {buyAmount} of {buyCurrency} with {sellCurrency}: Unknown error - swap API has returned a null response"; + OnSwapQuoteError?.Invoke(error); + throw new Exception(error); + } + OnSwapQuoteReturn?.Invoke(response.swapQuote); + return response.swapQuote; + } + catch (Exception e) + { + string error = + $"Error fetching swap quote for buying {buyAmount} of {buyCurrency} with {sellCurrency}: {e.Message}"; + OnSwapQuoteError?.Invoke(error); + throw new Exception(error); + } + } + + private async Task AssertWeHaveSufficientBalance(Address userWallet, Address buyCurrency, Address sellCurrency, + string buyAmount, uint slippagePercentage = ISwap.DefaultSlippagePercentage) + { + BigInteger required, have; + try + { + SwapPrice price = await GetSwapPrice(buyCurrency, sellCurrency, buyAmount, slippagePercentage); + required = BigInteger.Parse(price.maxPrice); + } + catch (Exception e) + { + throw new Exception($"Error fetching swap price for buying {buyAmount} of {buyCurrency} with {sellCurrency}: {e.Message}"); + } + + TokenBalance[] sellCurrencyBalances; + try + { + GetTokenBalancesReturn balanceResponse = await _indexer.GetTokenBalances(new GetTokenBalancesArgs(userWallet, sellCurrency)); + sellCurrencyBalances = balanceResponse.balances; + } + catch (Exception e) + { + throw new Exception($"Error fetching token balance of {sellCurrency}: {e.Message}"); + } + + if (sellCurrencyBalances == null || sellCurrencyBalances.Length == 0) + { + have = 0; + } + else + { + have = sellCurrencyBalances[0].balance; + } + + if (have < required) + { + throw new Exception( + $"Insufficient balance of {sellCurrency} to buy {buyAmount} of {buyCurrency}, have {have}, need {required}"); + } + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/CurrencySwap.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/CurrencySwap.cs.meta new file mode 100644 index 000000000..4730069e2 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/CurrencySwap.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ec6040c614ec427db7ff9c7624038ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceRequest.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceRequest.cs new file mode 100644 index 000000000..734a524bf --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceRequest.cs @@ -0,0 +1,37 @@ +using System; +using Newtonsoft.Json; +using UnityEngine.Scripting; +using UnityEngine.Serialization; + +namespace Sequence.Marketplace +{ + [Serializable] + internal class GetSwapPriceRequest + { + public string buyCurrencyAddress; + public string sellCurrencyAddress; + public string buyAmount; + public ulong chainId; + [FormerlySerializedAs("slippagePercentageInBasisPoints")] public ulong slippagePercentage; + + [Preserve] + [JsonConstructor] + public GetSwapPriceRequest(string buyCurrencyAddress, string sellCurrencyAddress, string buyAmount, ulong chainId, ulong slippagePercentage) + { + this.buyCurrencyAddress = buyCurrencyAddress; + this.sellCurrencyAddress = sellCurrencyAddress; + this.buyAmount = buyAmount; + this.chainId = chainId; + this.slippagePercentage = slippagePercentage; + } + + public GetSwapPriceRequest(Address buyCurrencyAddress, Address sellCurrencyAddress, string buyAmount, Chain chain, ulong slippagePercentageInBasisPoints) + { + this.buyCurrencyAddress = buyCurrencyAddress; + this.sellCurrencyAddress = sellCurrencyAddress; + this.buyAmount = buyAmount; + this.chainId = ulong.Parse(ChainDictionaries.ChainIdOf[chain]); + this.slippagePercentage = slippagePercentageInBasisPoints; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceRequest.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceRequest.cs.meta new file mode 100644 index 000000000..df4558780 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c000d793f87124416a6efa5d614d58b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceResponse.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceResponse.cs new file mode 100644 index 000000000..88e1c20e6 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceResponse.cs @@ -0,0 +1,17 @@ +using System; +using UnityEngine.Scripting; + +namespace Sequence.Marketplace +{ + [Serializable] + internal class GetSwapPriceResponse + { + public SwapPrice swapPrice; + + [Preserve] + public GetSwapPriceResponse(SwapPrice swapPrice) + { + this.swapPrice = swapPrice; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceResponse.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceResponse.cs.meta new file mode 100644 index 000000000..d60aef5e8 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPriceResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3de69b6b189ba4fe8b483e34fad69865 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesRequest.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesRequest.cs new file mode 100644 index 000000000..21ed2df2a --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesRequest.cs @@ -0,0 +1,36 @@ +using System; +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace Sequence.Marketplace +{ + [Serializable] + internal class GetSwapPricesRequest + { + public string userAddress; + public string buyCurrencyAddress; + public string buyAmount; + public ulong chainId; + public ulong slippagePercentage; + + [Preserve] + [JsonConstructor] + public GetSwapPricesRequest(string userAddress, string buyCurrencyAddress, string buyAmount, ulong chainId, ulong slippagePercentage) + { + this.userAddress = userAddress; + this.buyCurrencyAddress = buyCurrencyAddress; + this.buyAmount = buyAmount; + this.chainId = chainId; + this.slippagePercentage = slippagePercentage; + } + + public GetSwapPricesRequest(Address userAddress, Address buyCurrencyAddress, string buyAmount, Chain chain, ulong slippagePercentage) + { + this.userAddress = userAddress; + this.buyCurrencyAddress = buyCurrencyAddress; + this.buyAmount = buyAmount; + this.chainId = ulong.Parse(ChainDictionaries.ChainIdOf[chain]); + this.slippagePercentage = slippagePercentage; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesRequest.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesRequest.cs.meta new file mode 100644 index 000000000..22319eb44 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 65bb71e1ba29f4e0db9e5007e47b9e76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesResponse.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesResponse.cs new file mode 100644 index 000000000..30e1c7048 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesResponse.cs @@ -0,0 +1,17 @@ +using System; +using UnityEngine.Scripting; + +namespace Sequence.Marketplace +{ + [Serializable] + public class GetSwapPricesResponse + { + public SwapPrice[] swapPrices; + + [Preserve] + public GetSwapPricesResponse(SwapPrice[] swapPrices) + { + this.swapPrices = swapPrices; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesResponse.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesResponse.cs.meta new file mode 100644 index 000000000..c1a6d8635 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapPricesResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cb2ae82da338747769ef7b7368ce7e04 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteRequest.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteRequest.cs new file mode 100644 index 000000000..09b0899c3 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteRequest.cs @@ -0,0 +1,44 @@ +using System; +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace Sequence.Marketplace +{ + [Serializable] + internal class GetSwapQuoteRequest + { + public string userAddress; + public string buyCurrencyAddress; + public string sellCurrencyAddress; + public string buyAmount; + public ulong chainId; + public bool includeApprove; + public ulong slippagePercentage; + + [Preserve] + [JsonConstructor] + public GetSwapQuoteRequest(string userAddress, string buyCurrencyAddress, string sellCurrencyAddress, + string buyAmount, ulong chainId, bool includeApprove, ulong slippagePercentage) + { + this.userAddress = userAddress; + this.buyCurrencyAddress = buyCurrencyAddress; + this.sellCurrencyAddress = sellCurrencyAddress; + this.buyAmount = buyAmount; + this.chainId = chainId; + this.slippagePercentage = slippagePercentage; + this.includeApprove = includeApprove; + } + + public GetSwapQuoteRequest(Address userAddress, Address buyCurrencyAddress, Address sellCurrencyAddress, + string buyAmount, Chain chain, ulong slippagePercentage, bool includeApprove) + { + this.userAddress = userAddress; + this.buyCurrencyAddress = buyCurrencyAddress; + this.sellCurrencyAddress = sellCurrencyAddress; + this.buyAmount = buyAmount; + this.chainId = ulong.Parse(ChainDictionaries.ChainIdOf[chain]); + this.slippagePercentage = slippagePercentage; + this.includeApprove = includeApprove; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteRequest.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteRequest.cs.meta new file mode 100644 index 000000000..2c49ba5df --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9bdc1d650a134a6c82fb465bdb9228c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteResponse.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteResponse.cs new file mode 100644 index 000000000..24bf5966c --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteResponse.cs @@ -0,0 +1,17 @@ +using System; +using UnityEngine.Scripting; + +namespace Sequence.Marketplace +{ + [Serializable] + internal class GetSwapQuoteResponse + { + public SwapQuote swapQuote; + + [Preserve] + public GetSwapQuoteResponse(SwapQuote swapQuote) + { + this.swapQuote = swapQuote; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteResponse.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteResponse.cs.meta new file mode 100644 index 000000000..9f27f6e5e --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/GetSwapQuoteResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 72c7d2ff51e5f4fc48466939128d953d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapPrice.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapPrice.cs new file mode 100644 index 000000000..d550ebc15 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapPrice.cs @@ -0,0 +1,25 @@ +using System; +using UnityEngine.Scripting; + +namespace Sequence.Marketplace +{ + [Serializable] + public class SwapPrice + { + public Address currencyAddress; + public string currencyBalance; + public string price; + public string maxPrice; // Guaranteed price for the swap (including slippage) + public string transactionValue; + + [Preserve] + public SwapPrice(Address currencyAddress, string currencyBalance, string price, string maxPrice, string transactionValue) + { + this.currencyAddress = currencyAddress; + this.currencyBalance = currencyBalance; + this.price = price; + this.maxPrice = maxPrice; + this.transactionValue = transactionValue; + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapPrice.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapPrice.cs.meta new file mode 100644 index 000000000..5407370d1 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapPrice.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c64f8d4f985c14f80851694662572345 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapQuote.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapQuote.cs new file mode 100644 index 000000000..fcdcadcd3 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapQuote.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Sequence.EmbeddedWallet; +using UnityEngine.Scripting; + +namespace Sequence.Marketplace +{ + [Serializable] + public class SwapQuote + { + public Address currencyAddress; + public string currencyBalance; + public string price; + public string maxPrice; // Guaranteed price for the swap (including slippage) + public Address to; // Must be approved to access maxPrice of currencyAddress for the sender + public string transactionData; + public string transactionValue; + public string approveData; // Supplied when includeApprove is true + + [Preserve] + public SwapQuote(Address currencyAddress, string currencyBalance, string price, string maxPrice, Address to, string transactionData, string transactionValue, string approveData) + { + this.currencyAddress = currencyAddress; + this.currencyBalance = currencyBalance; + this.price = price; + this.maxPrice = maxPrice; + this.to = to; + this.transactionData = transactionData; + this.transactionValue = transactionValue; + this.approveData = approveData; + } + } + + public static class SwapQuoteExtensions + { + public static Transaction[] AsTransactionArray(this SwapQuote swapQuote) + { + List transactions = new List(); + if (!string.IsNullOrWhiteSpace(swapQuote.approveData)) + { + transactions.Add(new RawTransaction(swapQuote.to, null, swapQuote.approveData)); + } + transactions.Add(new RawTransaction(swapQuote.to, swapQuote.transactionValue, swapQuote.transactionData)); + + return transactions.ToArray(); + } + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapQuote.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapQuote.cs.meta new file mode 100644 index 000000000..1388c7251 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/DataTypes/SwapQuote.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d56ea5e7e18374628b66a0e0c4ae190d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/HttpClient.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/HttpClient.cs index edc46727d..edbd70c3e 100644 --- a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/HttpClient.cs +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/HttpClient.cs @@ -11,22 +11,53 @@ namespace Sequence.Marketplace { - public class HttpClient + public class HttpClient : IHttpClient { private string _apiKey; - private const string _baseUrl = "https://marketplace-api.sequence.app/"; + private const string _prodUrl = "https://marketplace-api.sequence.app/"; + private static string _baseUrl = "https://marketplace-api.sequence.app/"; private const string _endUrl = "/rpc/Marketplace/"; + private JsonSerializerSettings serializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }; public HttpClient() { SequenceConfig config = SequenceConfig.GetConfig(); _apiKey = config.BuilderAPIKey; + _baseUrl = _prodUrl; + } + + public static HttpClient UseHttpClientWithDevEnvironment(string devApiKey) + { + _baseUrl = "https://dev-marketplace-api.sequence-dev.app/"; + return new HttpClient(devApiKey); + } + + private HttpClient(string apiKey) + { + _apiKey = apiKey; + } + + public async Task SendRequest(Chain chain, string url) + { + return await SendRequest(chain, url, null); } public async Task SendRequest(Chain chain, string endpoint, ArgType args) { string url = _baseUrl + ChainDictionaries.PathOf[chain] + _endUrl + endpoint; - string requestJson = JsonConvert.SerializeObject(args); + return await SendRequest(url, args); + } + + public async Task SendRequest(string url, ArgType args) + { + string requestJson = ""; + if (args != null) + { + requestJson = JsonConvert.SerializeObject(args, serializerSettings); + } using UnityWebRequest request = UnityWebRequest.Get(url); request.method = UnityWebRequest.kHttpVerbPOST; byte[] requestData = Encoding.UTF8.GetBytes(requestJson); diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/IHttpClient.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/IHttpClient.cs new file mode 100644 index 000000000..5ed855059 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/IHttpClient.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Sequence.Marketplace +{ + public interface IHttpClient + { + public Task SendRequest(Chain chain, string url); + public Task SendRequest(Chain chain, string endpoint, ArgType args); + public Task SendRequest(string url, ArgType args); + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/IHttpClient.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/IHttpClient.cs.meta new file mode 100644 index 000000000..ee4d58d57 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/IHttpClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 546979f8964854ceb9bf1892c072a0fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/ISwap.cs b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/ISwap.cs new file mode 100644 index 000000000..647f72c04 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/ISwap.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; + +namespace Sequence.Marketplace +{ + public interface ISwap + { + public const uint DefaultSlippagePercentage = 5; + + public event Action OnSwapPriceReturn; + public event Action OnSwapPriceError; + + /// + /// Get the current SwapPrice for a given buyCurrency, sellCurrency, and buyAmount + /// + /// + /// + /// + /// the maximum slippage percentage allowed + /// + public Task GetSwapPrice(Address buyCurrency, Address sellCurrency, string buyAmount, uint slippagePercent = DefaultSlippagePercentage); + + public event Action OnSwapPricesReturn; + public event Action OnSwapPricesError; + + /// + /// Get a SwapPrice[] for a given buyCurrency and buyAmount using the available sell currencies in the userWallet + /// + /// + /// + /// + /// the maximum slippage percentage allowed + /// + public Task GetSwapPrices(Address userWallet, Address buyCurrency, string buyAmount, uint slippagePercentage = DefaultSlippagePercentage); + + public event Action OnSwapQuoteReturn; + public event Action OnSwapQuoteError; + + /// + /// Get a SwapQuote for a given buyCurrency, sellCurrency, and buyAmount, executable by userWallet + /// + /// + /// + /// + /// + /// if true, the requested swap quote will include transaction data required to approve the needed amount of sellCurrency for spending by the "to" address (the full-filler of the SwapQuote) + /// the maximum slippage percentage allowed + /// + public Task GetSwapQuote(Address userWallet, Address buyCurrency, Address sellCurrency, + string buyAmount, bool includeApprove, uint slippagePercentage = DefaultSlippagePercentage); + } +} \ No newline at end of file diff --git a/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/ISwap.cs.meta b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/ISwap.cs.meta new file mode 100644 index 000000000..7f060ba63 --- /dev/null +++ b/Packages/Sequence-Unity/Sequence/SequenceSDK/Marketplace/ISwap.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: efffd5a4f02364b0c970f07c65c0ed6e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/Sequence-Unity/package.json b/Packages/Sequence-Unity/package.json index 6ab8fdcd9..333d1bdbc 100644 --- a/Packages/Sequence-Unity/package.json +++ b/Packages/Sequence-Unity/package.json @@ -1,6 +1,6 @@ { "name": "xyz.0xsequence.waas-unity", - "version": "3.14.2", + "version": "3.15.0", "displayName": "Sequence Embedded Wallet SDK", "description": "A Unity SDK for the Sequence WaaS API", "unity": "2021.3",