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
4 changes: 2 additions & 2 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Build

on:
push:
branches: [ "master" ]
branches: [ "master", "develop" ]
pull_request:
branches: [ "master" ]
branches: [ "master", "develop" ]

jobs:
build:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ name: Publish Plugin on Tag
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-[a-zA-Z0-9]+.[0-9]+"

jobs:
build-plugin:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
.idea
Plugins/packed
.vs/
*.DotSettings
*.DotSettings.user
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand Down
1 change: 1 addition & 0 deletions BTCPayServer.Plugins.USDt.sln
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution items", "Solution
README.md = README.md
.github\workflows\publish.yml = .github\workflows\publish.yml
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
.gitignore = .gitignore
EndProjectSection
EndProject
Global
Expand Down
21 changes: 8 additions & 13 deletions BTCPayServer.Plugins.USDt/BTCPayServer.Plugins.USDt.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12</LangVersion>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>13</LangVersion>
<Configurations>Release;Altcoins-Debug</Configurations>
<Platforms>AnyCPU</Platforms>
<Nullable>enable</Nullable>
Expand Down Expand Up @@ -42,12 +42,12 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Nethereum.Contracts" Version="4.20.0"/>
<PackageReference Include="Nethereum.Hex" Version="4.20.0"/>
<PackageReference Include="Nethereum.RPC" Version="4.20.0"/>
<PackageReference Include="Nethereum.StandardTokenEIP20" Version="4.20.0"/>
<PackageReference Include="Nethereum.Web3" Version="4.20.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
<PackageReference Include="Nethereum.Contracts" Version="6.1.0"/>
<PackageReference Include="Nethereum.Hex" Version="6.1.0"/>
<PackageReference Include="Nethereum.RPC" Version="6.1.0"/>
<PackageReference Include="Nethereum.StandardTokenEIP20" Version="6.1.0"/>
<PackageReference Include="Nethereum.Web3" Version="6.1.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />

</ItemGroup>

Expand All @@ -62,11 +62,6 @@
</ItemGroup>


<ItemGroup>
<Folder Include="Views\Shared\"/>
</ItemGroup>


