Implemente OpenSea importer

This commit is contained in:
Dario Gabriel Lipicar 2024-03-21 08:43:59 -03:00 committed by dlipicar
parent 7cad03ee70
commit 800cfb3af3
26 changed files with 681 additions and 10 deletions

View File

@ -8,4 +8,6 @@ public interface IToken
public DateTime CreatedAt { get; }
public ITokenMedia MainFile { get; }
public ITokenMedia CoverFile { get; }
public Guid? ImporterId { get; set; }
public string Location { get; }
}

View File

@ -10,4 +10,6 @@ public class Token : IToken
public DateTime CreatedAt { get; set; } = DateTime.Now;
public ITokenMedia MainFile { get; set; }
public ITokenMedia CoverFile { get; set; }
public Guid? ImporterId { get; set; }
public string Location { get; set; } = "";
}

View File

@ -0,0 +1,8 @@
using NftFaucet.Domain.Models.Abstraction;
namespace NftFaucet.Domain.Services;
public interface ITokenMediaDownloader
{
public Task<ITokenMedia> Download(Uri location);
}

View File

@ -0,0 +1,30 @@
using System.Net.Http;
using NftFaucet.Domain.Models;
using NftFaucet.Domain.Models.Abstraction;
namespace NftFaucet.Domain.Services;
public class TokenMediaDownloader : ITokenMediaDownloader
{
public async Task<ITokenMedia> Download(Uri location)
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(location);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"TokenMediaDownloader Status: {(int) response.StatusCode}. Reason: {response.ReasonPhrase}");
}
var fileData = await response.Content.ReadAsByteArrayAsync();
var fileName = location.Segments[^1];
var fileType = response.Content.Headers.ContentType.MediaType;
var fileSize = fileData.Length;
return new TokenMedia
{
FileName = fileName,
FileType = fileType,
FileData = $"data:{fileType};base64,{Convert.ToBase64String(fileData)}",
FileSize = fileSize,
};
}
}

View File

@ -0,0 +1,7 @@
namespace NftFaucet.Infrastructure.Models.Dto;
public class ImporterStateDto
{
public Guid Id { get; set; }
public string State { get; set; }
}

View File

@ -9,4 +9,5 @@ public class PluginStateStorage
public ICollection<IWallet> Wallets { get; set; }
public ICollection<IUploader> Uploaders { get; set; }
public ICollection<IContract> Contracts { get; set; }
public ICollection<IImporter> Importers { get; set; }
}

View File

@ -12,12 +12,14 @@ public interface IStateRepository
public Task SaveUploadLocation(ITokenUploadLocation uploadLocation);
public Task SaveWalletState(IWallet wallet);
public Task SaveUploaderState(IUploader uploader);
public Task SaveImporterState(IImporter importer);
public Task LoadAppState(ScopedAppState appState);
public Task<IToken[]> LoadTokens();
public Task<ITokenUploadLocation[]> LoadUploadLocations();
public Task<UploaderStateDto[]> LoadUploaderStates();
public Task<WalletStateDto[]> LoadWalletStates();
public Task<ImporterStateDto[]> LoadImporterStates();
public Task DeleteTokenLocation(Guid uploadLocationId);
public Task DeleteToken(Guid tokenId);

View File

