Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 81 additions & 10 deletions OneGateApp/Controls/Popups/SendTransactionPopup.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,58 @@
x:Class="NeoOrder.OneGate.Controls.Popups.SendTransactionPopup"
x:TypeArguments="x:Boolean">
<og:MyPopup.Resources>
<toolkit:IsNotNullConverter x:Key="IsNotNullConverter" />
<toolkit:InvertedBoolConverter x:Key="InvertedBoolConverter" />
</og:MyPopup.Resources>
<VerticalStackLayout Spacing="15" MaximumWidthRequest="450" BindingContext="{Binding Source={RelativeSource AncestorType={x:Type og:SendTransactionPopup}}}">
<Grid ColumnDefinitions="*,auto">
<Label Text="{Binding Title}" FontAttributes="Bold" />
<Button Grid.Column="1" StyleClass="Icon" Text="&#xe6b5;" FontSize="24" Margin="-10" Padding="10" Clicked="OnCancel" />
</Grid>
<ScrollView MaximumHeightRequest="400">
<ScrollView MaximumHeightRequest="520">
<VerticalStackLayout Spacing="15">
<Label Text="{Binding Message}" />
<og:IntentsView Intents="{Binding Intents}" />
<Border StrokeShape="RoundRectangle 10" BackgroundColor="{toolkit:AppThemeResource Panel}">
<VerticalStackLayout Padding="10,0">
<Grid ColumnDefinitions="auto,*" ColumnSpacing="15" Padding="0,10" IsVisible="{Binding InvocationResult, Converter={StaticResource IsNotNullConverter}}">
<Label StyleClass="Secondary" Text="{x:Static og:Strings.ExecutionResult}" VerticalOptions="Center" />
<VerticalStackLayout Grid.Column="1" HorizontalOptions="End">
<Label Text="{Binding InvocationResult.State}" HorizontalOptions="End" />
<Label StyleClass="Secondary" Text="{Binding InvocationResult.Exception, TargetNullValue={x:Static og:Strings.Success}}" HorizontalOptions="End" />
<VerticalStackLayout Padding="14,0">
<Grid Padding="0,12">
<VerticalStackLayout Spacing="3">
<Label Text="{x:Static og:Strings.AssetChanges}" FontAttributes="Bold" />
<Label StyleClass="Secondary" Text="{x:Static og:Strings.AssetChangesText}" FontSize="12" />
</VerticalStackLayout>
</Grid>
<BoxView HeightRequest="1" IsVisible="{Binding InvocationResult, Converter={StaticResource IsNotNullConverter}}" />
<BoxView HeightRequest="1" IsVisible="{Binding HasAssetChanges}" />
<VerticalStackLayout BindableLayout.ItemsSource="{Binding AssetChanges}" IsVisible="{Binding HasAssetChanges}">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="og:TransactionPreviewAssetChange">
<Grid ColumnDefinitions="*,auto" RowDefinitions="auto,auto,auto" ColumnSpacing="12" RowSpacing="6" Padding="0,10">
<Label Text="{Binding Title}" FontAttributes="Bold" MaxLines="1" LineBreakMode="TailTruncation" />
<Label Grid.Column="1" Text="{Binding AmountText}" FontAttributes="Bold" HorizontalOptions="End">
<Label.Triggers>
<DataTrigger TargetType="Label" Binding="{Binding IsOutgoing}" Value="True">
<Setter Property="TextColor" Value="{toolkit:AppThemeResource Danger}" />
</DataTrigger>
<DataTrigger TargetType="Label" Binding="{Binding IsIncoming}" Value="True">
<Setter Property="TextColor" Value="{toolkit:AppThemeResource Success}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Grid.Row="1" Grid.ColumnSpan="2" StyleClass="Secondary" Text="{Binding DetailText}" FontSize="12" LineBreakMode="TailTruncation" />
<Grid Grid.Row="2" Grid.ColumnSpan="2" ColumnDefinitions="auto,*" RowDefinitions="auto,auto,auto" RowSpacing="3" ColumnSpacing="10">
<Label Text="{x:Static og:Strings.AssetHash}" StyleClass="Secondary" FontSize="11" />
<Label Grid.Column="1" Text="{Binding AssetHashText}" StyleClass="Secondary" FontSize="11" LineBreakMode="MiddleTruncation" HorizontalOptions="End" />
<Label Grid.Row="1" Text="{x:Static og:Strings.PaymentAddress}" StyleClass="Secondary" FontSize="11" />
<Label Grid.Row="1" Grid.Column="1" Text="{Binding PaymentAddressText}" StyleClass="Secondary" FontSize="11" LineBreakMode="MiddleTruncation" HorizontalOptions="End" />
<Label Grid.Row="2" Text="{x:Static og:Strings.ReceivingAddress}" StyleClass="Secondary" FontSize="11" />
<Label Grid.Row="2" Grid.Column="1" Text="{Binding ReceivingAddressText}" StyleClass="Secondary" FontSize="11" LineBreakMode="MiddleTruncation" HorizontalOptions="End" />
</Grid>
</Grid>
</DataTemplate>
</BindableLayout.ItemTemplate>
</VerticalStackLayout>
<Label StyleClass="Secondary" Text="{x:Static og:Strings.NoDirectAssetChanges}" FontSize="12" Padding="0,0,0,12" IsVisible="{Binding HasAssetChanges, Converter={StaticResource InvertedBoolConverter}}" />
</VerticalStackLayout>
</Border>
<Border StrokeShape="RoundRectangle 10" BackgroundColor="{toolkit:AppThemeResource Panel}">
<VerticalStackLayout Padding="10,0">
<Grid ColumnDefinitions="auto,*" ColumnSpacing="15" Padding="0,10">
<Label StyleClass="Secondary" Text="{x:Static og:Strings.Fee}" VerticalOptions="Center" />
<VerticalStackLayout Grid.Column="1" HorizontalOptions="End">
Expand All @@ -36,6 +67,46 @@
</Grid>
</VerticalStackLayout>
</Border>
<Border StrokeShape="RoundRectangle 10" BackgroundColor="{toolkit:AppThemeResource Panel}" IsVisible="{Binding HasRiskWarnings}">
<VerticalStackLayout Padding="14,0">
<Grid Padding="0,12">
<VerticalStackLayout Spacing="3">
<Label Text="{x:Static og:Strings.RiskReview}" FontAttributes="Bold" />
<Label StyleClass="Secondary" Text="{x:Static og:Strings.RiskReviewText}" FontSize="12" />
</VerticalStackLayout>
</Grid>
<BoxView HeightRequest="1" />
<VerticalStackLayout BindableLayout.ItemsSource="{Binding RiskWarnings}">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="og:TransactionPreviewWarning">
<Grid RowDefinitions="auto,auto" Padding="0,10">
<Label Text="{Binding Title}" FontAttributes="Bold" FontSize="13">
<Label.Triggers>
<DataTrigger TargetType="Label" Binding="{Binding IsHighRisk}" Value="True">
<Setter Property="TextColor" Value="{toolkit:AppThemeResource Danger}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Grid.Row="1" StyleClass="Secondary" Text="{Binding Message}" FontSize="12" />
</Grid>
</DataTemplate>
</BindableLayout.ItemTemplate>
</VerticalStackLayout>
</VerticalStackLayout>
</Border>
<Label StyleClass="Secondary" Text="{x:Static og:Strings.RequestDetails}" FontAttributes="Bold" IsVisible="{Binding HasRequestDetails}" />
<og:IntentsView Intents="{Binding Intents}" IsVisible="{Binding HasRequestDetails}" />
<Border StrokeShape="RoundRectangle 10" BackgroundColor="{toolkit:AppThemeResource Panel}" IsVisible="{Binding HasExecutionWarning}">
<VerticalStackLayout Padding="10,0">
<Grid ColumnDefinitions="auto,*" ColumnSpacing="15" Padding="0,10">
<Label StyleClass="Secondary" Text="{x:Static og:Strings.ExecutionResult}" VerticalOptions="Center" />
<VerticalStackLayout Grid.Column="1" HorizontalOptions="End">
<Label Text="{Binding InvocationResult.State}" HorizontalOptions="End" />
<Label StyleClass="Secondary" Text="{Binding InvocationResult.Exception, TargetNullValue={x:Static og:Strings.Success}}" HorizontalOptions="End" />
</VerticalStackLayout>
</Grid>
</VerticalStackLayout>
</Border>
</VerticalStackLayout>
</ScrollView>
<Grid ColumnDefinitions="*,auto,*" Margin="-15,0,-15,-15">
Expand Down
140 changes: 136 additions & 4 deletions OneGateApp/Controls/Popups/SendTransactionPopup.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Neo;
using Neo.Network.P2P.Payloads;
using Neo.SmartContract.Native;
using Neo.VM;
using Neo.Wallets;
using NeoOrder.OneGate.Controls.Views;
using NeoOrder.OneGate.Models;
using NeoOrder.OneGate.Models.Intents;
Expand All @@ -13,23 +15,34 @@ namespace NeoOrder.OneGate.Controls.Popups;
public partial class SendTransactionPopup : MyPopup<bool>
{
readonly WalletAuthorizationService walletAuthorizationService;
readonly ProtocolSettings protocolSettings;
readonly Wallet wallet;

public string Title { get; set { field = value; OnPropertyChanged(); } } = Strings.SendTransaction;
public string Message { get; set { field = value; OnPropertyChanged(); } } = Strings.SendTransactionText;
public required Transaction Transaction { get; set { field = value; OnPropertyChanged(null); } }
public TransactionIntent[]? Intents { get; set { field = value; OnPropertyChanged(); } }
public InvocationResult? InvocationResult { get; set { field = value; OnPropertyChanged(); } }
public required Transaction Transaction { get; set { field = value; OnPropertyChanged(null); RefreshPreview(); } }
public TransactionIntent[]? Intents { get { return field; } set { field = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasIntents)); RefreshPreview(); } }
public InvocationResult? InvocationResult { get { return field; } set { field = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasExecutionWarning)); RefreshPreview(); } }
public TransactionPreviewAssetChange[] AssetChanges { get; private set { field = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasAssetChanges)); } } = [];
public TransactionPreviewWarning[] RiskWarnings { get; private set { field = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasRiskWarnings)); } } = [];

