Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create blink lightning invoices (BTC, or stablesats) using BTCPay Server #151

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
29 changes: 29 additions & 0 deletions BTCPayServer.Lightning.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "misc", "misc", "{216059DB-7
src\Build\Common.csproj = src\Build\Common.csproj
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Lightning.Blink", "src\BTCPayServer.Lightning.Blink\BTCPayServer.Lightning.Blink.csproj", "{DD000D73-3AD5-465E-9AA9-64802CC88BF3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlinkTest", "BlinkTest\BlinkTest.csproj", "{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -149,6 +153,30 @@ Global
{B024DBD2-FCF4-4C48-9EBE-09AA7AAF36FB}.Release|x64.Build.0 = Release|Any CPU
{B024DBD2-FCF4-4C48-9EBE-09AA7AAF36FB}.Release|x86.ActiveCfg = Release|Any CPU
{B024DBD2-FCF4-4C48-9EBE-09AA7AAF36FB}.Release|x86.Build.0 = Release|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Debug|x64.ActiveCfg = Debug|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Debug|x64.Build.0 = Debug|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Debug|x86.ActiveCfg = Debug|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Debug|x86.Build.0 = Debug|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Release|Any CPU.Build.0 = Release|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Release|x64.ActiveCfg = Release|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Release|x64.Build.0 = Release|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Release|x86.ActiveCfg = Release|Any CPU
{DD000D73-3AD5-465E-9AA9-64802CC88BF3}.Release|x86.Build.0 = Release|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Debug|x64.ActiveCfg = Debug|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Debug|x64.Build.0 = Debug|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Debug|x86.ActiveCfg = Debug|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Debug|x86.Build.0 = Debug|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Release|Any CPU.Build.0 = Release|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Release|x64.ActiveCfg = Release|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Release|x64.Build.0 = Release|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Release|x86.ActiveCfg = Release|Any CPU
{4D375E18-F7FF-4A6D-9994-EE1D0C3C05C3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -162,6 +190,7 @@ Global
{542D3F73-7067-4873-89EF-FA0345E32C04} = {5BA1A1B2-2713-4CF4-9B63-087531598797}
{4057015B-9D8A-411A-B7C2-3342D9F53BD0} = {5BA1A1B2-2713-4CF4-9B63-087531598797}
{B024DBD2-FCF4-4C48-9EBE-09AA7AAF36FB} = {5BA1A1B2-2713-4CF4-9B63-087531598797}
{DD000D73-3AD5-465E-9AA9-64802CC88BF3} = {5BA1A1B2-2713-4CF4-9B63-087531598797}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5E91C820-C650-4ACA-B064-05ECE0A74CE8}
Expand Down
13 changes: 13 additions & 0 deletions BlinkTest/BlinkTest.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\src\BTCPayServer.Lightning.Blink\BTCPayServer.Lightning.Blink.csproj" />
</ItemGroup>
</Project>
38 changes: 38 additions & 0 deletions BlinkTest/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Text.Json;

using BTCPayServer.Lightning.Blink;
using BTCPayServer.Lightning.Blink.Utilities;

Uri apiUri = new Uri("https://api.blink.sv/graphql");
// put your blink API key here
string apiKey = "";

// To include your stablesats in your total balance as converted to sats
// var blinkApiClient = new BlinkApiClient(apiUri, apiKey, true);
var blinkApiClient = new BlinkApiClient(apiUri, apiKey, false);

var defaultWallet = await blinkApiClient.GetDefaultWallet(CancellationToken.None);
Console.WriteLine(defaultWallet.Id);

var totalBalance = await blinkApiClient.GetBalance(CancellationToken.None);
Console.WriteLine(totalBalance.OffchainBalance.Local);

var createIncoiceResponse = await blinkApiClient.CreateLnInvoice(
CancellationToken.None,
100L,
TimeSpan.FromMinutes(10),
"a memo to remember");

string jsonResponse = JsonSerializer.Serialize(createIncoiceResponse, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonResponse);

var btcRate = await blinkApiClient.getBtcRate(CancellationToken.None);

var convertedToSats = RateConversions.convertCentsToSats(createIncoiceResponse.Amount, btcRate);
var deviation = (createIncoiceResponse.AmountReceived - convertedToSats) / createIncoiceResponse.AmountReceived * 100;

Console.WriteLine($"Invoice created with {createIncoiceResponse.Amount.MilliSatoshi} {defaultWallet.WalletCurrency}");
Console.WriteLine($"Conversion rate is {btcRate.Base} / 10^{btcRate.Offset}");
Console.WriteLine($"My own conversion: {convertedToSats} sats");
Console.WriteLine($"Received sats: {createIncoiceResponse.AmountReceived.MilliSatoshi}");
Console.WriteLine($"Deviation: %{deviation}");
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GraphQL.Client.Serializer.Newtonsoft" Version="6.0.2" />
<PackageReference Include="GraphQL.Client" Version="6.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Lightning.Common\BTCPayServer.Lightning.Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Models\" />
<None Remove="Models\Responses\" />
<None Remove="Utilities\" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\" />
<Folder Include="Models\Responses\" />
<Folder Include="Utilities\" />
</ItemGroup>
</Project>
234 changes: 234 additions & 0 deletions src/BTCPayServer.Lightning.Blink/BlinkApiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
using GraphQL;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;

using BTCPayServer.Lightning.Blink.Models.Responses;
using BTCPayServer.Lightning.Blink.Models;
using BTCPayServer.Lightning.Blink.Utilities;

namespace BTCPayServer.Lightning.Blink;

public class BlinkApiClient
{
private Uri _baseUri;
private string _apiKey;
private bool _includeStablesats;

public BlinkApiClient(Uri baseUri, string apiKey, bool includeStablesats = false)
{
_baseUri = baseUri;
_apiKey = apiKey;
_includeStablesats = includeStablesats;
}

public async Task<BlinkWallet> GetDefaultWallet(CancellationToken cancellationToken)
{
string query = @"
query GetDefaultWallet {
me {
defaultAccount {
defaultWalletId
wallets {
balance
id
walletCurrency
}
}
}
}";

var defaultWalletRequest = new GraphQLRequest
{
Query = query,
OperationName = "GetDefaultWallet"
};

var response = await getGraphQLHttpClient().SendQueryAsync<GetDefaultWalletIdResponse>(defaultWalletRequest, cancellationToken);
string defaultWalletId = response.Data.Me.DefaultAccount.DefaultWalletId;
var defaultWallet = response.Data.Me.DefaultAccount.Wallets.Find((w) => w.Id == defaultWalletId);

if (defaultWallet == null)
{
throw new Exception(string.Format("Could not find wallet information for wallet id {0}", defaultWalletId));
}

return new BlinkWallet
{
Id = defaultWallet.Id,
Balance = defaultWallet.Balance,
WalletCurrency = defaultWallet.WalletCurrency
};
}

public async Task<PriceInfo> getBtcRate(CancellationToken cancellationToken)
{
string query = @"
query RealtimePrice($currency: DisplayCurrency) {
realtimePrice(currency: $currency) {
btcSatPrice {
base,
offset
}
}
}";

var getBtcRateRequest = new GraphQLRequest
{
Query = query,
Variables = new
{
input = new
{
currency = "USD"
}
}
};

var response = await getGraphQLHttpClient().SendQueryAsync<GetBtcRateResponse>(getBtcRateRequest, cancellationToken);

return response.Data.RealtimePrice.BtcSatPrice;
}

public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellationToken)
{
string realtimePriceQuery = _includeStablesats
? @"realtimePrice {
btcSatPrice {
base
offset
}
denominatorCurrency
}"
: "";

string query = $@"
query GetWalletBalances {{
me {{
defaultAccount {{
defaultWalletId,
wallets {{
balance
id
walletCurrency
}}
}}
}}
{realtimePriceQuery}
}}";

var balancesRequest = new GraphQLRequest
{
Query = query,
OperationName = "GetWalletBalances"
};

var response = await getGraphQLHttpClient().SendQueryAsync<GetWalletBalancesResponse>(balancesRequest, cancellationToken);
var responseData = response.Data;

long totalBalanceSats = responseData.Me.DefaultAccount.Wallets.Sum(wallet =>
{
if (wallet.WalletCurrency == "USD")
{
var realtimePrice = responseData.RealtimePrice;

if (_includeStablesats
&& realtimePrice != null
&& realtimePrice.BtcSatPrice != null
&& realtimePrice.DenominatorCurrency == "USD")
{
return RateConversions.convertCentsToSats(wallet.Balance, realtimePrice.BtcSatPrice);
}

return 0;
} else
{
return wallet.Balance;
}
});

var offchain = new OffchainBalance
{
Local = new LightMoney(totalBalanceSats, LightMoneyUnit.MilliSatoshi)
};

return new LightningNodeBalance(null, offchain);
}

public async Task<LightningInvoice> CreateLnInvoice(CancellationToken cancellationToken, long amount, TimeSpan expiresIn, string? memo = null)
{
var wallet = await this.GetDefaultWallet(cancellationToken);

string apiCall = wallet.WalletCurrency == "USD" ? "lnUsdInvoiceCreate" : "LnInvoiceCreate";

string query = $@"
mutation {apiCall}($input: {apiCall.Capitalize()}Input!) {{
{apiCall}(input: $input) {{
invoice {{
paymentRequest
paymentHash
paymentSecret
satoshis
}}
errors {{
message
}}
}}
}}";

var createLnInvoiceRequest = new GraphQLRequest
{
Query = query,
OperationName = apiCall,
Variables = new
{
input = new
{
amount,
walletId = wallet.Id,
expiresIn,
memo
}
}
};

var response = await getGraphQLHttpClient().SendMutationAsync<LnInvoiceCreateResponse>(createLnInvoiceRequest, cancellationToken);
var graphQlErrors = response.Errors;

if (graphQlErrors != null && graphQlErrors.Length > 0)
{
var messages = string.Join(", ", graphQlErrors.Select(e => e.Message));
throw new Exception($"An error occured with invoice creation query: {messages}");
}

var invoiceData = response.Data.LnInvoiceCreate != null
? response.Data.LnInvoiceCreate
: response.Data.LnUsdInvoiceCreate;

if (invoiceData.Errors.Count > 0)
{
var messages = string.Join(", ", invoiceData.Errors.Select(e => e.Message));
throw new Exception($"An error occured with invoice creation: {messages}");
}

return new LightningInvoice
{
Amount = amount,
PaymentHash = invoiceData.Invoice.PaymentHash,
ExpiresAt = DateTimeOffset.UtcNow.Add(expiresIn),
AmountReceived = invoiceData.Invoice.Satoshis,
Status = LightningInvoiceStatus.Unpaid,
BOLT11 = invoiceData.Invoice.PaymentRequest,
Preimage = invoiceData.Invoice.PaymentSecret
};
}

private GraphQLHttpClient getGraphQLHttpClient() {
var graphQLClient = new GraphQLHttpClient(_baseUri, new NewtonsoftJsonSerializer());

graphQLClient.HttpClient.DefaultRequestHeaders.Clear();
graphQLClient.HttpClient.DefaultRequestHeaders.Add("X-API-KEY", _apiKey);
graphQLClient.HttpClient.DefaultRequestHeaders.Add("User-Agent", "BTCPayServer.Lightning.BlinkApiClient");

return graphQLClient;
}
}
Loading