mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 18:11:36 +01:00
Add QR code scan/show for PSBT + Import wallet via QR (#1931)
* Add PSBT QR code scan/show This PR introduces support to show and read PSBTs in BC-UR format via animated QR codes. This allows you to use BTCPay with HW devices such as Cobo Vault and Blue wallet to sign transactions without ever exposing the keys outside of that device. Spec: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md I've also bumped the QR code library we sue as it had a bug with large datasets. * Reuse same code for all and allow wallet import via QR code scan * remove unecessary js vendor files * Allow export wallet from settings via QR * formatting * bundle * fix wallet receive bundle
This commit is contained in:
parent
5979fe5eef
commit
4176f3659b
@ -102,7 +102,8 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
if (vm.WalletFile != null)
|
if (vm.WalletFile != null)
|
||||||
{
|
{
|
||||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy))
|
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network,
|
||||||
|
out strategy))
|
||||||
{
|
{
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
{
|
{
|
||||||
@ -113,6 +114,19 @@ namespace BTCPayServer.Controllers
|
|||||||
return View(nameof(AddDerivationScheme), vm);
|
return View(nameof(AddDerivationScheme), vm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
|
||||||
|
{
|
||||||
|
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
|
||||||
|
{
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
|
Message = "QR import was not in the correct format"
|
||||||
|
});
|
||||||
|
vm.Confirmation = false;
|
||||||
|
return View(nameof(AddDerivationScheme), vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -122,16 +136,24 @@ namespace BTCPayServer.Controllers
|
|||||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||||
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
||||||
{
|
{
|
||||||
var accountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
var accountKey = string.IsNullOrEmpty(vm.AccountKey)
|
||||||
|
? null
|
||||||
|
: new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||||
if (accountKey != null)
|
if (accountKey != null)
|
||||||
{
|
{
|
||||||
var accountSettings = newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
|
var accountSettings =
|
||||||
|
newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
|
||||||
if (accountSettings != null)
|
if (accountSettings != null)
|
||||||
{
|
{
|
||||||
accountSettings.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
accountSettings.AccountKeyPath =
|
||||||
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||||
|
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
|
||||||
|
? (HDFingerprint?)null
|
||||||
|
: new HDFingerprint(
|
||||||
|
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strategy = newStrategy;
|
strategy = newStrategy;
|
||||||
strategy.Source = vm.Source;
|
strategy.Source = vm.Source;
|
||||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||||
@ -163,7 +185,7 @@ namespace BTCPayServer.Controllers
|
|||||||
var willBeExcluded = !vm.Enabled;
|
var willBeExcluded = !vm.Enabled;
|
||||||
|
|
||||||
var showAddress = // Show addresses if:
|
var showAddress = // Show addresses if:
|
||||||
// - If the user is testing the hint address in confirmation screen
|
// - If the user is testing the hint address in confirmation screen
|
||||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||||
// - The user is clicking on continue after changing the config
|
// - The user is clicking on continue after changing the config
|
||||||
(!vm.Confirmation && oldConfig != vm.Config) ||
|
(!vm.Confirmation && oldConfig != vm.Config) ||
|
||||||
@ -189,22 +211,22 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _Repo.UpdateStore(store);
|
await _Repo.UpdateStore(store);
|
||||||
_EventAggregator.Publish(new WalletChangedEvent()
|
_EventAggregator.Publish(new WalletChangedEvent() {WalletId = new WalletId(storeId, cryptoCode)});
|
||||||
{
|
|
||||||
WalletId = new WalletId(storeId, cryptoCode)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (willBeExcluded != wasExcluded)
|
if (willBeExcluded != wasExcluded)
|
||||||
{
|
{
|
||||||
var label = willBeExcluded ? "disabled" : "enabled";
|
var label = willBeExcluded ? "disabled" : "enabled";
|
||||||
TempData[WellKnownTempData.SuccessMessage] = $"On-Chain payments for {network.CryptoCode} has been {label}.";
|
TempData[WellKnownTempData.SuccessMessage] =
|
||||||
|
$"On-Chain payments for {network.CryptoCode} has been {label}.";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
TempData[WellKnownTempData.SuccessMessage] = $"Derivation settings for {network.CryptoCode} has been modified.";
|
TempData[WellKnownTempData.SuccessMessage] =
|
||||||
|
$"Derivation settings for {network.CryptoCode} has been modified.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is success case when derivation scheme is added to the store
|
// This is success case when derivation scheme is added to the store
|
||||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrEmpty(vm.HintAddress))
|
else if (!string.IsNullOrEmpty(vm.HintAddress))
|
||||||
{
|
{
|
||||||
@ -269,7 +291,7 @@ namespace BTCPayServer.Controllers
|
|||||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
Html = $"There was an error generating your wallet: {e.Message}"
|
Html = $"There was an error generating your wallet: {e.Message}"
|
||||||
});
|
});
|
||||||
return RedirectToAction(nameof(AddDerivationScheme), new { storeId, cryptoCode });
|
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response == null)
|
if (response == null)
|
||||||
@ -279,7 +301,7 @@ namespace BTCPayServer.Controllers
|
|||||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
Html = "There was an error generating your wallet. Is your node available?"
|
Html = "There was an error generating your wallet. Is your node available?"
|
||||||
});
|
});
|
||||||
return RedirectToAction(nameof(AddDerivationScheme), new { storeId, cryptoCode });
|
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
|
||||||
}
|
}
|
||||||
|
|
||||||
var store = HttpContext.GetStoreData();
|
var store = HttpContext.GetStoreData();
|
||||||
@ -315,7 +337,7 @@ namespace BTCPayServer.Controllers
|
|||||||
Mnemonic = response.Mnemonic,
|
Mnemonic = response.Mnemonic,
|
||||||
Passphrase = response.Passphrase,
|
Passphrase = response.Passphrase,
|
||||||
IsStored = request.SavePrivateKeys,
|
IsStored = request.SavePrivateKeys,
|
||||||
ReturnUrl = Url.Action(nameof(UpdateStore), new { storeId })
|
ReturnUrl = Url.Action(nameof(UpdateStore), new {storeId})
|
||||||
};
|
};
|
||||||
return this.RedirectToRecoverySeedBackup(vm);
|
return this.RedirectToRecoverySeedBackup(vm);
|
||||||
}
|
}
|
||||||
@ -332,7 +354,8 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
|
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
|
||||||
{
|
{
|
||||||
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
|
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
|
||||||
|
.Succeeded;
|
||||||
if (isAdmin)
|
if (isAdmin)
|
||||||
return (true, true);
|
return (true, true);
|
||||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||||
|
@ -80,6 +80,7 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
vm.Decoded = psbt.ToString();
|
vm.Decoded = psbt.ToString();
|
||||||
vm.PSBT = psbt.ToBase64();
|
vm.PSBT = psbt.ToBase64();
|
||||||
|
vm.PSBTHex = psbt.ToHex();
|
||||||
}
|
}
|
||||||
|
|
||||||
return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
|
return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
|
||||||
@ -104,6 +105,8 @@ namespace BTCPayServer.Controllers
|
|||||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vm.PSBTHex = psbt.ToHex();
|
||||||
var res = await TryHandleSigningCommands(walletId, psbt, command, new SigningContextModel(psbt));
|
var res = await TryHandleSigningCommands(walletId, psbt, command, new SigningContextModel(psbt));
|
||||||
if (res != null)
|
if (res != null)
|
||||||
{
|
{
|
||||||
@ -117,6 +120,7 @@ namespace BTCPayServer.Controllers
|
|||||||
ModelState.Remove(nameof(vm.FileName));
|
ModelState.Remove(nameof(vm.FileName));
|
||||||
ModelState.Remove(nameof(vm.UploadedPSBTFile));
|
ModelState.Remove(nameof(vm.UploadedPSBTFile));
|
||||||
vm.PSBT = psbt.ToBase64();
|
vm.PSBT = psbt.ToBase64();
|
||||||
|
vm.PSBTHex = psbt.ToHex();
|
||||||
vm.FileName = vm.UploadedPSBTFile?.FileName;
|
vm.FileName = vm.UploadedPSBTFile?.FileName;
|
||||||
return View(vm);
|
return View(vm);
|
||||||
|
|
||||||
|
@ -35,6 +35,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
|
|
||||||
[Display(Name = "Wallet File")]
|
[Display(Name = "Wallet File")]
|
||||||
public IFormFile WalletFile { get; set; }
|
public IFormFile WalletFile { get; set; }
|
||||||
|
[Display(Name = "Wallet File Content")]
|
||||||
|
public string WalletFileContent { get; set; }
|
||||||
public string Config { get; set; }
|
public string Config { get; set; }
|
||||||
public string Source { get; set; }
|
public string Source { get; set; }
|
||||||
public string DerivationSchemeFormat { get; set; }
|
public string DerivationSchemeFormat { get; set; }
|
||||||
|
@ -13,6 +13,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
public string Decoded { get; set; }
|
public string Decoded { get; set; }
|
||||||
string _FileName;
|
string _FileName;
|
||||||
|
public string PSBTHex { get; set; }
|
||||||
public bool NBXSeedAvailable { get; set; }
|
public bool NBXSeedAvailable { get; set; }
|
||||||
|
|
||||||
public string FileName
|
public string FileName
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.WalletViewModels
|
namespace BTCPayServer.Models.WalletViewModels
|
||||||
{
|
{
|
||||||
@ -22,6 +23,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
|
|
||||||
public class WalletSettingsAccountKeyViewModel
|
public class WalletSettingsAccountKeyViewModel
|
||||||
{
|
{
|
||||||
|
[JsonProperty("ExtPubKey")]
|
||||||
[DisplayName("Account key")]
|
[DisplayName("Account key")]
|
||||||
public string AccountKey { get; set; }
|
public string AccountKey { get; set; }
|
||||||
[DisplayName("Master fingerprint")]
|
[DisplayName("Master fingerprint")]
|
||||||
|
220
BTCPayServer/Views/Shared/CameraScanner.cshtml
Normal file
220
BTCPayServer/Views/Shared/CameraScanner.cshtml
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<div id="camera-qr-scanner-modal-app" v-cloak class="only-for-js">
|
||||||
|
<div class="modal fade" data-backdrop="static" :id="modalId">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
{{title}}
|
||||||
|
<span v-if="workload.length > 0">Animated QR detected: {{workload.length}} / {{workload[0].total}} scanned</span>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" v-on:click="close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-0" v-if="loaded" v-bind:class="{'alert-danger': errorMessage}">
|
||||||
|
<qrcode-drop-zone v-on:decode="onDecode" v-on:init="logErrors">
|
||||||
|
<qrcode-stream v-on:decode="onDecode" v-on:init="onInit" v-bind:camera="camera" v-bind:track="paint">
|
||||||
|
<div v-if="data || errorMessage" class="pending-action">
|
||||||
|
|
||||||
|
<div class="text-danger p-2" v-if="errorMessage">{{errorMessage}}</div>
|
||||||
|
<span class="text-muted text-truncate">{{data}}</span>
|
||||||
|
<div class="w-100 btn-group">
|
||||||
|
<button v-if="data" type="button" class="btn btn-primary" data-dismiss="modal" v-on:click="submitData">Submit</button>
|
||||||
|
<button type="button" class="btn btn-secondary" v-on:click="retry">Retry</button>
|
||||||
|
<button type="button" class="btn btn-danger" data-dismiss="modal" v-on:click="close">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</qrcode-stream>
|
||||||
|
</qrcode-drop-zone>
|
||||||
|
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" v-bind:camera="camera"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.pending-action {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, .8);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
padding: 10px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function initCameraScanningApp(title, onDataSubmit, modalId)
|
||||||
|
{
|
||||||
|
|
||||||
|
new Vue(
|
||||||
|
{
|
||||||
|
el: '#camera-qr-scanner-modal-app',
|
||||||
|
data:
|
||||||
|
{
|
||||||
|
noStreamApiSupport: false,
|
||||||
|
loaded: false,
|
||||||
|
workload: [],
|
||||||
|
data: "",
|
||||||
|
title: title,
|
||||||
|
errorMessage: "",
|
||||||
|
modalId: modalId
|
||||||
|
},
|
||||||
|
mounted: function ()
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
$("#" + this.modalId)
|
||||||
|
.on("shown.bs.modal", function ()
|
||||||
|
{
|
||||||
|
self.loaded = true;
|
||||||
|
})
|
||||||
|
.on("hide.bs.modal", function ()
|
||||||
|
{
|
||||||
|
self.close();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
computed:
|
||||||
|
{
|
||||||
|
camera: function ()
|
||||||
|
{
|
||||||
|
return this.data ? "off" : "auto";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods:
|
||||||
|
{
|
||||||
|
retry: function ()
|
||||||
|
{
|
||||||
|
if (!this.data)
|
||||||
|
{
|
||||||
|
this.close();
|
||||||
|
this.$nextTick(function ()
|
||||||
|
{
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.data = "";
|
||||||
|
this.workload = [];
|
||||||
|
this.errorMessage = "";
|
||||||
|
},
|
||||||
|
close: function ()
|
||||||
|
{
|
||||||
|
this.loaded = false;
|
||||||
|
this.data = "";
|
||||||
|
this.workload = [];
|
||||||
|
this.errorMessage = "";
|
||||||
|
},
|
||||||
|
onDecode: function (content)
|
||||||
|
{
|
||||||
|
if (this.data)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!content.toLowerCase().startsWith("ur:"))
|
||||||
|
{
|
||||||
|
this.data = content;
|
||||||
|
this.workload = [];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const [index, total] = window.bcur.extractSingleWorkload(content);
|
||||||
|
if (this.workload.length > 0)
|
||||||
|
{
|
||||||
|
const currentTotal = this.workload[0].total;
|
||||||
|
if (total !== currentTotal)
|
||||||
|
{
|
||||||
|
this.workload = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.workload.find(i => i.index === index))
|
||||||
|
{
|
||||||
|
this.workload.push(
|
||||||
|
{
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
data: content,
|
||||||
|
});
|
||||||
|
if (this.workload.length === total)
|
||||||
|
{
|
||||||
|
this.data = window.bcur.decodeUR(this.workload.map(i => i.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitData: function ()
|
||||||
|
{
|
||||||
|
if (onDataSubmit)
|
||||||
|
{
|
||||||
|
onDataSubmit(this.data);
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
logErrors: function (promise)
|
||||||
|
{
|
||||||
|
promise.catch(console.error)
|
||||||
|
},
|
||||||
|
paint: function (location, ctx)
|
||||||
|
{
|
||||||
|
ctx.fillStyle = '#137547';
|
||||||
|
[
|
||||||
|
location.topLeftFinderPattern,
|
||||||
|
location.topRightFinderPattern,
|
||||||
|
location.bottomLeftFinderPattern
|
||||||
|
].forEach((
|
||||||
|
{
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
}) =>
|
||||||
|
{
|
||||||
|
ctx.fillRect(x - 5, y - 5, 10, 10);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onInit: function (promise)
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
promise.then(() =>
|
||||||
|
{
|
||||||
|
self.errorMessage = "";
|
||||||
|
})
|
||||||
|
.catch(error =>
|
||||||
|
{
|
||||||
|
if (error.name === 'StreamApiNotSupportedError')
|
||||||
|
{
|
||||||
|
self.noStreamApiSupport = true;
|
||||||
|
}
|
||||||
|
else if (error.name === 'NotAllowedError')
|
||||||
|
{
|
||||||
|
self.errorMessage = 'A permission to the camera is needed to scan the QR code.'
|
||||||
|
}
|
||||||
|
else if (error.name === 'NotFoundError')
|
||||||
|
{
|
||||||
|
self.errorMessage = 'A camera was not detected on your device.'
|
||||||
|
}
|
||||||
|
else if (error.name === 'NotSupportedError')
|
||||||
|
{
|
||||||
|
self.errorMessage = 'This page is served in non-secure context (HTTPS, localhost or file://)'
|
||||||
|
}
|
||||||
|
else if (error.name === 'NotReadableError')
|
||||||
|
{
|
||||||
|
self.errorMessage = 'Couldn\'t access your camera. Is it already in use?'
|
||||||
|
}
|
||||||
|
else if (error.name === 'OverconstrainedError')
|
||||||
|
{
|
||||||
|
self.errorMessage = 'Constraints don\'t match any installed camera.'
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.errorMessage = 'UNKNOWN ERROR: ' + error.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
105
BTCPayServer/Views/Shared/ShowQR.cshtml
Normal file
105
BTCPayServer/Views/Shared/ShowQR.cshtml
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<div id="scan-qr-modal-app">
|
||||||
|
<div class="modal" tabindex="-1" role="dialog" :id="modalId">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{title}} <template v-if="fragments.length > 1">({{index+1}}/{{fragments.length}})</template></h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body ">
|
||||||
|
<div class="qr-container text-center" style="min-height: 256px;">
|
||||||
|
<qrcode v-bind:value="currentFragment" :options="{ width: 256,height:256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }">
|
||||||
|
</qrcode>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function initQRShow(title, data, modalId)
|
||||||
|
{
|
||||||
|
return new Vue(
|
||||||
|
{
|
||||||
|
el: '#scan-qr-modal-app',
|
||||||
|
components:
|
||||||
|
{
|
||||||
|
qrcode: VueQrcode
|
||||||
|
},
|
||||||
|
data:
|
||||||
|
{
|
||||||
|
index: -1,
|
||||||
|
title: title,
|
||||||
|
speed: 500,
|
||||||
|
data: data,
|
||||||
|
fragments: [],
|
||||||
|
active: false,
|
||||||
|
modalId: modalId
|
||||||
|
},
|
||||||
|
computed:
|
||||||
|
{
|
||||||
|
currentFragment: function ()
|
||||||
|
{
|
||||||
|
return this.fragments[this.index];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: function ()
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
$("#" + this.modalId)
|
||||||
|
.on("shown.bs.modal", function ()
|
||||||
|
{
|
||||||
|
self.start();
|
||||||
|
})
|
||||||
|
.on("hide.bs.modal", function ()
|
||||||
|
{
|
||||||
|
self.active = false;
|
||||||
|
});
|
||||||
|
self.setFragments();
|
||||||
|
},
|
||||||
|
watch:
|
||||||
|
{
|
||||||
|
data: function ()
|
||||||
|
{
|
||||||
|
this.setFragments();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods:
|
||||||
|
{
|
||||||
|
setFragments: function ()
|
||||||
|
{
|
||||||
|
if (!this.data)
|
||||||
|
{
|
||||||
|
this.fragments = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.fragments = window.bcur.encodeUR(this.data.toString(), 200);
|
||||||
|
},
|
||||||
|
start: function ()
|
||||||
|
{
|
||||||
|
this.active = true;
|
||||||
|
this.index = -1;
|
||||||
|
this.playNext();
|
||||||
|
},
|
||||||
|
playNext: function ()
|
||||||
|
{
|
||||||
|
if (!this.active)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.index++;
|
||||||
|
if (this.index > (this.fragments.length - 1))
|
||||||
|
{
|
||||||
|
this.index = 0;
|
||||||
|
}
|
||||||
|
setTimeout(this.playNext, this.speed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,4 +1,5 @@
|
|||||||
@model DerivationSchemeViewModel
|
@model DerivationSchemeViewModel
|
||||||
|
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
ViewData.SetActivePageAndTitle(StoreNavPages.Index, $"{Model.CryptoCode} Derivation scheme");
|
ViewData.SetActivePageAndTitle(StoreNavPages.Index, $"{Model.CryptoCode} Derivation scheme");
|
||||||
@ -94,6 +95,7 @@
|
|||||||
<button class="dropdown-item check-for-vault" type="button">... a hardware wallet</button>
|
<button class="dropdown-item check-for-vault" type="button">... a hardware wallet</button>
|
||||||
}
|
}
|
||||||
<button class="dropdown-item" type="button" data-toggle="modal" data-target="#electrumimport">... a wallet file (Electrum, Wasabi, Cobo Vault, ColdCard)</button>
|
<button class="dropdown-item" type="button" data-toggle="modal" data-target="#electrumimport">... a wallet file (Electrum, Wasabi, Cobo Vault, ColdCard)</button>
|
||||||
|
<button class="dropdown-item" type="button" data-toggle="modal" data-target="#scanqrModal">... a QR code</button>
|
||||||
@if (Model.CanUseHotWallet)
|
@if (Model.CanUseHotWallet)
|
||||||
{
|
{
|
||||||
<button class="dropdown-item" data-toggle="modal" data-target="#nbxplorergeneratewallet" type="button" id="nbxplorergeneratewalletbtn">... a new/existing seed.</button>
|
<button class="dropdown-item" data-toggle="modal" data-target="#nbxplorergeneratewallet" type="button" id="nbxplorergeneratewalletbtn">... a new/existing seed.</button>
|
||||||
@ -216,7 +218,16 @@
|
|||||||
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
|
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
|
||||||
<script src="~/js/vaultbridge.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
|
<script src="~/js/vaultbridge.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
|
||||||
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
|
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
|
||||||
|
<bundle name="wwwroot/bundles/camera-bundle.min.js"></bundle>
|
||||||
|
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||||
<script>
|
<script>
|
||||||
window.coinName = "@Model.Network.DisplayName.ToLowerInvariant()";
|
window.coinName = "@Model.Network.DisplayName.ToLowerInvariant()";
|
||||||
|
$(function () {
|
||||||
|
initCameraScanningApp("Scan wallet QR", function(data){
|
||||||
|
$("#WalletFileContent").val(data);
|
||||||
|
$("#qr-import-form").submit();
|
||||||
|
},"scanqrModal");
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
<partial name="AddDerivationSchemes_NBXWalletGenerate" model="@(new GenerateWalletRequest())"/>
|
<partial name="AddDerivationSchemes_NBXWalletGenerate" model="@(new GenerateWalletRequest())"/>
|
||||||
}
|
}
|
||||||
|
<partial name="CameraScanner"/>
|
||||||
|
<form id="qr-import-form" method="post">
|
||||||
|
<input type="hidden" asp-for="WalletFileContent"/>
|
||||||
|
</form>
|
||||||
<div class="modal fade" id="electrumimport" tabindex="-1" role="dialog" aria-labelledby="electrumimport" aria-hidden="true">
|
<div class="modal fade" id="electrumimport" tabindex="-1" role="dialog" aria-labelledby="electrumimport" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
<form class="modal-content" method="post" enctype="multipart/form-data">
|
<form class="modal-content" method="post" enctype="multipart/form-data">
|
||||||
@ -62,7 +65,7 @@
|
|||||||
|
|
||||||
<template id="btcpayservervault_template">
|
<template id="btcpayservervault_template">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<form class="modal-content" form method="post" enctype="multipart/form-data">
|
<form class="modal-content" method="post" enctype="multipart/form-data">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="exampleModalLabel">Import from BTCPayServer Vault</h5>
|
<h5 class="modal-title" id="exampleModalLabel">Import from BTCPayServer Vault</h5>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
@model WalletSendModel
|
|
||||||
|
|
||||||
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true" />
|
|
||||||
<div id="wallet-camera-app" v-cloak class="only-for-js">
|
|
||||||
<div class="modal fade" data-backdrop="static" id="scanModal">
|
|
||||||
|
|
||||||
<div class="modal-dialog" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
|
|
||||||
<div class="modal-body p-0" v-if="loaded" v-bind:class="{'alert-danger': errorMessage}">
|
|
||||||
<div class="p-2" style="position: absolute; right: 0; top: 0; width: 100%; z-index:5">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" v-on:click="close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<qrcode-drop-zone v-on:decode="onDecode" v-on:init="logErrors">
|
|
||||||
<qrcode-stream v-on:decode="onDecode" v-on:init="onInit" v-bind:camera="camera" v-bind:track="paint">
|
|
||||||
<div v-if="data || errorMessage" class="pending-action">
|
|
||||||
|
|
||||||
<div class="text-danger p-2" v-if="errorMessage">{{errorMessage}}</div>
|
|
||||||
<span class="text-muted">{{data}}</span>
|
|
||||||
<div class="w-100 btn-group">
|
|
||||||
<button v-if="data" type="button" class="btn btn-primary" data-dismiss="modal" v-on:click="submitData">Submit</button>
|
|
||||||
<button type="button" class="btn btn-secondary" v-on:click="retry">Retry</button>
|
|
||||||
<button type="button" class="btn btn-danger" data-dismiss="modal" v-on:click="close">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</qrcode-stream>
|
|
||||||
</qrcode-drop-zone>
|
|
||||||
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" v-bind:camera="camera"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
.pending-action {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
background-color: rgba(255, 255, 255, .8);
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
padding: 10px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,4 +1,5 @@
|
|||||||
@model WalletPSBTViewModel
|
@model WalletPSBTViewModel
|
||||||
|
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
ViewData["Title"] = "PSBT";
|
ViewData["Title"] = "PSBT";
|
||||||
@ -8,7 +9,7 @@
|
|||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-10 text-center">
|
<div class="col-md-10 text-center">
|
||||||
<partial name="_StatusMessage" />
|
<partial name="_StatusMessage"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -17,10 +18,13 @@
|
|||||||
@if (Model.Errors != null && Model.Errors.Count != 0)
|
@if (Model.Errors != null && Model.Errors.Count != 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger alert-dismissible" role="alert">
|
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
@foreach (var error in Model.Errors)
|
@foreach (var error in Model.Errors)
|
||||||
{
|
{
|
||||||
<span>@error</span><br />
|
<span>@error</span>
|
||||||
|
<br/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -29,11 +33,11 @@
|
|||||||
<h3>Decoded PSBT</h3>
|
<h3>Decoded PSBT</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@this.Context.GetRouteValue("walletId")">
|
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@this.Context.GetRouteValue("walletId")">
|
||||||
<input type="hidden" asp-for="CryptoCode" />
|
<input type="hidden" asp-for="CryptoCode"/>
|
||||||
<input type="hidden" asp-for="NBXSeedAvailable" />
|
<input type="hidden" asp-for="NBXSeedAvailable"/>
|
||||||
<input type="hidden" asp-for="PSBT" />
|
<input type="hidden" asp-for="PSBT"/>
|
||||||
<input type="hidden" asp-for="FileName" />
|
<input type="hidden" asp-for="FileName"/>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<partial name="WalletSigningMenu" model="@((Model.CryptoCode, Model.NBXSeedAvailable))"/>
|
<partial name="WalletSigningMenu" model="@((Model.CryptoCode, Model.NBXSeedAvailable))"/>
|
||||||
<div class="ml-2 dropdown">
|
<div class="ml-2 dropdown">
|
||||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="OtherActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="OtherActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
@ -44,6 +48,7 @@
|
|||||||
<button name="command" type="submit" class="dropdown-item" value="update">Update</button>
|
<button name="command" type="submit" class="dropdown-item" value="update">Update</button>
|
||||||
<button name="command" type="submit" class="dropdown-item" value="combine">Combine</button>
|
<button name="command" type="submit" class="dropdown-item" value="combine">Combine</button>
|
||||||
<button name="command" type="submit" class="dropdown-item" value="save-psbt">Download</button>
|
<button name="command" type="submit" class="dropdown-item" value="save-psbt">Download</button>
|
||||||
|
<button name="command" type="button" class="dropdown-item only-for-js" data-toggle="modal" data-target="#scan-qr-modal">Show QR</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -58,18 +63,40 @@
|
|||||||
<label asp-for="PSBT"></label>
|
<label asp-for="PSBT"></label>
|
||||||
<textarea class="form-control" rows="5" asp-for="PSBT"></textarea>
|
<textarea class="form-control" rows="5" asp-for="PSBT"></textarea>
|
||||||
<span asp-validation-for="PSBT" class="text-danger"></span>
|
<span asp-validation-for="PSBT" class="text-danger"></span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="UploadedPSBTFile"></label>
|
<label asp-for="UploadedPSBTFile"></label>
|
||||||
<input type="file" class="form-control-file" asp-for="UploadedPSBTFile">
|
<input type="file" class="form-control-file" asp-for="UploadedPSBTFile">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" name="command" value="decode" class="btn btn-primary">Decode</button>
|
|
||||||
|
<button type="button" id="scanqrcode" class="ml-2 btn btn-secondary only-for-js" data-toggle="modal" data-target="#scanModal" title="Scan with camera">
|
||||||
|
<i class="fa fa-camera"></i>
|
||||||
|
</button>
|
||||||
|
<button type="submit" name="command" value="decode" class="btn btn-primary" id="Decode">Decode</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<partial name="ShowQR"/>
|
||||||
|
<partial name="CameraScanner"/>
|
||||||
|
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css" asp-append-version="true">
|
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css" asp-append-version="true">
|
||||||
<script src="~/vendor/highlightjs/highlight.min.js" asp-append-version="true"></script>
|
<script src="~/vendor/highlightjs/highlight.min.js" asp-append-version="true"></script>
|
||||||
|
<bundle name="wwwroot/bundles/camera-bundle.min.js"></bundle>
|
||||||
|
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||||
<script>hljs.initHighlightingOnLoad();</script>
|
<script>hljs.initHighlightingOnLoad();</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
$(function (){
|
||||||
|
initQRShow("Scan PSBT", @Json.Serialize(Model.PSBTHex), "scan-qr-modal");
|
||||||
|
|
||||||
|
initCameraScanningApp("Scan PSBT", function (data){
|
||||||
|
$("textarea[name=PSBT]").val(data);
|
||||||
|
$("#Decode").click();
|
||||||
|
}, "scanModal")
|
||||||
|
});
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@using Microsoft.AspNetCore.Mvc.ModelBinding
|
|
||||||
|
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||||
@model BTCPayServer.Controllers.WalletReceiveViewModel
|
@model BTCPayServer.Controllers.WalletReceiveViewModel
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
@ -74,7 +75,7 @@
|
|||||||
@section HeadScripts
|
@section HeadScripts
|
||||||
|
|
||||||
{
|
{
|
||||||
<script src="~/bundles/lightning-node-info-bundle.min.js" type="text/javascript" asp-append-version="true"></script>
|
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" asp-append-version="true" />
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var srvModel = @Safe.Json(Model);
|
var srvModel = @Safe.Json(Model);
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<partial name="WalletCameraScanner"/>
|
<partial name="CameraScanner"/>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="@(!Model.InputSelection && Model.Outputs.Count==1? "col-lg-7 transaction-output-form": "col-lg-8")">
|
<div class="@(!Model.InputSelection && Model.Outputs.Count==1? "col-lg-7 transaction-output-form": "col-lg-8")">
|
||||||
@ -229,7 +229,8 @@
|
|||||||
|
|
||||||
@section HeadScripts
|
@section HeadScripts
|
||||||
{
|
{
|
||||||
<bundle name="wwwroot/bundles/wallet-send-bundle.min.js" asp-append-version="true"></bundle>
|
<bundle name="wwwroot/bundles/wallet-send-bundle.min.js"></bundle>
|
||||||
|
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||||
<style>
|
<style>
|
||||||
.remove-destination-btn { font-size: 1.5rem; }
|
.remove-destination-btn { font-size: 1.5rem; }
|
||||||
.remove-destination-btn:hover { border-color: var(--btcpay-color-danger); }
|
.remove-destination-btn:hover { border-color: var(--btcpay-color-danger); }
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
@model WalletSettingsViewModel
|
@using Newtonsoft.Json
|
||||||
|
@using System.Text
|
||||||
|
@using NBitcoin.DataEncoders
|
||||||
|
@model WalletSettingsViewModel
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
ViewData["Title"] = "Wallet settings";
|
ViewData["Title"] = "Wallet settings";
|
||||||
@ -37,8 +40,9 @@
|
|||||||
@for (int i = 0; i < Model.AccountKeys.Count; i++)
|
@for (int i = 0; i < Model.AccountKeys.Count; i++)
|
||||||
{
|
{
|
||||||
<hr class="my-4"/>
|
<hr class="my-4"/>
|
||||||
|
<span class="d-flex">
|
||||||
<h4 class="mb-3">Account key @i</h4>
|
<h4 class="mb-3">Account key @i</h4>
|
||||||
|
<button type="button" class="btn btn-link only-for-js mb-2 fa fa-qrcode text-decoration-none" data-wallet="@i" title="Show QR"></button></span>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="@Model.AccountKeys[i].AccountKey"></label>
|
<label asp-for="@Model.AccountKeys[i].AccountKey"></label>
|
||||||
<input asp-for="@Model.AccountKeys[i].AccountKey" class="form-control" readonly/>
|
<input asp-for="@Model.AccountKeys[i].AccountKey" class="form-control" readonly/>
|
||||||
@ -86,9 +90,25 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||||
|
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||||
|
<script src="~/vendor/bc-ur/web-bundle.js" asp-append-version="true"></script>
|
||||||
|
<script src="~/vendor/vue-qrcode-reader/vue-qrcode-reader.browser.js" asp-append-version="true"></script>
|
||||||
|
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||||
|
|
||||||
|
<partial name="ShowQR"/>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function(){
|
var wallets = @Safe.Json(Model.AccountKeys.Select(model => Encoders.Hex.EncodeData(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)))));
|
||||||
|
$(document).ready(function(){
|
||||||
|
var qrApp = initQRShow("Wallet QR", "", "scan-qr-modal");
|
||||||
|
$("button[data-wallet]").on("click", function (){
|
||||||
|
var data = $(this).data("wallet");
|
||||||
|
var wallet = wallets[parseInt(data)];
|
||||||
|
qrApp.data = wallet;
|
||||||
|
$("#scan-qr-modal").modal("show");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
if(navigator.registerProtocolHandler){
|
if(navigator.registerProtocolHandler){
|
||||||
$(".register-wallet")
|
$(".register-wallet")
|
||||||
.show()
|
.show()
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<div class="dropdown-menu" aria-labelledby="SendMenu">
|
<div class="dropdown-menu" aria-labelledby="SendMenu">
|
||||||
@if (Model.CryptoCode == "BTC")
|
@if (Model.CryptoCode == "BTC")
|
||||||
{
|
{
|
||||||
<button name="command" type="submit" class="dropdown-item" value="vault">... a hardware wallet</button>
|
<button name="command" type="submit" class="dropdown-item" value="vault">... a hardware wallet</button>
|
||||||
}
|
}
|
||||||
<button name="command" type="submit" class="dropdown-item" value="seed">... an HD private key or mnemonic seed</button>
|
<button name="command" type="submit" class="dropdown-item" value="seed">... an HD private key or mnemonic seed</button>
|
||||||
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... a wallet supporting PSBT</button>
|
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... a wallet supporting PSBT</button>
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
@using BTCPayServer.Views.Wallets
|
@using BTCPayServer.Views.Wallets
|
||||||
@using BTCPayServer.Models.WalletViewModels
|
@using BTCPayServer.Models.WalletViewModels
|
||||||
|
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
"wwwroot/vendor/clipboard.js/clipboard.js",
|
"wwwroot/vendor/clipboard.js/clipboard.js",
|
||||||
"wwwroot/vendor/jquery/jquery.js",
|
"wwwroot/vendor/jquery/jquery.js",
|
||||||
"wwwroot/vendor/vuejs/vue.min.js",
|
"wwwroot/vendor/vuejs/vue.min.js",
|
||||||
"wwwroot/vendor/vue-qrcode/vue-qrcode.js",
|
"wwwroot/vendor/vue-qrcode/vue-qrcode.min.js",
|
||||||
"wwwroot/vendor/i18next/i18next.js",
|
"wwwroot/vendor/i18next/i18next.js",
|
||||||
"wwwroot/vendor/i18next/i18nextXHRBackend.js",
|
"wwwroot/vendor/i18next/i18nextXHRBackend.js",
|
||||||
"wwwroot/vendor/i18next/vue-i18next.js",
|
"wwwroot/vendor/i18next/vue-i18next.js",
|
||||||
@ -64,7 +64,7 @@
|
|||||||
"wwwroot/vendor/clipboard.js/clipboard.js",
|
"wwwroot/vendor/clipboard.js/clipboard.js",
|
||||||
"wwwroot/vendor/jquery/jquery.js",
|
"wwwroot/vendor/jquery/jquery.js",
|
||||||
"wwwroot/vendor/vuejs/vue.min.js",
|
"wwwroot/vendor/vuejs/vue.min.js",
|
||||||
"wwwroot/vendor/vue-qrcode/vue-qrcode.js",
|
"wwwroot/vendor/vue-qrcode/vue-qrcode.min.js",
|
||||||
"wwwroot/vendor/vue-toasted/vue-toasted.min.js"
|
"wwwroot/vendor/vue-toasted/vue-toasted.min.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -196,6 +196,7 @@
|
|||||||
"inputFiles": [
|
"inputFiles": [
|
||||||
"wwwroot/vendor/vuejs/vue.min.js",
|
"wwwroot/vendor/vuejs/vue.min.js",
|
||||||
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
||||||
|
"wwwroot/vendor/bc-ur/web-bundle.js",
|
||||||
"wwwroot/vendor/vue-qrcode-reader/vue-qrcode-reader.browser.js",
|
"wwwroot/vendor/vue-qrcode-reader/vue-qrcode-reader.browser.js",
|
||||||
"wwwroot/js/wallet/**/*.js"
|
"wwwroot/js/wallet/**/*.js"
|
||||||
],
|
],
|
||||||
@ -209,5 +210,17 @@
|
|||||||
"wwwroot/modal/btcpay.js",
|
"wwwroot/modal/btcpay.js",
|
||||||
"wwwroot/shopify/btcpay-shopify.js"
|
"wwwroot/shopify/btcpay-shopify.js"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outputFileName": "wwwroot/bundles/camera-bundle.min.js",
|
||||||
|
"inputFiles": [
|
||||||
|
"wwwroot/vendor/vuejs/vue.min.js",
|
||||||
|
"wwwroot/vendor/vue-qrcode/vue-qrcode.min.js",
|
||||||
|
"wwwroot/vendor/bc-ur/web-bundle.js",
|
||||||
|
"wwwroot/vendor/vue-qrcode-reader/vue-qrcode-reader.browser.js"
|
||||||
|
],
|
||||||
|
"minify": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,83 +1,6 @@
|
|||||||
$(function () {
|
$(function () {
|
||||||
new Vue({
|
initCameraScanningApp("Scan address/ payment link", function(data){
|
||||||
el: '#wallet-camera-app',
|
$("#BIP21").val(data);
|
||||||
data: {
|
$("form").submit();
|
||||||
noStreamApiSupport: false,
|
},"scanModal");
|
||||||
loaded: false,
|
|
||||||
data: "",
|
|
||||||
errorMessage: ""
|
|
||||||
},
|
|
||||||
mounted: function () {
|
|
||||||
var self = this;
|
|
||||||
$("#scanqrcode").click(function () {
|
|
||||||
self.loaded = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
camera: function () {
|
|
||||||
return this.data ? "off" : "auto";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
retry: function () {
|
|
||||||
if (!this.data) {
|
|
||||||
this.close();
|
|
||||||
this.$nextTick(function () {
|
|
||||||
this.loaded = true;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.data = "";
|
|
||||||
},
|
|
||||||
close: function () {
|
|
||||||
this.loaded = false;
|
|
||||||
this.data = "";
|
|
||||||
this.errorMessage = "";
|
|
||||||
},
|
|
||||||
onDecode(content) {
|
|
||||||
this.data = decodeURIComponent(content);
|
|
||||||
},
|
|
||||||
submitData: function () {
|
|
||||||
$("#BIP21").val(this.data);
|
|
||||||
$("form").submit();
|
|
||||||
this.close();
|
|
||||||
},
|
|
||||||
logErrors: function (promise) {
|
|
||||||
promise.catch(console.error)
|
|
||||||
},
|
|
||||||
paint: function (location, ctx) {
|
|
||||||
ctx.fillStyle = '#137547';
|
|
||||||
[
|
|
||||||
location.topLeftFinderPattern,
|
|
||||||
location.topRightFinderPattern,
|
|
||||||
location.bottomLeftFinderPattern
|
|
||||||
].forEach(({x, y}) => {
|
|
||||||
ctx.fillRect(x - 5, y - 5, 10, 10);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onInit: function (promise) {
|
|
||||||
var self = this;
|
|
||||||
promise.then(() => {
|
|
||||||
self.errorMessage = "";
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error.name === 'StreamApiNotSupportedError') {
|
|
||||||
self.noStreamApiSupport = true;
|
|
||||||
} else if (error.name === 'NotAllowedError') {
|
|
||||||
self.errorMessage = 'A permission to the camera is needed to scan the QR code.'
|
|
||||||
} else if (error.name === 'NotFoundError') {
|
|
||||||
self.errorMessage = 'A camera was not detected on your device.'
|
|
||||||
} else if (error.name === 'NotSupportedError') {
|
|
||||||
self.errorMessage = 'This page is served in non-secure context (HTTPS, localhost or file://)'
|
|
||||||
} else if (error.name === 'NotReadableError') {
|
|
||||||
self.errorMessage = 'Couldn\'t access your camera. Is it already in use?'
|
|
||||||
} else if (error.name === 'OverconstrainedError') {
|
|
||||||
self.errorMessage = 'Constraints don\'t match any installed camera.'
|
|
||||||
} else {
|
|
||||||
self.errorMessage = 'UNKNOWN ERROR: ' + error.message
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
1
BTCPayServer/wwwroot/vendor/bc-ur/web-bundle.js
vendored
Normal file
1
BTCPayServer/wwwroot/vendor/bc-ur/web-bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
3466
BTCPayServer/wwwroot/vendor/vue-qrcode/vue-qrcode.esm.js
vendored
3466
BTCPayServer/wwwroot/vendor/vue-qrcode/vue-qrcode.esm.js
vendored
File diff suppressed because it is too large
Load Diff
3474
BTCPayServer/wwwroot/vendor/vue-qrcode/vue-qrcode.js
vendored
3474
BTCPayServer/wwwroot/vendor/vue-qrcode/vue-qrcode.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user