public long Fee => (Transaction?.SystemFee + Transaction?.NetworkFee) ?? 0;
public BigDecimal DecimalFee => new((BigInteger)Fee, NativeContract.GAS.Decimals);
public string DisplayFee => $"{DecimalFee} {NativeContract.GAS.Symbol}";
public BigDecimal DecimalSystemFee => new((BigInteger)(Transaction?.SystemFee ?? 0), NativeContract.GAS.Decimals);
public BigDecimal DecimalNetworkFee => new((BigInteger)(Transaction?.NetworkFee ?? 0), NativeContract.GAS.Decimals);
public string FeeDetails => $"{DecimalSystemFee} (sys) + {DecimalNetworkFee} (net)";
public bool HasAssetChanges => AssetChanges.Length > 0;
public bool HasIntents => Intents?.Length > 0;
public bool HasRequestDetails => HasIntents && !HasAssetChanges;
public bool HasRiskWarnings => RiskWarnings.Length > 0;
public bool HasExecutionWarning => InvocationResult is { State: not VMState.HALT } || !string.IsNullOrWhiteSpace(InvocationResult?.Exception);

public SendTransactionPopup(WalletAuthorizationService walletAuthorizationService)
public SendTransactionPopup(WalletAuthorizationService walletAuthorizationService, ProtocolSettings protocolSettings, IWalletProvider walletProvider)
{
this.walletAuthorizationService = walletAuthorizationService;
this.protocolSettings = protocolSettings;
this.wallet = walletProvider.GetWallet()!;
InitializeComponent();
}