@ -16,6 +16,7 @@ public class StateRepository : IStateRepository
private const string UploadLocationsStoreName = "UploadLocations";
private const string WalletStatesStoreName = "WalletStates";
private const string UploaderStatesStoreName = "UploaderStates";
private const string ImporterStatesStoreName = "ImporterStates";
public StateRepository(IndexedDBManager dbManager, Mapper mapper)
{
@ -167,6 +168,40 @@ public class StateRepository : IStateRepository
}
}
public async Task<ImporterStateDto[]> LoadImporterStates()
{
var existingImporterStates = await _dbManager.GetRecords<ImporterStateDto>(ImporterStatesStoreName);
if (existingImporterStates == null || existingImporterStates.Count == 0)
return Array.Empty<ImporterStateDto>();
return existingImporterStates.ToArray();
}
public async Task SaveImporterState(IImporter importer)
{
var state = await importer.GetState();
var stateDto = new ImporterStateDto
{
Id = importer.Id,
State = state,
};
var record = new StoreRecord<ImporterStateDto>
{
Storename = ImporterStatesStoreName,
Data = stateDto,
};
var existingStateDto = await _dbManager.GetRecordById<Guid, ImporterStateDto>(ImporterStatesStoreName, stateDto.Id);
if (existingStateDto == null)
{
await _dbManager.AddRecord(record);
}
else
{
await _dbManager.UpdateRecord(record);
}
}
public async Task<WalletStateDto[]> LoadWalletStates()
{
var existingWalletStates = await _dbManager.GetRecords<WalletStateDto>(WalletStatesStoreName);

View File

@ -0,0 +1,8 @@
using NftFaucet.Plugins.Models.Abstraction;
namespace NftFaucet.Plugins;
public interface IImporterPlugin
{
public IReadOnlyCollection<IImporter> Importers { get; }
}

View File

@ -0,0 +1,10 @@
using System.Numerics;
using NftFaucet.Domain.Models.Abstraction;
namespace NftFaucet.Plugins.Models.Abstraction;
public interface IImporter : INamedEntity, IEntityWithOrder, IStateful, IInitializable, IEntityWithProperties, IConfigurable
{
public Task<IToken> Import(ulong chainId, string contractAddress, BigInteger tokenId);
public bool IsChainIDSupported(ulong chainId);
}

View File

@ -0,0 +1,11 @@
using System.Numerics;
using NftFaucet.Domain.Models.Abstraction;
using NftFaucet.Plugins.Models.Abstraction;
namespace NftFaucet.Plugins.Models;
public abstract class Importer : DefaultEntity, IImporter
{
public abstract Task<IToken> Import(ulong chainId, string contractAddress, BigInteger tokenId);
public abstract bool IsChainIDSupported(ulong chainId);
}

View File

@ -1,5 +1,8 @@

Microsoft Visual Studio Solution File, Format Version 12.00
#
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion =
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NftFaucet", "NftFaucet\NftFaucet.csproj", "{E113DAEE-A1E4-4BE2-8CDA-6E06245A471A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NftFaucet.Plugins", "NftFaucet.Plugins\NftFaucet.Plugins.csproj", "{CF83F0DB-41CD-46AA-8696-8351E098986D}"
@ -44,6 +47,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NftFaucet.NetworkPlugins.So
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NftFaucet.Infrastructure", "NftFaucet.Infrastructure\NftFaucet.Infrastructure.csproj", "{06949AF1-D775-4E6B-A309-C358B05F4D50}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ImportPlugins", "ImportPlugins", "{BE7D3C27-91BF-4EC0-A9C8-462B3FC6C9EC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NftFaucet.ImportPlugins.OpenSea", "plugins\import-plugins\NftFaucet.ImportPlugins.OpenSea\NftFaucet.ImportPlugins.OpenSea.csproj", "{BC5740C3-D439-413C-8CA0-4D0EED720513}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -122,6 +129,13 @@ Global
{06949AF1-D775-4E6B-A309-C358B05F4D50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{06949AF1-D775-4E6B-A309-C358B05F4D50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06949AF1-D775-4E6B-A309-C358B05F4D50}.Release|Any CPU.Build.0 = Release|Any CPU
{BC5740C3-D439-413C-8CA0-4D0EED720513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC5740C3-D439-413C-8CA0-4D0EED720513}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC5740C3-D439-413C-8CA0-4D0EED720513}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC5740C3-D439-413C-8CA0-4D0EED720513}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E29C020E-A3FD-4756-ACB3-3828F915EB40} = {926C18AE-808E-4E77-985F-4EF54792D523}
@ -141,5 +155,7 @@ Global
{5EAB42A1-4C5B-407A-AE4E-34F608365760} = {E29C020E-A3FD-4756-ACB3-3828F915EB40}
{DD751505-81EC-4B8D-B2A5-FB246DCB68E4} = {E29C020E-A3FD-4756-ACB3-3828F915EB40}
{A2EE3F83-C57C-48B1-A5FE-E573284B6661} = {E29C020E-A3FD-4756-ACB3-3828F915EB40}
{BE7D3C27-91BF-4EC0-A9C8-462B3FC6C9EC} = {926C18AE-808E-4E77-985F-4EF54792D523}
{BC5740C3-D439-413C-8CA0-4D0EED720513} = {BE7D3C27-91BF-4EC0-A9C8-462B3FC6C9EC}
EndGlobalSection
EndGlobal

View File

@ -38,6 +38,7 @@
<ProjectReference Include="..\plugins\upload-plugins\NftFaucet.UploadPlugins.Crust\NftFaucet.UploadPlugins.Crust.csproj" />
<ProjectReference Include="..\plugins\upload-plugins\NftFaucet.UploadPlugins.Infura\NftFaucet.UploadPlugins.Infura.csproj" />
<ProjectReference Include="..\plugins\upload-plugins\NftFaucet.UploadPlugins.NftStorage\NftFaucet.UploadPlugins.NftStorage.csproj" />
<ProjectReference Include="..\plugins\import-plugins\NftFaucet.ImportPlugins.OpenSea\NftFaucet.ImportPlugins.OpenSea.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,26 @@
@page "/tokens/import"
@inherits BasicComponent
<PageTitle>Import new token</PageTitle>
<RadzenContent Container="main">
<h3>Select importer</h3>
<CardList Data="@ImporterCards" @bind-SelectedItems="@SelectedImporterIds"/>
<div class="mb-4">
<RadzenText TextStyle="TextStyle.H6" Style="display: inline;">ChainID</RadzenText><text style="color: red"> *</text>
<RadzenDropDown TextProperty="Name" ValueProperty="ChainId" Data="@SupportedNetworks" @bind-Value="@Model.ChainId" Class="w-100"/>
</div>
<div class="mb-4">
<RadzenText TextStyle="TextStyle.H6" Style="display: inline;">Contract Address</RadzenText><text style="color: red"> *</text>
<RadzenTextBox Placeholder="0x06012c8cf97BEaD5deAe237070F9587f8E7A266d" MaxLength="42" @bind-Value="@Model.ContractAddress" Class="w-100"/>
</div>
<div class="mb-4">
<RadzenText TextStyle="TextStyle.H6" Style="display: inline;">TokenID</RadzenText><text style="color: red"> *</text>
<RadzenTextBox Placeholder="0" MaxLength="78" @bind-Value="@Model.TokenId" Class="w-100"/>
</div>
<div class="row">
<div class="col-md-12 text-right">
<RadzenButton Text="Cancel" Click="@(args => DialogService.Close())" ButtonStyle="ButtonStyle.Secondary" Disabled="IsImporting" Style="width: 120px" Class="mr-1"/>
<RadzenButton Text="Import" Icon="eject" BusyText="Importing..." IsBusy=@IsImporting Click="@(async args => await OnImportPressed())" Disabled="@(SelectedImporter == null || !SelectedImporter.IsConfigured)" Style="width: 180px"/>
</div>
</div>
</RadzenContent>

View File

@ -0,0 +1,162 @@
using System.Numerics;
using CSharpFunctionalExtensions;
using Nethereum.Util;
using NftFaucet.Components;
using NftFaucet.Components.CardList;
using NftFaucet.Domain.Utils;
using NftFaucet.Plugins.Models;
using NftFaucet.Plugins.Models.Abstraction;
using Radzen;
#pragma warning disable CS8974
namespace NftFaucet.Pages;
public partial class ImportTokenDialog : BasicComponent
{
private TokenModel Model { get; set; } = new TokenModel();
private bool ModelIsValid => IsValid();
protected override void OnInitialized()
{
base.OnInitialized();
RefreshCards();
}
private CardListItem[] ImporterCards { get; set; }
private Guid[] SelectedImporterIds { get; set; }
private IImporter SelectedImporter => AppState?.PluginStorage?.Importers?.FirstOrDefault(x => x.Id == SelectedImporterIds?.FirstOrDefault());
private bool IsImporting { get; set; }
private void RefreshCards()
{
ImporterCards = AppState.PluginStorage.Importers.OrderBy(x => x.Order ?? int.MaxValue).Select(MapCardListItem).ToArray();
RefreshMediator.NotifyStateHasChangedSafe();
}
private CardListItem MapCardListItem(IImporter model)
{
var configurationItems = model.GetConfigurationItems();
return new CardListItem
{
Id = model.Id,
ImageLocation = model.ImageName != null ? "./images/" + model.ImageName : null,
Header = model.Name,
Properties = model.GetProperties().Select(Map).ToArray(),
IsDisabled = !model.IsSupported,
SelectionIcon = model.IsConfigured ? CardListItemSelectionIcon.Checkmark : CardListItemSelectionIcon.Warning,
Badges = new[]
{
(Settings?.RecommendedUploaders?.Contains(model.Id) ?? false)
? new CardListItemBadge {Style = BadgeStyle.Success, Text = "Recommended"}
: null,
!model.IsSupported ? new CardListItemBadge { Style = BadgeStyle.Light, Text = "Not Supported" } : null,
model.IsDeprecated ? new CardListItemBadge { Style = BadgeStyle.Warning, Text = "Deprecated" } : null,
}.Where(x => x != null).ToArray(),
Buttons = configurationItems != null && configurationItems.Any()
? new[]
{
new CardListItemButton
{
Icon = "build",
Style = ButtonStyle.Secondary,
Action = async () =>
{
var result = await OpenConfigurationDialog(model);
RefreshCards();
if (result.IsSuccess)
{
await StateRepository.SaveImporterState(model);
}
}
}
}
: Array.Empty<CardListItemButton>(),
};
}
private List<INetwork> SupportedNetworks => AppState.PluginStorage.Networks.Where(x => SelectedImporter?.IsChainIDSupported(x.ChainId ?? 0) ?? false).ToList();
private async Task OnImportPressed()
{
if (!IsValid())
return;
IsImporting = true;
RefreshMediator.NotifyStateHasChangedSafe();
var importResult = await ResultWrapper.Wrap(() => SelectedImporter.Import(Model.ChainId, Model.ContractAddress, BigInteger.Parse(Model.TokenId)));
IsImporting = false;
RefreshMediator.NotifyStateHasChangedSafe();
if (importResult.IsFailure)
{
NotificationService.Notify(NotificationSeverity.Error, "Failed to import token", importResult.Error);
return;
}
DialogService.Close(importResult.Value);
}
private async Task<Result> OpenConfigurationDialog(IImporter importer)
{
var configurationItems = importer.GetConfigurationItems();
foreach (var configurationItem in configurationItems)
{
var prevClickHandler = configurationItem.ClickAction;
if (prevClickHandler != null)
{
configurationItem.ClickAction = () =>
{
prevClickHandler();
RefreshMediator.NotifyStateHasChangedSafe();
};
}
}
var result = (bool?) await DialogService.OpenAsync<ConfigurationDialog>("Configuration",
new Dictionary<string, object>
{
{ nameof(ConfigurationDialog.ConfigurationItems), configurationItems },
{ nameof(ConfigurationDialog.ConfigureAction), importer.Configure },
},
new DialogOptions() {Width = "700px", Height = "570px", Resizable = true, Draggable = true});
return result != null && result.Value ? Result.Success() : Result.Failure("Operation cancelled");
}
private CardListItemProperty Map(Property model)
=> model == null ? null : new CardListItemProperty
{
Name = model.Name,
Value = model.Value,
ValueColor = model.ValueColor,
Link = model.Link,
};
private class TokenModel
{
public ulong ChainId { get; set; }
public string ContractAddress { get; set; }
public string TokenId { get; set; }
}
private bool IsValid()
{
if (Model.ChainId == 0)
return false;
var addrValidator = new AddressUtil();
if (!addrValidator.IsValidEthereumAddressHexFormat(Model.ContractAddress))
return false;
try {
var tokenId = BigInteger.Parse(Model.TokenId);
} catch {
return false;
}
return true;
}
}

View File

@ -5,14 +5,8 @@
<RadzenContent Container="main">
<RadzenHeading Size="H1" Text="Select or create a token to mint" />
<div style="width: 100%; display: flex; flex-direction: row; justify-content: end;">
<RadzenSplitButton Text="Import from..." Icon="backup" Style="margin-right: 1rem;" Click="@(() => NotificationService.Notify(NotificationSeverity.Warning, "NOT IMPLEMENTED", "Will be implemented later"))" >
<ChildContent>
<RadzenSplitButtonItem Text="OpenSea" Value="1" />
<RadzenSplitButtonItem Text="Rarible" Value="2" />
<RadzenSplitButtonItem Text="Nifty" Value="3" />
<RadzenSplitButtonItem Text="SuperRare" Value="4" />
</ChildContent>
</RadzenSplitButton>
<RadzenButton Text="Import" Icon="backup" Style="margin-right: 1rem;" ButtonStyle="ButtonStyle.Secondary"
Click="@OpenImportTokenDialog"/>
<RadzenButton Text="Create New" Icon="add_circle_outline" ButtonStyle="ButtonStyle.Secondary"
Click="@OpenCreateTokenDialog"/>
</div>

View File

@ -82,6 +82,27 @@ public partial class TokensPage : BasicComponent
await SaveAppState();
}
private async Task OpenImportTokenDialog()
{
var token = (IToken) await DialogService.OpenAsync<ImportTokenDialog>("Import token",
new Dictionary<string, object>(),
new DialogOptions() { Width = "1000px", Height = "700px", Resizable = true, Draggable = true });
if (token == null)
{
return;
}
AppState.UserStorage.Tokens ??= new List<IToken>();
AppState.UserStorage.Tokens.Add(token);
AppState.UserStorage.SelectedTokens = new[] { token.Id };
AppState.UserStorage.SelectedUploadLocations = Array.Empty<Guid>();
RefreshCards();
RefreshMediator.NotifyStateHasChangedSafe();
await StateRepository.SaveToken(token);
await SaveAppState();
}
private async Task OnTokenChange()
{
AppState.UserStorage.SelectedUploadLocations = Array.Empty<Guid>();

View File

@ -114,6 +114,21 @@ builder.Services.AddIndexedDB(dbStore =>
});
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<IndexedDBManager>();
await db.AddNewStore(new StoreSchema
{
Name = "ImporterStates",
PrimaryKey = new IndexSpec { Name = "id", KeyPath = "id", Auto = true },
Indexes = new List<IndexSpec>
{
new IndexSpec {Name = "state", KeyPath = "state", Auto = false},
}
});
}
var initializationService = app.Services.GetRequiredService<IInitializationService>();
await initializationService.Initialize();
await app.RunAsync();

