Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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,104 @@
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 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,
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 IActionResult AddAddress(PaymentMethodId paymentMethodId, [FromBody] EthUSDtAddAddressRequest request)
{
if (!pluginConfiguration.EVMUSDtLikeConfigurationItems.ContainsKey(paymentMethodId))
return NotFound();

if (!EthAddressHelper.IsValid(request.Address))
return BadRequest(new { message = "Invalid EVM address." });

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

if (currentConfig.Addresses.Contains(address))
return BadRequest(new { message = "Address already exists." });

currentConfig.Addresses = [.. currentConfig.Addresses, address];
store.SetPaymentMethodConfig(handlers[paymentMethodId], currentConfig);
return Ok();
Comment thread
learntheropes marked this conversation as resolved.
Outdated
}

[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/evmUSDtlike/{paymentMethodId}/addresses/{address}")]
public 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);
return Ok();
Comment thread
learntheropes marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class GreenfieldTronUSDtLikeStoreController(
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}")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace BTCPayServer.Plugins.USDt.Controllers.Models;

public class EthUSDtPaymentMethodInformation
{
public required string StoreId { get; init; }
public required string PaymentMethodId { get; init; }
public required bool Enabled { get; init; }

public EthUSDtPaymentMethodAddressInformation[] Addresses { get; init; } = [];

public class EthUSDtPaymentMethodAddressInformation
{
public required string Value { get; init; }
public bool Available { get; init; }
public required decimal? Balance { get; init; }
}
}

public class EthUSDtAddAddressRequest
{
public required string Address { get; init; }
}
94 changes: 94 additions & 0 deletions BTCPayServer.Plugins.USDt/Controllers/SwaggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,100 @@ public Task<JObject> Fetch()
var swaggerJson = @"
{
""paths"": {
""/api/v1/stores/{storeId}/evmUSDtlike/{paymentMethodId}"": {
""get"": {
""tags"": [""Store (Payment Methods)""],
""summary"": ""Get EVM USDt configuration"",
""operationId"": ""GetEvmUSDtLikeStoreInformation"",
""security"": [{""API_Key"": [""btcpay.store.canviewstoresettings""], ""Basic"": []}],
""parameters"": [
{""$ref"": ""#/components/parameters/StoreId""},
{""$ref"": ""#/components/parameters/PaymentMethodId""}
],
""responses"": {
""200"": {
""description"": ""Success"",
""content"": {
""application/json"": {
""schema"": {
""type"": ""object"",
""properties"": {
""storeId"": {""type"": ""string""},
""paymentMethodId"": {""type"": ""string""},
""enabled"": {""type"": ""boolean""},
""addresses"": {
""type"": ""array"",
""items"": {
""type"": ""object"",
""properties"": {
""value"": {""type"": ""string""},
""available"": {""type"": ""boolean""},
""balance"": {""type"": ""number"", ""nullable"": true}
}
}
}
}
}
}
}
},
""404"": {""description"": ""Not Found""}
}
}
},
""/api/v1/stores/{storeId}/evmUSDtlike/{paymentMethodId}/addresses"": {
""post"": {
""tags"": [""Store (Payment Methods)""],
""summary"": ""Add an EVM USDt address"",
""operationId"": ""AddEvmUSDtAddress"",
""security"": [{""API_Key"": [""btcpay.store.canmodifystoresettings""], ""Basic"": []}],
""parameters"": [
{""$ref"": ""#/components/parameters/StoreId""},
{""$ref"": ""#/components/parameters/PaymentMethodId""}
],
""requestBody"": {
""required"": true,
""content"": {
""application/json"": {
""schema"": {
""type"": ""object"",
""required"": [""address""],
""properties"": {
""address"": {""type"": ""string"", ""description"": ""EVM address (0x...)""}
}
}
}
}
},
""responses"": {
""200"": {""description"": ""Address added""},
""400"": {""description"": ""Invalid address or already exists""},
""404"": {""description"": ""Payment method not found""}
}
}
},
""/api/v1/stores/{storeId}/evmUSDtlike/{paymentMethodId}/addresses/{address}"": {
""delete"": {
""tags"": [""Store (Payment Methods)""],
""summary"": ""Remove an EVM USDt address"",
""operationId"": ""DeleteEvmUSDtAddress"",
""security"": [{""API_Key"": [""btcpay.store.canmodifystoresettings""], ""Basic"": []}],
""parameters"": [
{""$ref"": ""#/components/parameters/StoreId""},
{""$ref"": ""#/components/parameters/PaymentMethodId""},
{
""name"": ""address"",
""in"": ""path"",
""required"": true,
""schema"": {""type"": ""string""}
}
],
""responses"": {
""200"": {""description"": ""Address removed""},
""404"": {""description"": ""Address or payment method not found""}
}
}
},
""/api/v1/stores/{storeId}/tronUSDtlike/{paymentMethodId}"": {
""get"": {
""tags"": [""Store (Payment Methods)""],
Expand Down
Loading
Loading