Expand All @@ -47,4 +60,123 @@ async void OnCancel(object sender, EventArgs e)
{
await CloseAsync(false);
}

void RefreshPreview()
{
AssetChanges = BuildAssetChanges();
RiskWarnings = BuildRiskWarnings();
OnPropertyChanged(nameof(Fee));
OnPropertyChanged(nameof(DecimalFee));
OnPropertyChanged(nameof(DisplayFee));
OnPropertyChanged(nameof(DecimalSystemFee));
OnPropertyChanged(nameof(DecimalNetworkFee));
OnPropertyChanged(nameof(FeeDetails));
OnPropertyChanged(nameof(HasRequestDetails));
OnPropertyChanged(nameof(HasExecutionWarning));
}

TransactionPreviewAssetChange[] BuildAssetChanges()
{
if (Intents is null || Intents.Length == 0) return [];

List<TransactionPreviewAssetChange> changes = [];
foreach (TransactionIntent intent in Intents)
{
switch (intent)
{
case TransferIntent transfer:
bool fromWallet = wallet.Contains(transfer.From);
bool toWallet = wallet.Contains(transfer.To);
changes.Add(new TransactionPreviewAssetChange
{
Title = transfer.Asset.Symbol,
AmountText = $"{GetAmountPrefix(fromWallet, toWallet)}{transfer.DisplayAmount}",
DetailText = GetTransferDetail(transfer.From, transfer.To, fromWallet, toWallet),
AssetHashText = transfer.Asset.Hash.ToString(),
PaymentAddressText = FullAddress(transfer.From),
ReceivingAddressText = FullAddress(transfer.To),
IsOutgoing = fromWallet && !toWallet,
IsIncoming = toWallet && !fromWallet
});
break;
case Nep11TransferIntent nftTransfer:
bool nftFromWallet = wallet.Contains(nftTransfer.From);
bool nftToWallet = wallet.Contains(nftTransfer.To);
changes.Add(new TransactionPreviewAssetChange
{
Title = nftTransfer.Asset.Name,
AmountText = $"{GetAmountPrefix(nftFromWallet, nftToWallet)}{Strings.NFT}",
DetailText = GetTransferDetail(nftTransfer.From, nftTransfer.To, nftFromWallet, nftToWallet),
AssetHashText = (nftTransfer.Asset.TokenInfo?.Hash ?? nftTransfer.Asset.CollectionId).ToString(),
PaymentAddressText = FullAddress(nftTransfer.From),
ReceivingAddressText = FullAddress(nftTransfer.To),
IsOutgoing = nftFromWallet && !nftToWallet,
IsIncoming = nftToWallet && !nftFromWallet
});
break;
}
}
return changes.ToArray();
}

