From 8fb4114a2092b918fc8207689596cbd7211b0c7a Mon Sep 17 00:00:00 2001 From: Firekeeper <0xFirekeeper@gmail.com> Date: Fri, 5 Apr 2024 23:20:09 +0300 Subject: [PATCH] Storage download upload (#4) * Storage download upload * Update dotnet-ci.yml --- .github/workflows/dotnet-ci.yml | 2 + Thirdweb.Console/Program.cs | 4 + Thirdweb.Tests/BaseTests.cs | 4 + ...lientTests.cs => Thirdweb.Client.Tests.cs} | 0 .../{RpcTests.cs => Thirdweb.RPC.Tests.cs} | 0 Thirdweb.Tests/Thirdweb.Storage.Tests.cs | 43 +++++++++++ Thirdweb/Thirdweb.Client/ThirdwebClient.cs | 1 + Thirdweb/Thirdweb.Storage/StorageTypes.cs | 11 +++ Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs | 77 +++++++++++++++++++ Thirdweb/Thirdweb.Utils/Constants.cs | 2 + Thirdweb/Thirdweb.Utils/Utils.cs | 34 ++++++++ 11 files changed, 178 insertions(+) rename Thirdweb.Tests/{ClientTests.cs => Thirdweb.Client.Tests.cs} (100%) rename Thirdweb.Tests/{RpcTests.cs => Thirdweb.RPC.Tests.cs} (100%) create mode 100644 Thirdweb.Tests/Thirdweb.Storage.Tests.cs create mode 100644 Thirdweb/Thirdweb.Storage/StorageTypes.cs create mode 100644 Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml index 6b5809d..307a642 100644 --- a/.github/workflows/dotnet-ci.yml +++ b/.github/workflows/dotnet-ci.yml @@ -32,3 +32,5 @@ jobs: shell: bash env: THIRDWEB_SECRET_KEY: ${{ secrets.THIRDWEB_SECRET_KEY }} + THIRDWEB_CLIENT_ID_BUNDLE_ID_ONLY: ${{ secrets.THIRDWEB_CLIENT_ID_BUNDLE_ID_ONLY }} + THIRDWEB_BUNDLE_ID_BUNDLE_ID_ONLY: ${{ secrets.THIRDWEB_BUNDLE_ID_BUNDLE_ID_ONLY }} diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index 6af7d5c..aa66567 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -36,6 +36,10 @@ private static async Task Main(string[] args) await privateKeyAccount.Connect(); await embeddedAccount.Connect(); + // Reset embedded account + if (await embeddedAccount.IsConnected()) + await embeddedAccount.Disconnect(); + // Relog if embedded account not logged in if (!await embeddedAccount.IsConnected()) { diff --git a/Thirdweb.Tests/BaseTests.cs b/Thirdweb.Tests/BaseTests.cs index f004f7f..741fa02 100644 --- a/Thirdweb.Tests/BaseTests.cs +++ b/Thirdweb.Tests/BaseTests.cs @@ -6,12 +6,16 @@ public class BaseTests { protected readonly ITestOutputHelper _output; protected readonly string? _secretKey; + protected readonly string? _clientIdBundleIdOnly; + protected readonly string? _bundleIdBundleIdOnly; public BaseTests(ITestOutputHelper output) { DotEnv.Load(); _output = output; _secretKey = Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY"); + _clientIdBundleIdOnly = Environment.GetEnvironmentVariable("THIRDWEB_CLIENT_ID_BUNDLE_ID_ONLY"); + _bundleIdBundleIdOnly = Environment.GetEnvironmentVariable("THIRDWEB_BUNDLE_ID_BUNDLE_ID_ONLY"); } [Fact] diff --git a/Thirdweb.Tests/ClientTests.cs b/Thirdweb.Tests/Thirdweb.Client.Tests.cs similarity index 100% rename from Thirdweb.Tests/ClientTests.cs rename to Thirdweb.Tests/Thirdweb.Client.Tests.cs diff --git a/Thirdweb.Tests/RpcTests.cs b/Thirdweb.Tests/Thirdweb.RPC.Tests.cs similarity index 100% rename from Thirdweb.Tests/RpcTests.cs rename to Thirdweb.Tests/Thirdweb.RPC.Tests.cs diff --git a/Thirdweb.Tests/Thirdweb.Storage.Tests.cs b/Thirdweb.Tests/Thirdweb.Storage.Tests.cs new file mode 100644 index 0000000..616190e --- /dev/null +++ b/Thirdweb.Tests/Thirdweb.Storage.Tests.cs @@ -0,0 +1,43 @@ +namespace Thirdweb.Tests; + +public class StorageTests : BaseTests +{ + public StorageTests(ITestOutputHelper output) + : base(output) { } + + [Fact] + public async Task DownloadTest_SecretKey() + { + var client = new ThirdwebClient(secretKey: _secretKey); + var res = await ThirdwebStorage.Download(client, "https://1.rpc.thirdweb.com/providers"); + Assert.NotNull(res); + } + + [Fact] + public async Task DownloadTest_Client_BundleId() + { + var client = new ThirdwebClient(clientId: _clientIdBundleIdOnly, bundleId: _bundleIdBundleIdOnly); + var res = await ThirdwebStorage.Download(client, "https://1.rpc.thirdweb.com/providers"); + Assert.NotNull(res); + } + + [Fact] + public async Task UploadTest_SecretKey() + { + var client = new ThirdwebClient(secretKey: _secretKey); + var path = Path.Combine(Path.GetTempPath(), "testJson.json"); + File.WriteAllText(path, "{\"test\": \"test\"}"); + var res = await ThirdwebStorage.Upload(client, path); + Assert.StartsWith($"https://{client.ClientId}.ipfscdn.io/ipfs/", res.PreviewUrl); + } + + [Fact] + public async Task UploadTest_Client_BundleId() + { + var client = new ThirdwebClient(clientId: _clientIdBundleIdOnly, bundleId: _bundleIdBundleIdOnly); + var path = Path.Combine(Path.GetTempPath(), "testJson.json"); + File.WriteAllText(path, "{\"test\": \"test\"}"); + var res = await ThirdwebStorage.Upload(client, path); + Assert.StartsWith($"https://{client.ClientId}.ipfscdn.io/ipfs/", res.PreviewUrl); + } +} diff --git a/Thirdweb/Thirdweb.Client/ThirdwebClient.cs b/Thirdweb/Thirdweb.Client/ThirdwebClient.cs index cc0cacb..6cefd7c 100644 --- a/Thirdweb/Thirdweb.Client/ThirdwebClient.cs +++ b/Thirdweb/Thirdweb.Client/ThirdwebClient.cs @@ -1,4 +1,5 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Thirdweb.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Thirdweb.Console")] namespace Thirdweb { diff --git a/Thirdweb/Thirdweb.Storage/StorageTypes.cs b/Thirdweb/Thirdweb.Storage/StorageTypes.cs new file mode 100644 index 0000000..164e09a --- /dev/null +++ b/Thirdweb/Thirdweb.Storage/StorageTypes.cs @@ -0,0 +1,11 @@ +namespace Thirdweb +{ + [System.Serializable] + public struct IPFSUploadResult + { + public string IpfsHash; + public string PinSize; + public string Timestamp; + public string PreviewUrl; + } +} diff --git a/Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs b/Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs new file mode 100644 index 0000000..a0e8de5 --- /dev/null +++ b/Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs @@ -0,0 +1,77 @@ +using Newtonsoft.Json; + +namespace Thirdweb +{ + public class ThirdwebStorage + { + public static async Task Download(ThirdwebClient client, string uri, int? requestTimeout = null) + { + if (string.IsNullOrEmpty(uri)) + { + throw new ArgumentNullException(nameof(uri)); + } + + uri = uri.ReplaceIPFS(string.IsNullOrEmpty(client.ClientId) ? Constants.FALLBACK_IPFS_GATEWAY : $"https://{client.ClientId}.ipfscdn.io/ipfs/"); + + using var httpClient = new HttpClient(); + + var isThirdwebRequest = new Uri(uri).Host.EndsWith(".ipfscdn.io"); + if (isThirdwebRequest) + { + var headers = Utils.GetThirdwebHeaders(client); + foreach (var header in headers) + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + + requestTimeout ??= client.FetchTimeoutOptions.GetTimeout(TimeoutType.Storage); + + var response = await httpClient.GetAsync(uri, new CancellationTokenSource(requestTimeout.Value).Token); + + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Failed to download {uri}: {response.StatusCode} | {response.ReasonPhrase} | {await response.Content.ReadAsStringAsync()}"); + } + + var content = await response.Content.ReadAsStringAsync(); + + return typeof(T) == typeof(string) ? (T)(object)content : JsonConvert.DeserializeObject(content); + } + + public static async Task Upload(ThirdwebClient client, string path) + { + if (string.IsNullOrEmpty(client.ClientId)) + { + throw new UnauthorizedAccessException("You cannot use Upload features without setting a Client ID."); + } + + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + using var httpClient = new HttpClient(); + using var form = new MultipartFormDataContent { { new ByteArrayContent(File.ReadAllBytes(path)), "file", Path.GetFileName(path) } }; + + var headers = Utils.GetThirdwebHeaders(client); + foreach (var header in headers) + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + + var response = await httpClient.PostAsync(Constants.PIN_URI, form); + + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Failed to upload {path}: {response.StatusCode} | {response.ReasonPhrase} | {await response.Content.ReadAsStringAsync()}"); + } + + var result = await response.Content.ReadAsStringAsync(); + + var res = JsonConvert.DeserializeObject(result); + res.PreviewUrl = $"https://{client.ClientId}.ipfscdn.io/ipfs/{res.IpfsHash}"; + return res; + } + } +} diff --git a/Thirdweb/Thirdweb.Utils/Constants.cs b/Thirdweb/Thirdweb.Utils/Constants.cs index edb0196..9770af4 100644 --- a/Thirdweb/Thirdweb.Utils/Constants.cs +++ b/Thirdweb/Thirdweb.Utils/Constants.cs @@ -12,5 +12,7 @@ public static class Constants internal const string DUMMY_SIG = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; internal const string DUMMY_PAYMASTER_AND_DATA_HEX = "0x0101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000001010101010100000000000000000000000000000000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"; + internal const string FALLBACK_IPFS_GATEWAY = "https://ipfs.io/ipfs/"; + internal const string PIN_URI = "https://storage.thirdweb.com/ipfs/upload"; } } diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs index 6c60df7..8db893e 100644 --- a/Thirdweb/Thirdweb.Utils/Utils.cs +++ b/Thirdweb/Thirdweb.Utils/Utils.cs @@ -95,5 +95,39 @@ public static long GetUnixTimeStampIn10Years() { return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60 * 60 * 24 * 365 * 10; } + + public static string ReplaceIPFS(this string uri, string gateway = null) + { + gateway ??= Constants.FALLBACK_IPFS_GATEWAY; + return !string.IsNullOrEmpty(uri) && uri.StartsWith("ipfs://") ? uri.Replace("ipfs://", gateway) : uri; + } + + public static Dictionary GetThirdwebHeaders(ThirdwebClient client) + { + var headers = new Dictionary + { + { "x-sdk-name", "Thirdweb.NET" }, + { "x-sdk-os", System.Runtime.InteropServices.RuntimeInformation.OSDescription }, + { "x-sdk-platform", "dotnet" }, + { "x-sdk-version", Constants.VERSION } + }; + + if (!string.IsNullOrEmpty(client.ClientId)) + { + headers.Add("x-client-id", client.ClientId); + } + + if (!string.IsNullOrEmpty(client.SecretKey)) + { + headers.Add("x-secret-key", client.SecretKey); + } + + if (!string.IsNullOrEmpty(client.BundleId)) + { + headers.Add("x-bundle-id", client.BundleId); + } + + return headers; + } } }