View File

@ -40,12 +40,14 @@ public class InitializationService : IInitializationService
var isFirstRun = _appState.PluginStorage.Networks == null &&
_appState.PluginStorage.Wallets == null &&
_appState.PluginStorage.Uploaders == null &&
_appState.PluginStorage.Contracts == null;
_appState.PluginStorage.Contracts == null &&
_appState.PluginStorage.Importers == null;
_appState.PluginStorage.Networks ??= _pluginLoader.NetworkPlugins.SelectMany(x => x.Networks).Where(x => x != null).ToArray();
_appState.PluginStorage.Wallets ??= _pluginLoader.WalletPlugins.SelectMany(x => x.Wallets).Where(x => x != null).ToArray();
_appState.PluginStorage.Uploaders ??= _pluginLoader.UploaderPlugins.SelectMany(x => x.Uploaders).Where(x => x != null).ToArray();
_appState.PluginStorage.Contracts ??= _appState.PluginStorage.Networks.SelectMany(x => x.DeployedContracts).Where(x => x != null).ToArray();
_appState.PluginStorage.Importers ??= _pluginLoader.ImporterPlugins.SelectMany(x => x.Importers).Where(x => x != null).ToArray();
if (isFirstRun)
{
@ -78,6 +80,16 @@ public class InitializationService : IInitializationService
}
await uploader.SetState(uploaderState.State);
}
var importerStates = await _stateRepository.LoadImporterStates();
foreach (var importerState in importerStates)
{
var importer = _appState.PluginStorage.Importers.FirstOrDefault(x => x.Id == importerState.Id);
if (importer == null)
{
continue;
}
await importer.SetState(importerState.State);
}
}
private void ValidatePluginsData()
@ -86,6 +98,7 @@ public class InitializationService : IInitializationService
var walletIds = _appState.PluginStorage.Wallets.Select(x => x.Id).ToArray();
var uploaderIds = _appState.PluginStorage.Uploaders.Select(x => x.Id).ToArray();
var contractIds = _appState.PluginStorage.Contracts.Select(x => x.Id).ToArray();
var importerIds = _appState.PluginStorage.Importers.Select(x => x.Id).ToArray();
var networkIdDuplicates = networkIds.Duplicates().ToArray();
if (networkIdDuplicates.Any())
@ -111,7 +124,13 @@ public class InitializationService : IInitializationService
throw new ApplicationException($"[{nameof(ValidatePluginsData)}] There are contracts with same ids: {string.Join(", ", contractIdDuplicates)}");
}
var allIds = networkIds.Concat(walletIds).Concat(uploaderIds).Concat(contractIds).ToArray();
var importerIdDuplicates = importerIds.Duplicates().ToArray();
if (importerIdDuplicates.Any())
{
throw new ApplicationException($"[{nameof(ValidatePluginsData)}] There are importers with same ids: {string.Join(", ", importerIdDuplicates)}");
}
var allIds = networkIds.Concat(walletIds).Concat(uploaderIds).Concat(contractIds).Concat(importerIds).ToArray();
var allIdDuplicates = allIds.Duplicates().ToArray();
if (allIdDuplicates.Any())
{
@ -152,5 +171,13 @@ public class InitializationService : IInitializationService
{
throw new ApplicationException($"[{nameof(ValidatePluginsData)}] There are contracts with same deployment datetime: {string.Join(", ", txDeploymentDateDuplicates)}");
}
var importerShortNames = _appState.PluginStorage.Importers.Select(x => x.ShortName).Where(x => x != null).ToArray();
var importerShortNameDuplicates = importerShortNames.Duplicates().ToArray();
if (importerShortNameDuplicates.Any())
{
throw new ApplicationException($"[{nameof(ValidatePluginsData)}] There are importers with same short name: {string.Join(", ", importerShortNameDuplicates)}");
}
}
}