TransactionPreviewWarning[] BuildRiskWarnings()
{
List<TransactionPreviewWarning> warnings = [];
if (Intents is null || Intents.Length == 0)
{
warnings.Add(new TransactionPreviewWarning
{
Title = Strings.UnknownAssetChanges,
Message = Strings.UnknownAssetChangesText,
IsHighRisk = true
});
}
else if (Intents.Any(p => p is InvocationIntent))
{
warnings.Add(new TransactionPreviewWarning
{
Title = Strings.ContractRequest,
Message = Strings.ContractRequestRiskText
});
}
if (HasExecutionWarning)
{
warnings.Add(new TransactionPreviewWarning
{
Title = Strings.ExecutionWarning,
Message = Strings.ExecutionWarningText,
IsHighRisk = true
});
}
return warnings.ToArray();
}

string GetTransferDetail(UInt160 from, UInt160 to, bool fromWallet, bool toWallet)
{
if (fromWallet && !toWallet)
return string.Format(Strings.SendingToFormat, ShortAddress(to));
if (toWallet && !fromWallet)
return string.Format(Strings.ReceivingFromFormat, ShortAddress(from));
if (fromWallet && toWallet)
return Strings.InternalWalletTransfer;
return string.Format(Strings.ExternalTransferFormat, ShortAddress(from), ShortAddress(to));
}

string ShortAddress(UInt160 hash)
{
string address = hash.ToAddress(protocolSettings.AddressVersion);
return address.Length <= 12 ? address : $"{address[..6]}...{address[^4..]}";
}

string FullAddress(UInt160 hash)
{
return hash.ToAddress(protocolSettings.AddressVersion);
}

static string GetAmountPrefix(bool fromWallet, bool toWallet)
{
if (fromWallet && !toWallet) return "-";
if (toWallet && !fromWallet) return "+";
return "";
}
}
13 changes: 13 additions & 0 deletions OneGateApp/Models/TransactionPreviewAssetChange.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace NeoOrder.OneGate.Models;

public class TransactionPreviewAssetChange
{
public required string Title { get; init; }
public required string AmountText { get; init; }
public required string DetailText { get; init; }
public required string AssetHashText { get; init; }
public required string PaymentAddressText { get; init; }
public required string ReceivingAddressText { get; init; }
public bool IsOutgoing { get; init; }
public bool IsIncoming { get; init; }
}
8 changes: 8 additions & 0 deletions OneGateApp/Models/TransactionPreviewWarning.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace NeoOrder.OneGate.Models;

public class TransactionPreviewWarning
{
public required string Title { get; init; }
public required string Message { get; init; }
public bool IsHighRisk { get; init; }
}
Loading