diff --git a/OneGateApp/Controls/Popups/AddTokenPopup.xaml b/OneGateApp/Controls/Popups/AddTokenPopup.xaml
new file mode 100644
index 0000000..083ba76
--- /dev/null
+++ b/OneGateApp/Controls/Popups/AddTokenPopup.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OneGateApp/Controls/Popups/AddTokenPopup.xaml.cs b/OneGateApp/Controls/Popups/AddTokenPopup.xaml.cs
new file mode 100644
index 0000000..4b0a481
--- /dev/null
+++ b/OneGateApp/Controls/Popups/AddTokenPopup.xaml.cs
@@ -0,0 +1,54 @@
+using Neo;
+using NeoOrder.OneGate.Properties;
+using NeoOrder.OneGate.Services;
+using NeoOrder.OneGate.Services.RPC;
+
+namespace NeoOrder.OneGate.Controls.Popups;
+
+public partial class AddTokenPopup : MyPopup
+{
+ readonly TokenManager tokenManager;
+
+ public string? ContractHash
+ {
+ get;
+ set { field = value; OnPropertyChanged(); ErrorMessage = null; }
+ }
+ public string? ErrorMessage
+ {
+ get;
+ set { field = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasError)); }
+ }
+ public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
+
+ public AddTokenPopup(TokenManager tokenManager)
+ {
+ this.tokenManager = tokenManager;
+ InitializeComponent();
+ }
+
+ async void OnAdd(object sender, EventArgs e)
+ {
+ if (!UInt160.TryParse(ContractHash?.Trim() ?? string.Empty, out UInt160? parsedHash) || parsedHash is null)
+ {
+ ErrorMessage = Strings.InvalidContractHash;
+ return;
+ }
+ UInt160 hash = parsedHash;
+ try
+ {
+ await tokenManager.AddCustomTokenAsync(hash);
+ await CloseAsync(true);
+ }
+ catch (RpcException)
+ {
+ ErrorMessage = Strings.TokenNotFound;
+ }
+ catch
+ {
+ ErrorMessage = Strings.UnknownError;
+ }
+ }
+
+ async void OnCancel(object sender, EventArgs e) => await CloseAsync(false);
+}
diff --git a/OneGateApp/Pages/WalletPage.xaml b/OneGateApp/Pages/WalletPage.xaml
index ecc6e63..4fe7ab2 100644
--- a/OneGateApp/Pages/WalletPage.xaml
+++ b/OneGateApp/Pages/WalletPage.xaml
@@ -13,6 +13,7 @@
+
diff --git a/OneGateApp/Pages/WalletPage.xaml.cs b/OneGateApp/Pages/WalletPage.xaml.cs
index ff3cba8..2528567 100644
--- a/OneGateApp/Pages/WalletPage.xaml.cs
+++ b/OneGateApp/Pages/WalletPage.xaml.cs
@@ -1,7 +1,9 @@
using CommunityToolkit.Maui.Alerts;
+using CommunityToolkit.Maui.Extensions;
using Neo;
using Neo.Wallets;
using NeoOrder.OneGate.Controls;
+using NeoOrder.OneGate.Controls.Popups;
using NeoOrder.OneGate.Data;
using NeoOrder.OneGate.Models;
using NeoOrder.OneGate.Properties;
@@ -13,6 +15,7 @@ public partial class WalletPage : ContentPage
{
readonly ApplicationDbContext dbContext;
readonly TokenManager tokenManager;
+ readonly IServiceProvider serviceProvider;
public LoadingService LoadingService { get; set { field = value; OnPropertyChanged(); } }
public Wallet Wallet { get; set { field = value; OnPropertyChanged(); } }
@@ -23,9 +26,10 @@ public partial class WalletPage : ContentPage
public IReadOnlyList? NFTs { get; set { field = value; OnPropertyChanged(); } }
public string TotalValuation { get; set { field = value; OnPropertyChanged(); } } = "N/A";
- public WalletPage(ApplicationDbContext dbContext, IWalletProvider walletProvider, TokenManager tokenManager)
+ public WalletPage(IServiceProvider serviceProvider, ApplicationDbContext dbContext, IWalletProvider walletProvider, TokenManager tokenManager)
{
this.LoadingService = new(RefreshWallet, LoadAssetsAsync, LoadNFTsAsync);
+ this.serviceProvider = serviceProvider;
this.dbContext = dbContext;
this.tokenManager = tokenManager;
Wallet = walletProvider.GetWallet()!;
@@ -92,4 +96,12 @@ async void OnNFTTapped(object sender, TappedEventArgs e)
NFT nft = (NFT)e.Parameter!;
await Shell.Current.GoToAsync("//wallet/nft/details", new Dictionary { ["nft"] = nft });
}
+
+ async void OnAddToken(object sender, EventArgs e)
+ {
+ AddTokenPopup popup = serviceProvider.GetServiceOrCreateInstance();
+ var result = await this.ShowPopupAsync(popup);
+ if (result.Result)
+ LoadingService.BeginLoad();
+ }
}
diff --git a/OneGateApp/Properties/Strings.Designer.cs b/OneGateApp/Properties/Strings.Designer.cs
index 9b455dd..383170d 100644
--- a/OneGateApp/Properties/Strings.Designer.cs
+++ b/OneGateApp/Properties/Strings.Designer.cs
@@ -2625,5 +2625,59 @@ internal static string WifPrivateKeyWarning {
return ResourceManager.GetString("WifPrivateKeyWarning", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Add token.
+ ///
+ internal static string AddToken {
+ get {
+ return ResourceManager.GetString("AddToken", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Enter a NEP-17 contract hash to track a custom token..
+ ///
+ internal static string AddTokenText {
+ get {
+ return ResourceManager.GetString("AddTokenText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to 0x… contract hash.
+ ///
+ internal static string ContractHashPlaceholder {
+ get {
+ return ResourceManager.GetString("ContractHashPlaceholder", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Enter a valid contract hash..
+ ///
+ internal static string InvalidContractHash {
+ get {
+ return ResourceManager.GetString("InvalidContractHash", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No NEP-17 token found at this contract..
+ ///
+ internal static string TokenNotFound {
+ get {
+ return ResourceManager.GetString("TokenNotFound", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Add.
+ ///
+ internal static string Add {
+ get {
+ return ResourceManager.GetString("Add", resourceCulture);
+ }
+ }
}
}
diff --git a/OneGateApp/Properties/Strings.resx b/OneGateApp/Properties/Strings.resx
index e0fbfd0..a44f01f 100644
--- a/OneGateApp/Properties/Strings.resx
+++ b/OneGateApp/Properties/Strings.resx
@@ -989,4 +989,22 @@ It is recommended to store the NEP-2 key and password separately and back them u
Unavailable
+
+ Add token
+
+
+ Enter a NEP-17 contract hash to track a custom token.
+
+
+ 0x… contract hash
+
+
+ Enter a valid contract hash.
+
+
+ No NEP-17 token found at this contract.
+
+
+ Add
+
diff --git a/OneGateApp/Properties/Strings.zh-Hans.resx b/OneGateApp/Properties/Strings.zh-Hans.resx
index 068ed29..453a85b 100644
--- a/OneGateApp/Properties/Strings.zh-Hans.resx
+++ b/OneGateApp/Properties/Strings.zh-Hans.resx
@@ -989,4 +989,22 @@
不可用
+
+ 添加代币
+
+
+ 输入 NEP-17 合约哈希以添加自定义代币。
+
+
+ 0x… 合约哈希
+
+
+ 请输入有效的合约哈希。
+
+
+ 该合约上未找到 NEP-17 代币。
+
+
+ 添加
+
diff --git a/OneGateApp/Properties/Strings.zh-Hant.resx b/OneGateApp/Properties/Strings.zh-Hant.resx
index e985d08..48a4c81 100644
--- a/OneGateApp/Properties/Strings.zh-Hant.resx
+++ b/OneGateApp/Properties/Strings.zh-Hant.resx
@@ -989,4 +989,22 @@
不可用
+
+ 新增代幣
+
+
+ 輸入 NEP-17 合約雜湊以新增自訂代幣。
+
+
+ 0x… 合約雜湊
+
+
+ 請輸入有效的合約雜湊。
+
+
+ 該合約上未找到 NEP-17 代幣。
+
+
+ 新增
+
\ No newline at end of file
diff --git a/OneGateApp/Services/TokenManager.cs b/OneGateApp/Services/TokenManager.cs
index 8a5b087..3bc2ec1 100644
--- a/OneGateApp/Services/TokenManager.cs
+++ b/OneGateApp/Services/TokenManager.cs
@@ -15,6 +15,16 @@ public class TokenManager(ApplicationDbContext dbContext, IWalletProvider wallet
public async Task> LoadTokensAsync(bool includeHiddens = false)
{
List tokens = EmbeddedResource.LoadJson>("tokens.json");
+ UInt160[]? custom = await dbContext.Settings.GetAsync("tokens/custom");
+ if (custom is not null)
+ {
+ foreach (UInt160 hash in custom)
+ {
+ if (tokens.Any(p => p.Hash == hash)) continue;
+ try { tokens.Add(await rpcClient.GetTokenInfo(hash)); }
+ catch (RpcException) { /* custom token no longer resolvable on chain; skip */ }
+ }
+ }
if (!includeHiddens)
{
UInt160[]? hiddens = await dbContext.Settings.GetAsync("tokens/hidden");
@@ -23,6 +33,16 @@ public async Task> LoadTokensAsync(bool includeHiddens
return tokens;
}
+ // Resolve a NEP-17 by contract hash (throws if not a valid token) and persist it as a user-added token.
+ public async Task AddCustomTokenAsync(UInt160 hash)
+ {
+ TokenInfo token = await rpcClient.GetTokenInfo(hash);
+ UInt160[] custom = await dbContext.Settings.GetAsync("tokens/custom") ?? [];
+ if (!custom.Contains(hash))
+ await dbContext.Settings.PutAsync("tokens/custom", custom.Append(hash).ToArray());
+ return token;
+ }
+
public async Task LoadAssetAsync(UInt160 assetId)
{
Wallet wallet = walletProvider.GetWallet()!;