View File

@ -1,3 +1,4 @@
using NftFaucet.ImportPlugins.OpenSea;
using NftFaucet.NetworkPlugins.Arbitrum;
using NftFaucet.NetworkPlugins.Avalanche;
using NftFaucet.NetworkPlugins.BinanceSmartChain;
@ -43,4 +44,9 @@ public class PluginLoader
new NftStorageUploaderPlugin(),
new CrustUploaderPlugin(),
};
public IReadOnlyCollection<IImporterPlugin> ImporterPlugins { get; } = new IImporterPlugin[]
{
new OpenSeaImporterPlugin(),
};
}

View File

@ -0,0 +1,17 @@
<svg width="360" height="360" viewBox="0 0 360 360" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2_57)">
<g clip-path="url(#clip1_2_57)">
<path d="M252.072 212.292C245.826 220.662 232.686 234.558 225.378 234.558H191.412V212.274H218.466C222.336 212.274 226.026 210.708 228.69 207.954C242.586 193.554 250.614 176.418 250.614 158.04C250.614 126.684 227.178 98.964 191.394 82.26V67.284C191.394 60.84 186.174 55.62 179.73 55.62C173.286 55.62 168.066 60.84 168.066 67.284V73.494C158.04 70.56 147.42 68.328 136.332 67.05C154.692 86.994 165.906 113.67 165.906 142.92C165.906 169.146 156.942 193.23 141.876 212.31H168.066V234.63H129.726C124.542 234.63 120.33 230.436 120.33 225.234V215.478C120.33 213.768 118.944 212.364 117.216 212.364H66.672C65.682 212.364 64.836 213.174 64.836 214.164C64.8 254.088 96.39 284.058 134.172 284.058H240.822C266.382 284.058 277.812 251.298 292.788 230.454C298.602 222.39 312.552 215.91 316.782 214.11C317.556 213.786 318.006 213.066 318.006 212.22V199.26C318.006 197.946 316.71 196.956 315.432 197.316C315.432 197.316 253.782 211.482 253.062 211.68C252.342 211.896 252.072 212.31 252.072 212.31V212.292Z" fill="white"/>
<path d="M146.16 142.83C146.16 122.724 139.266 104.22 127.746 89.586L69.732 189.972H132.138C141.012 176.436 146.178 160.236 146.178 142.848L146.16 142.83Z" fill="white"/>
<path d="M181.566 -5.19844e-06C80.91 -0.828005 -0.82799 80.91 1.00604e-05 181.566C0.84601 279.306 80.694 359.172 178.416 359.982C279.072 360.846 360.846 279.072 359.982 178.416C359.172 80.712 279.306 0.845995 181.566 -5.19844e-06ZM127.746 89.586C139.266 104.22 146.16 122.742 146.16 142.83C146.16 160.236 140.994 176.436 132.12 189.954H69.714L127.728 89.568L127.746 89.586ZM318.006 199.242V212.202C318.006 213.048 317.556 213.768 316.782 214.092C312.552 215.892 298.602 222.372 292.788 230.436C277.812 251.28 266.382 284.04 240.822 284.04H134.172C96.408 284.04 64.818 254.07 64.836 214.146C64.836 213.156 65.682 212.346 66.672 212.346H117.216C118.962 212.346 120.33 213.75 120.33 215.46V225.216C120.33 230.4 124.524 234.612 129.726 234.612H168.066V212.292H141.876C156.942 193.212 165.906 169.128 165.906 142.902C165.906 113.652 154.692 86.976 136.332 67.032C147.438 68.328 158.058 70.542 168.066 73.476V67.266C168.066 60.822 173.286 55.602 179.73 55.602C186.174 55.602 191.394 60.822 191.394 67.266V82.242C227.178 98.946 250.614 126.666 250.614 158.022C250.614 176.418 242.568 193.536 228.69 207.936C226.026 210.69 222.336 212.256 218.466 212.256H191.412V234.54H225.378C232.704 234.54 245.844 220.644 252.072 212.274C252.072 212.274 252.342 211.86 253.062 211.644C253.782 211.428 315.432 197.28 315.432 197.28C316.728 196.92 318.006 197.91 318.006 199.224V199.242Z" fill="#0086FF"/>
</g>
</g>
<defs>
<clipPath id="clip0_2_57">
<rect width="360" height="360" fill="white"/>
</clipPath>
<clipPath id="clip1_2_57">
<rect width="360" height="360" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations;
using System.Numerics;
using NftFaucet.ImportPlugins.OpenSea.Models;
using RestEase;
namespace NftFaucet.ImportPlugins.OpenSea.ApiClients;
public enum Chains
{
[Display(Name = "arbitrum")]
Arbitrum = 42161,
[Display(Name = "arbitrum_sepolia")]
ArbitrumSepolia = 421614,
[Display(Name = "ethereum")]
Ethereum = 1,
[Display(Name = "ethereum_sepolia")]
EthereumSepolia = 11155111,
[Display(Name = "optimism")]
Optimism = 10,
[Display(Name = "optimism_sepolia")]
OptimismSepolia = 11155420,
}
[BasePath("/api/v2")]
public interface IOpenSeaApiClient
{
[Header("x-api-key")]
public string Auth { get; set; }
[Get("chain/{chain}/contract/{address}/nfts/{tokenId}")]
[AllowAnyStatusCode]
Task<Response<GetNFTResponse>> GetNFT([Path(PathSerializationMethod.Serialized)] Chains chain, [Path] string address, [Path] BigInteger tokenId);
}

