front-end for lightning-to-fiat exchange at https://lnurl-pay.me
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
269 lines
7.4 KiB
269 lines
7.4 KiB
<script> |
|
|
|
export let name; |
|
|
|
import SiteHead from './SiteHead.svelte'; |
|
import SiteCard from './SiteCard.svelte'; |
|
import SiteDeck from './SiteDeck.svelte'; |
|
import InputMask from './InputMask.svelte'; |
|
|
|
import payways from './payways.js'; |
|
|
|
import QR from './QR.svelte'; |
|
|
|
import CTC from './CTC.svelte'; |
|
import Tipped from './Tipped.svelte'; |
|
|
|
import { bech32 } from 'bech32' |
|
import UTF8 from 'utf-8' |
|
|
|
import PayFlow from './PayFlow.svelte'; |
|
import { ecEncrypt } from './ecies.js'; |
|
|
|
let payway = payways[0]; |
|
|
|
setTimeout(()=>{ |
|
payway = payways[0]; |
|
accounts[inputId]=null; |
|
},0); |
|
|
|
let inputId; |
|
let amountMask; |
|
let realAmount; |
|
let extAccount; |
|
let briefAccount; |
|
let encrypt = false; |
|
|
|
$: inputId = payway.iid||payway.id; |
|
$: accountReady = accountComplete && accounts[inputId] |
|
$: amountMask = { |
|
mask: Number, scale:2, |
|
min: payway.min, max:payway.max, |
|
radix:".",mapToRadix:[","], |
|
padFractionalZeros: true, |
|
normalizeZeros: true} |
|
|
|
$: realAmount = amounts[payway.id] && ( |
|
Math.max(Math.min(amounts[payway.id], payway.max),payway.min)) |
|
|
|
$: extAccount = accountReady ? (encrypt? "0g" + ecEncrypt(accounts[inputId].padEnd(8)): accounts[inputId]):""; |
|
|
|
$: briefAccount = extAccount ? (encrypt ? extAccount.slice(0,28)+"…" : extAccount):"" |
|
|
|
function genAutoMemo(payway,account,amount) { |
|
if (!account) |
|
return "...automatic"; |
|
if (amount) |
|
amount = parseFloat(amount) |
|
let sum = amount? |
|
amount.toFixed(amount===Math.round(amount)?0:2) |
|
+ " " + payway.currency.toLowerCase() : "some sats"; |
|
return sum + " to " + payway.id + " " + account; |
|
} |
|
|
|
let autoMemo; |
|
$: autoMemo = genAutoMemo(payway, briefAccount, realAmount) |
|
let lightningAddress; |
|
$: lightningAddress = accountComplete ? genAddress(payway, extAccount, realAmount):"" |
|
|
|
function lnurlEncode(url) { |
|
return bech32.encode("LNURL", bech32.toWords(UTF8.setBytesFromString(url)),2048) |
|
} |
|
|
|
function toHexString(byteArray) { |
|
return Array.from(byteArray, function(byte) { |
|
return ('0' + (byte & 0xFF).toString(16)).slice(-2); |
|
}).join('') |
|
} |
|
|
|
function genAddress(payway,account,amount) { |
|
if (!account || !payway) |
|
return "" |
|
const prefix = amount? |
|
((amount == Math.floor(amount)? ""+amount: |
|
amount.toFixed(2))+payway.currency.toLowerCase()+"-"):"" |
|
if (account[0]=='+') |
|
account=account.slice(1) |
|
return prefix + account + "@" + payway.id + ".lnurl-pay.me" |
|
} |
|
|
|
function genLNURL(payway,account,amount,memo,isEncrypted) { |
|
let params = new URLSearchParams(); |
|
params.set("mtg","pay"); |
|
params.set("p",payway.id); |
|
// don't hex-encode bech32-encoded account |
|
params.set("acc",isEncrypted? account: toHexString(UTF8.setBytesFromString(account))) |
|
params.set("v","1") |
|
if (amount) { |
|
params.set(payway.currency.toLowerCase(),amount) |
|
} |
|
if (memo) { |
|
params.set("m",toHexString(UTF8.setBytesFromString(memo))) |
|
} |
|
return lnurlEncode("https://lnurl-pay.me/pay?"+params.toString()).toUpperCase() |
|
} |
|
|
|
|
|
let accounts = {}; |
|
let amounts = {}; |
|
let memo; |
|
|
|
let lnurl; |
|
|
|
$: lnurl = accountComplete ? |
|
genLNURL(payway, extAccount, realAmount, memo, encrypt) : ""; |
|
|
|
const curr = { |
|
"UAH":"₴", |
|
"RUB":"₽", |
|
"USD":"$", |
|
"KZT":"₸", |
|
"NGN":"₦", |
|
"ZAR":"R", |
|
"GHS":"₵", |
|
"AFN":"Af", |
|
} |
|
|
|
let accountComplete = false; |
|
|
|
const canShare = !!navigator.share; |
|
|
|
let getInvoiceFor; |
|
|
|
</script> |
|
|
|
<SiteHead/> |
|
|
|
<SiteDeck> |
|
<SiteCard> |
|
<p class="form-text">Fill in everything to get your lnurl-pay link</p> |
|
<label class="form-label mb-3">Payment direction |
|
<select class="form-select" autofocus bind:value="{payway}"> |
|
{#each payways as payway} |
|
<option value="{payway}">{payway.name}</option> |
|
{/each} |
|
</select> |
|
</label> |
|
|
|
{#key payway} |
|
<label class="form-label mb-3">{payway.acc} |
|
<div class="input-group"> |
|
<InputMask |
|
unmask |
|
bind:value={accounts[inputId]} |
|
bind:isComplete={accountComplete} |
|
imask={payway.imask||{mask:/.*/}} |
|
autocomplete={payway.autocomplete} |
|
inputmode={payway.inputmode||"numeric"} |
|
placeholder={payway.placeholder} |
|
class="form-control"/> |
|
<span class="input-group-text"> |
|
<div class="form-check-inline form-check input-group-prepend"> |
|
<input class="form-check-input" type="checkbox" id="cbEncrypt" |
|
bind:checked={encrypt}> |
|
<label class="form-check-label" for="cbEncrypt"> |
|
Encrypt |
|
</label> |
|
</div> |
|
</span> |
|
</div> |
|
</label> |
|
<label class="form-label mb-3"> |
|
Amount: {payway.currency} {payway.min} – {payway.max} |
|
<div class="input-group"> |
|
<div class="input-group-prepend"> |
|
<span class="input-group-text">{curr[payway.currency]}</span> |
|
</div> |
|
|
|
<InputMask bind:value={amounts[payway.id]} |
|
unmask |
|
imask={amountMask} |
|
placeholder="determined by payer" |
|
inputmode={"numeric"} |
|
class="form-control"/> |
|
<button class="btn btn-outline-secondary" |
|
type="button" |
|
on:click={()=> amounts[payway.id]=payway.min.toString()}>Min |
|
</button> |
|
<button class="btn btn-outline-secondary" |
|
type="button" |
|
on:click={()=> amounts[payway.id]=payway.max.toString()}>Max |
|
</button> |
|
</div> |
|
</label> |
|
{/key} |
|
<label class="form-label mb-3">Lnurl MEMO |
|
<input bind:value={memo} placeholder="{autoMemo}" |
|
class="form-control"/> |
|
</label> |
|
</SiteCard> |
|
<SiteCard> |
|
{#if lnurl} |
|
<CTC let:id let:action force text={lnurl}> |
|
<div class="form-text user-select-all mb-0 mt-2" |
|
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis" |
|
on:click={action} id={id}>{lnurl} |
|
</div> |
|
</CTC> |
|
{:else} |
|
<div class="form-text mb-2 mt-2">Expecting: {payway.acc.toLowerCase()}</div> |
|
{/if} |
|
<div class="d-flex justify-content-center align-items-stretch m-1" |
|
class:invisible={!lnurl}> |
|
{#key lnurl} |
|
<a href="lightning:{lnurl}"> |
|
<QR value={lnurl} size="{230}" /> |
|
</a> |
|
{/key} |
|
</div> |
|
<div class="btn-group flex-wrap mt-auto mb-2" |
|
role="group" class:invisible={!lnurl}> |
|
<CTC let:id let:action text={lnurl}> |
|
<button type="button" class="btn btn-outline-secondary" |
|
id={id} on:click={action}>Copy</button> |
|
</CTC> |
|
{#if canShare} |
|
<button type="button" class="btn btn-outline-secondary" |
|
on:click={()=>{ navigator.share({text:lnurl})}}>Share |
|
</button> |
|
{/if} |
|
<Tipped let:id> |
|
<a slot="thing" role="button" class="btn btn-outline-secondary" |
|
href="lightning:{lnurl}" {id}>Pay</a> |
|
<div slot="tip">open in your wallet</div> |
|
</Tipped> |
|
|
|
<Tipped let:id> |
|
<button slot="thing" {id} type="button" |
|
class="btn btn-outline-secondary" |
|
on:click="{()=>getInvoiceFor=lnurl}"> |
|
Invoice</button> |
|
<div slot="tip">get an invoice for wallets with no lnurl-pay</div> |
|
</Tipped> |
|
</div> |
|
|
|
{#if lightningAddress} |
|
<CTC let:id let:action force text={lightningAddress}> |
|
<div class="input-group"> |
|
<div class="input-group-prepend"> |
|
<Tipped let:id> |
|
<span slot="thing" {id} class="input-group-text"> |
|
<a href="https://lightningaddress.com/">LA:</a> |
|
</span> |
|
<div slot="tip"> |
|
Lightning address for this payment |
|
</div> |
|
</Tipped> |
|
</div> |
|
<input class="user-select-all form-control" |
|
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis" |
|
tabindex="0" |
|
on:click={action} id={id} value={lightningAddress} readonly/> |
|
</div> |
|
</CTC> |
|
{/if} |
|
|
|
</SiteCard> |
|
</SiteDeck> |
|
|
|
<PayFlow bind:lnurl={getInvoiceFor} hasAmount={amounts[payway.id]}/>
|
|
|