<ItemGroup>
<AdditionalFiles Include="Views\Shared\TronUSDtLike\StoreNavTronUSDtExtension.cshtml" />
<AdditionalFiles Include="Views\Shared\TronUSDtLike\StoreWalletsNavTronUSDtExtension.cshtml" />
Expand Down
4 changes: 4 additions & 0 deletions BTCPayServer.Plugins.USDt/Configuration/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ namespace BTCPayServer.Plugins.USDt.Configuration;
public static class Constants
{
public const string TronChainName = "TRON";
public const string EthereumChainName = "ETHEREUM";
public const string SepoliaChainName = "SEPOLIA";
public const string PolygonChainName = "POLYGON";
public const string AmoyChainName = "AMOY";
public const string USDtCurrency = "USDt";
public const string USDtCurrencyDisplayName = "USD\u20ae";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;

namespace BTCPayServer.Plugins.USDt.Configuration.Ethereum;

public record EthUSDtLikeConfigurationItem(string Chain) : USDtPluginConfigurationItem
{
public required Uri JsonRpcUri { get; init; }
public required string SmartContractAddress { get; init; }
public override string Chain { get; } = Chain;
public override required string Currency { get; init; }
public required string DisplayName { get; init; }
public required int Divisibility { get; init; }
public required string CryptoImagePath { get; init; }
public required string BlockExplorerLink { get; init; }
public required string[] DefaultRateRules { get; init; }
public required string CurrencyDisplayName { get; init; }

/// <summary>
/// Average block time in seconds. Used to compute the sync lag threshold.
/// Ethereum mainnet ≈ 12s, Polygon PoS ≈ 2s.
/// </summary>
public double BlockTimeSeconds { get; init; } = 12.0;

/// <summary>
/// EIP-155 chain ID. Used in EIP-681 payment links so wallets select the correct network.
/// Ethereum mainnet = 1, Polygon = 137.
/// </summary>
public int ChainId { get; init; } = 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace BTCPayServer.Plugins.USDt.Configuration.Ethereum;

public class EthUSDtLikeServerSettings
{
public Uri? JsonRpcUri { get; init; }
public string? SmartContractAddress { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System.Collections.Generic;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.USDt.Configuration.Tron;
using BTCPayServer.Plugins.USDt.Configuration.Ethereum;

namespace BTCPayServer.Plugins.USDt.Configuration;

public class USDtPluginConfiguration
{
public Dictionary<PaymentMethodId, TronUSDtLikeConfigurationItem> TronUSDtLikeConfigurationItems { get; init; } = [];
public Dictionary<PaymentMethodId, TronUSDtLikeConfigurationItem> TronUSDtLikeConfigurationItems { get; init; } = new();
public Dictionary<PaymentMethodId, EthUSDtLikeConfigurationItem> EVMUSDtLikeConfigurationItems { get; init; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.USDt.Configuration;
using BTCPayServer.Plugins.USDt.Controllers.Models;
using BTCPayServer.Plugins.USDt.Services;
using BTCPayServer.Plugins.USDt.Services.Payments;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace BTCPayServer.Plugins.USDt.Controllers;

[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
public class GreenfieldEthUSDtLikeStoreController(
EthUSDtRPCProvider ethUSDtRpcProvider,
PaymentMethodHandlerDictionary handlers,
InvoiceRepository invoiceRepository,
StoreRepository storeRepository,
USDtPluginConfiguration pluginConfiguration) : ControllerBase
{
private StoreData StoreData => HttpContext.GetStoreDataOrNull()!;

[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/evmUSDtlike/{paymentMethodId}")]
public async Task<IActionResult> GetStoreInformation(PaymentMethodId paymentMethodId)
{
if (!pluginConfiguration.EVMUSDtLikeConfigurationItems.ContainsKey(paymentMethodId))
return NotFound();

var excludeFilters = StoreData.GetStoreBlob().GetExcludedPaymentMethods();
var matchedPaymentMethodConfig =
StoreData.GetPaymentMethodConfig<EthUSDtPaymentMethodConfig>(paymentMethodId, handlers);

if (matchedPaymentMethodConfig == null)
return NotFound();

var balances =
await ethUSDtRpcProvider.GetBalances(paymentMethodId, [.. matchedPaymentMethodConfig.Addresses]);
var reservedAddresses =
await EthUSDtPaymentMethodConfig.GetReservedAddresses(paymentMethodId, invoiceRepository);

return Ok(new EthUSDtPaymentMethodInformation
{
StoreId = StoreData.Id,
PaymentMethodId = paymentMethodId.ToString(),
Enabled = !excludeFilters.Match(paymentMethodId),
Addresses = matchedPaymentMethodConfig.Addresses.Select(s =>
new EthUSDtPaymentMethodInformation.EthUSDtPaymentMethodAddressInformation
{
Available = !reservedAddresses.Contains(s),
Balance = balances.Single(x => x.Item1 == s).Item2,
Value = s
}).ToArray()
});
}

[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/evmUSDtlike/{paymentMethodId}/addresses")]
public async Task<IActionResult> AddAddress(PaymentMethodId paymentMethodId, [FromBody] EthUSDtAddAddressRequest request)
{
if (!pluginConfiguration.EVMUSDtLikeConfigurationItems.ContainsKey(paymentMethodId))
return NotFound();

var invalid = request.Addresses.Where(a => !EthAddressHelper.IsValid(a)).ToArray();
if (invalid.Length > 0)
return BadRequest(new { message = "Invalid EVM address(es).", addresses = invalid });

var store = StoreData;
var currentConfig = store.GetPaymentMethodConfig<EthUSDtPaymentMethodConfig>(paymentMethodId, handlers)
?? new EthUSDtPaymentMethodConfig();

var normalized = request.Addresses.Select(a => a.ToLowerInvariant()).ToArray();
var duplicates = normalized.Where(a => currentConfig.Addresses.Contains(a)).ToArray();
if (duplicates.Length > 0)
return BadRequest(new { message = "Address(es) already exist.", addresses = duplicates });

currentConfig.Addresses = [.. currentConfig.Addresses, .. normalized];
store.SetPaymentMethodConfig(handlers[paymentMethodId], currentConfig);
await storeRepository.UpdateStore(store);
return Ok();
}

[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/evmUSDtlike/{paymentMethodId}/addresses/{address}")]
public async Task<IActionResult> DeleteAddress(PaymentMethodId paymentMethodId, string address)
{
if (!pluginConfiguration.EVMUSDtLikeConfigurationItems.ContainsKey(paymentMethodId))
return NotFound();

var store = StoreData;
var currentConfig = store.GetPaymentMethodConfig<EthUSDtPaymentMethodConfig>(paymentMethodId, handlers);
var normalized = address.ToLowerInvariant();

if (currentConfig == null || !currentConfig.Addresses.Contains(normalized))
return NotFound();

currentConfig.Addresses = currentConfig.Addresses.Where(a => a != normalized).ToArray();
store.SetPaymentMethodConfig(handlers[paymentMethodId], currentConfig);
await storeRepository.UpdateStore(store);
return Ok();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Text;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Data;
Expand All @@ -10,10 +9,10 @@
using BTCPayServer.Plugins.USDt.Services;
using BTCPayServer.Plugins.USDt.Services.Payments;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using StoreData = BTCPayServer.Data.StoreData;

namespace BTCPayServer.Plugins.USDt.Controllers;

Expand All @@ -24,16 +23,16 @@ public class GreenfieldTronUSDtLikeStoreController(
TronUSDtRPCProvider tronUSDtRpcProvider,
PaymentMethodHandlerDictionary handlers,
InvoiceRepository invoiceRepository,
StoreRepository storeRepository,
USDtPluginConfiguration pluginConfiguration) : ControllerBase
{

private StoreData StoreData => HttpContext.GetStoreData();
private StoreData StoreData => HttpContext.GetStoreDataOrNull()!;

[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/tronUSDtlike/{paymentMethodId}")]
public async Task<IActionResult> GetUSDtLikeStoreInformation(PaymentMethodId paymentMethodId)
{
if (pluginConfiguration.TronUSDtLikeConfigurationItems.ContainsKey(paymentMethodId) == false)
if (!pluginConfiguration.TronUSDtLikeConfigurationItems.ContainsKey(paymentMethodId))
return NotFound();

var excludeFilters = StoreData.GetStoreBlob().GetExcludedPaymentMethods();
Expand All @@ -48,22 +47,62 @@ public async Task<IActionResult> GetUSDtLikeStoreInformation(PaymentMethodId pay
var reservedAddresses =
await TronUSDtPaymentMethodConfig.GetReservedAddresses(paymentMethodId, invoiceRepository);

var data = new TronUSDtPaymentMethodInformation
return Ok(new TronUSDtPaymentMethodInformation
{
StoreId = StoreData.Id,
PaymentMethodId = paymentMethodId.ToString(),
Enabled = !excludeFilters.Match(paymentMethodId),
Addresses = matchedPaymentMethodConfig.Addresses.Select(s =>
new TronUSDtPaymentMethodInformation.TronUSDtPaymentMethodAddressInformation()
new TronUSDtPaymentMethodInformation.TronUSDtPaymentMethodAddressInformation
{
Available = reservedAddresses.Contains(s) == false,
Balance = balances.Single(x => x.Item1 == s).Item2 == null
? null
: balances.Single(x => x.Item1 == s).Item2!.Value,
Available = !reservedAddresses.Contains(s),
Balance = balances.Single(x => x.Item1 == s).Item2,
Value = s
}).ToArray()
};
});
}

[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/tronUSDtlike/{paymentMethodId}/addresses")]
public async Task<IActionResult> AddAddress(PaymentMethodId paymentMethodId, [FromBody] TronUSDtAddAddressRequest request)
{
if (!pluginConfiguration.TronUSDtLikeConfigurationItems.ContainsKey(paymentMethodId))
return NotFound();

var invalid = request.Addresses.Where(a => !TronUSDtAddressHelper.IsValid(a)).ToArray();
if (invalid.Length > 0)
return BadRequest(new { message = "Invalid Tron address(es).", addresses = invalid });

var store = StoreData;
var currentConfig = store.GetPaymentMethodConfig<TronUSDtPaymentMethodConfig>(paymentMethodId, handlers)
?? new TronUSDtPaymentMethodConfig();

var duplicates = request.Addresses.Where(a => currentConfig.Addresses.Contains(a)).ToArray();
if (duplicates.Length > 0)
return BadRequest(new { message = "Address(es) already exist.", addresses = duplicates });

currentConfig.Addresses = [.. currentConfig.Addresses, .. request.Addresses];
store.SetPaymentMethodConfig(handlers[paymentMethodId], currentConfig);
await storeRepository.UpdateStore(store);
return Ok();
}

[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/tronUSDtlike/{paymentMethodId}/addresses/{address}")]
public async Task<IActionResult> DeleteAddress(PaymentMethodId paymentMethodId, string address)
{
if (!pluginConfiguration.TronUSDtLikeConfigurationItems.ContainsKey(paymentMethodId))
return NotFound();

var store = StoreData;
var currentConfig = store.GetPaymentMethodConfig<TronUSDtPaymentMethodConfig>(paymentMethodId, handlers);

if (currentConfig == null || !currentConfig.Addresses.Contains(address))
return NotFound();

return Ok(data);
currentConfig.Addresses = currentConfig.Addresses.Where(a => a != address).ToArray();
store.SetPaymentMethodConfig(handlers[paymentMethodId], currentConfig);
await storeRepository.UpdateStore(store);
return Ok();
}
}
}
Loading