View File

@ -0,0 +1,24 @@
namespace NftFaucet.ImportPlugins.OpenSea.Models;
public class GetNFTResponse
{
public NFT nft { get; set; }
public class NFT {
public string identifier { get; set; }
public string collection { get; set; }
public string contract { get; set; }
public string token_standard { get; set; }
public string name { get; set; }
public string description { get; set; }
public string image_url { get; set; }
public string metadata_url { get; set; }
public string opensea_url { get; set; }
public string updated_at { get; set; }
public bool is_disabled { get; set; }
public bool is_nsfw { get; set; }
public string animation_url { get; set; }
public bool is_suspicious { get; set; }
public string creator { get; set; }
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\NftFaucet.Plugins\NftFaucet.Plugins.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.14" />
<PackageReference Include="RestEase" Version="1.5.7" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,178 @@
using System.Numerics;
using CSharpFunctionalExtensions;
using NftFaucet.Domain.Models.Abstraction;
using NftFaucet.Domain.Models;
using NftFaucet.Domain.Services;
using NftFaucet.Plugins.Models;
using NftFaucet.Plugins.Models.Enums;
using NftFaucet.ImportPlugins.OpenSea.ApiClients;
using RestEase;
namespace NftFaucet.ImportPlugins.OpenSea;
public class OpenSeaImporter : Importer
{
public override Guid Id { get; } = Guid.Parse("6e0c96e5-7810-4e15-80c0-bfdb21813220");
public override string Name { get; } = "opensea";
public override string ShortName { get; } = "OpenSea";
public override string ImageName { get; } = "opensea.svg";
public override bool IsConfigured { get; protected set; }
public override int? Order { get; } = 1;
private string ApiKey { get; set; }
public override Property[] GetProperties()
=> new Property[]
{
IsConfigured
? new Property {Name = "Configured", Value = "YES", ValueColor = "green"}
: new Property {Name = "Configured", Value = "NO", ValueColor = "red"}
};
public override ConfigurationItem[] GetConfigurationItems()
{
var instructionsText = new ConfigurationItem
{
DisplayType = UiDisplayType.Text,
Name = "Instructions",
Value = "Go to 'https://docs.opensea.io/reference/api-keys', follow instructions to create API key and copy it",
};
var apiKeyInput = new ConfigurationItem
{
DisplayType = UiDisplayType.Input,
Name = "API Key",
Placeholder = "<OpenSea token>",
Value = ApiKey,
};
return new[] { instructionsText, apiKeyInput };
}
public override async Task<Result> Configure(ConfigurationItem[] configurationItems)
{
var apiKey = configurationItems[1].Value;
if (string.IsNullOrEmpty(apiKey))
{
return Result.Failure("API key is null or empty");
}
var apiClient = GetApiClient(Chains.Ethereum, apiKey);
// Get first Cryptokitty to test API key
using var testResponse = await apiClient.GetNFT(Chains.Ethereum, "0x06012c8cf97bead5deae237070f9587f8e7a266d", new BigInteger(1));
if (!testResponse.ResponseMessage.IsSuccessStatusCode)
{
return Result.Failure<Uri>($"Status: {(int) testResponse.ResponseMessage.StatusCode}. Reason: {testResponse.ResponseMessage.ReasonPhrase}");
}
ApiKey = apiKey;
IsConfigured = true;
return Result.Success();
}
public override async Task<IToken> Import(ulong chainId, string contractAddress, BigInteger tokenId)
{
var chain = (Chains) chainId;
var apiClient = GetApiClient(chain, ApiKey) ?? throw new Exception("Unsupported chain");
using var response = await apiClient.GetNFT(chain, contractAddress, tokenId);
if (!response.ResponseMessage.IsSuccessStatusCode)
{
throw new Exception($"Status: {(int) response.ResponseMessage.StatusCode}. Reason: {response.ResponseMessage.ReasonPhrase}");
}
var getNFTResponse = response.GetContent();
if (getNFTResponse == null || getNFTResponse.nft == null || string.IsNullOrEmpty(getNFTResponse.nft.identifier))
{
throw new Exception($"Unexpected response: {response.StringContent}");
}
var nft = getNFTResponse.nft;
var downloader = new TokenMediaDownloader();
var token = new Token
{
Name = nft.name,
Description = nft.description,
ImporterId = Id,
Location = nft.metadata_url,
};
var imageUrls = new List<string> { nft.animation_url, nft.image_url };
foreach (var imageUrl in imageUrls)
{
if (string.IsNullOrEmpty(imageUrl))
continue;
try
{
var file = await downloader.Download(new Uri(imageUrl));
if (file != null) {
token.MainFile = file;
break;
}
}
catch (Exception e)
{
throw new Exception($"Error downloading media: {imageUrl}, {e.Message}");
}
}
return token;
}
public override bool IsChainIDSupported(ulong chainId)
{
var chain = (Chains) chainId;
return chain switch
{
Chains.Ethereum => true,
Chains.Optimism => true,
Chains.Arbitrum => true,
Chains.EthereumSepolia => true,
Chains.OptimismSepolia => true,
Chains.ArbitrumSepolia => true,
_ => false
};
}
public override Task<string> GetState()
=> Task.FromResult(ApiKey);
public override Task SetState(string state)
{
if (string.IsNullOrEmpty(state))
return Task.CompletedTask;
ApiKey = state;
IsConfigured = true;
return Task.CompletedTask;
}
private static IOpenSeaApiClient GetApiClient(Chains chain, string apiKey)
{
switch (chain) {
case Chains.EthereumSepolia:
case Chains.OptimismSepolia:
case Chains.ArbitrumSepolia: {
var importClient = new RestClient("https://testnets-api.opensea.io")
{
RequestPathParamSerializer = new StringEnumRequestPathParamSerializer(),
}.For<IOpenSeaApiClient>();
return importClient;
}
case Chains.Ethereum:
case Chains.Optimism:
case Chains.Arbitrum: {
var importClient = new RestClient("https://api.opensea.io")
{
RequestPathParamSerializer = new StringEnumRequestPathParamSerializer(),
}.For<IOpenSeaApiClient>();
importClient.Auth = apiKey;
return importClient;
}
}
return null;
}
}

View File

@ -0,0 +1,12 @@
using NftFaucet.Plugins;
using NftFaucet.Plugins.Models.Abstraction;
namespace NftFaucet.ImportPlugins.OpenSea;
public class OpenSeaImporterPlugin : IImporterPlugin
{
public IReadOnlyCollection<IImporter> Importers { get; } = new[]
{
new OpenSeaImporter(),
};
}