Merge remote-tracking branch 'origin/main' into ext-boltcards-2

This commit is contained in:
ben 2022-08-22 16:05:12 +01:00
commit baf993f185
45 changed files with 2996 additions and 185 deletions

View file

@ -9,9 +9,9 @@ jobs:
postgres: postgres:
image: postgres:latest image: postgres:latest
env: env:
POSTGRES_USER: postgres POSTGRES_USER: lnbits
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: lnbits
POSTGRES_DB: postgres POSTGRES_DB: migration
ports: ports:
# maps tcp port 5432 on service container to the host # maps tcp port 5432 on service container to the host
- 5432:5432 - 5432:5432
@ -36,11 +36,4 @@ jobs:
sudo apt install unzip sudo apt install unzip
- name: Run migrations - name: Run migrations
run: | run: |
rm -rf ./data make test-migration
mkdir -p ./data
export LNBITS_DATA_FOLDER="./data"
unzip tests/data/mock_data.zip -d ./data
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
export LNBITS_DATABASE_URL="postgres://postgres:postgres@0.0.0.0:5432/postgres"
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
poetry run python tools/conv.py

View file

@ -32,11 +32,13 @@ test:
FAKE_WALLET_SECRET="ToTheMoon1" \ FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest poetry run pytest
test-real-wallet: test-real-wallet:
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest poetry run pytest
test-venv: test-venv:
@ -44,7 +46,27 @@ test-venv:
FAKE_WALLET_SECRET="ToTheMoon1" \ FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests ./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-migration:
rm -rf ./migration-data
mkdir -p ./migration-data
unzip tests/data/mock_data.zip -d ./migration-data
HOST=0.0.0.0 \
PORT=5002 \
LNBITS_DATA_FOLDER="./migration-data" \
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
HOST=0.0.0.0 \
PORT=5002 \
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
LNBITS_DATA_FOLDER="./migration-data" \
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
poetry run python tools/conv.py
migration:
poetry run python tools/conv.py
bak: bak:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres # LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres

View file

@ -48,4 +48,25 @@ LNbits currently supports SQLite and PostgreSQL databases. There is a migration
### Adding mock data to `mock_data.zip` ### Adding mock data to `mock_data.zip`
`mock_data.zip` contains a few lines of sample SQLite data and is used in automated GitHub test to see whether your migration in `conv.py` works. Run your extension and save a few lines of data into a SQLite `your_extension.sqlite3` file. Unzip `tests/data/mock_data.zip`, add `your_extension.sqlite3` and zip it again. Add the updated `mock_data.zip` to your PR. `mock_data.zip` contains a few lines of sample SQLite data and is used in automated GitHub test to see whether your migration in `conv.py` works. Run your extension and save a few lines of data into a SQLite `your_extension.sqlite3` file. Unzip `tests/data/mock_data.zip`, add `your_extension.sqlite3`, updated `database.sqlite3` and zip it again. Add the updated `mock_data.zip` to your PR.
### running migration locally
you will need a running postgres database
#### create lnbits user for migration database
```console
sudo su - postgres -c "psql -c 'CREATE ROLE lnbits LOGIN PASSWORD 'lnbits';'"
```
#### create migration database
```console
sudo su - postgres -c "psql -c 'CREATE DATABASE migration;'"
```
#### run the migration
```console
make test-migration
```
sudo su - postgres -c "psql -c 'CREATE ROLE lnbits LOGIN PASSWORD 'lnbits';'"
#### clean migration database afterwards, fails if you try again
```console
sudo su - postgres -c "psql -c 'DROP DATABASE IF EXISTS migration;'"
```

View file

@ -20,7 +20,7 @@ cd lnbits-legend/
sudo apt update sudo apt update
sudo apt install software-properties-common sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt install python3.9 sudo apt install python3.9 python3.9-distutils
curl -sSL https://install.python-poetry.org | python3 - curl -sSL https://install.python-poetry.org | python3 -
export PATH="/home/ubuntu/.local/bin:$PATH" # or whatever is suggested in the poetry install notes printed to terminal export PATH="/home/ubuntu/.local/bin:$PATH" # or whatever is suggested in the poetry install notes printed to terminal
@ -95,6 +95,80 @@ mkdir data
docker run --detach --publish 5000:5000 --name lnbits-legend --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits-legend docker run --detach --publish 5000:5000 --name lnbits-legend --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits-legend
``` ```
## Option 5: Fly.io
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNBits for free on Fly.io for personal use.
First, sign up for an account at [Fly.io](https://fly.io) (no credit card required).
Then, install the Fly.io CLI onto your device [here](https://fly.io/docs/getting-started/installing-flyctl/).
After install is complete, the command will output a command you should copy/paste/run to get `fly` into your `$PATH`. Something like:
```
flyctl was installed successfully to /home/ubuntu/.fly/bin/flyctl
Manually add the directory to your $HOME/.bash_profile (or similar)
export FLYCTL_INSTALL="/home/ubuntu/.fly"
export PATH="$FLYCTL_INSTALL/bin:$PATH"
```
You can either run those commands, then `source ~/.bash_profile` or, if you don't, you'll have to call Fly from `~/.fly/bin/flyctl`.
Once installed, run the following commands.
```
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend
fly auth login
[complete login process]
fly launch
```
You'll be prompted to enter an app name, region, postgres (choose no), deploy now (choose no).
You'll now find a file in the directory called `fly.toml`. Open that file and modify/add the following settings.
Note: Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`. Environment variable strings should be quoted here, so if in `.env` you have `LNBITS_ENDPOINT=https://legend.lnbits.com` in `fly.toml` you should have `LNBITS_ENDPOINT="https://legend.lnbits.com"`.
Note: Don't enter secret environment variables here. Fly.io offers secrets (via the `fly secrets` command) that are exposed as environment variables in your runtime. So, for example, if using the LND_REST funding source, you can run `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`.
```
...
kill_timeout = 30
...
...
[mounts]
source="lnbits_data"
destination="/data"
...
...
[env]
HOST="127.0.0.1"
PORT=5000
LNBITS_FORCE_HTTPS=true
LNBITS_DATA_FOLDER="/data"
${PUT_YOUR_LNBITS_ENV_VARS_HERE}
...
...
[[services]]
internal_port = 5000
...
```
Next, create a volume to store the sqlite database for LNBits. Be sure to choose the same region for the volume that you chose earlier.
```
fly volumes create lnbits_data --size 1
```
You're ready to deploy! Run `fly deploy` and follow the steps to finish deployment. You'll select a `region` (up to you, choose the same as you did for the storage volume previously created), `postgres` (choose no), `deploy` (choose yes).
You can use `fly logs` to view the application logs, or `fly ssh console` to get a ssh shell in the running container.
### Troubleshooting ### Troubleshooting
Problems installing? These commands have helped us install LNbits. Problems installing? These commands have helped us install LNbits.
@ -170,8 +244,9 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# START LNbits # START LNbits
# STOP LNbits # STOP LNbits
# on the LNBits folder, locate and edit 'tools/conv.py' with the relevant credentials poetry run python tools/conv.py
python3 tools/conv.py # or
make migration
``` ```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly. Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
@ -194,15 +269,14 @@ Description=LNbits
[Service] [Service]
# replace with the absolute path of your lnbits installation # replace with the absolute path of your lnbits installation
WorkingDirectory=/home/bitcoin/lnbits WorkingDirectory=/home/lnbits/lnbits-legend
# same here # same here. run `which poetry` if you can't find the poetry binary
ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000 ExecStart=/home/lnbits/.local/bin/poetry run lnbits
# replace with the user that you're running lnbits on # replace with the user that you're running lnbits on
User=bitcoin User=lnbits
Restart=always Restart=always
TimeoutSec=120 TimeoutSec=120
RestartSec=30 RestartSec=30
# this makes sure that you receive logs in real time
Environment=PYTHONUNBUFFERED=1 Environment=PYTHONUNBUFFERED=1
[Install] [Install]

View file

@ -76,7 +76,11 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
# Only the browser sends "text/html" request # Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response # not fail proof, but everything else get's a JSON response
if "text/html" in request.headers["accept"]: if (
request.headers
and "accept" in request.headers
and "text/html" in request.headers["accept"]
):
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
"error.html", "error.html",
{"request": request, "err": f"{exc.errors()} is not a valid UUID."}, {"request": request, "err": f"{exc.errors()} is not a valid UUID."},
@ -197,7 +201,11 @@ def register_exception_handlers(app: FastAPI):
traceback.print_exception(etype, err, tb) traceback.print_exception(etype, err, tb)
exc = traceback.format_exc() exc = traceback.format_exc()
if "text/html" in request.headers["accept"]: if (
request.headers
and "accept" in request.headers
and "text/html" in request.headers["accept"]
):
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": err} "error.html", {"request": request, "err": err}
) )

View file

@ -232,6 +232,9 @@ new Vue({
generateChart(this.$refs.canvas, this.payments) generateChart(this.$refs.canvas, this.payments)
}) })
}, },
focusInput(el) {
this.$nextTick(() => this.$refs[el].focus())
},
showReceiveDialog: function () { showReceiveDialog: function () {
this.receive.show = true this.receive.show = true
this.receive.status = 'pending' this.receive.status = 'pending'
@ -243,6 +246,7 @@ new Vue({
this.receive.paymentChecker = null this.receive.paymentChecker = null
this.receive.minMax = [0, 2100000000000000] this.receive.minMax = [0, 2100000000000000]
this.receive.lnurl = null this.receive.lnurl = null
this.focusInput('setAmount')
}, },
showParseDialog: function () { showParseDialog: function () {
this.parse.show = true this.parse.show = true
@ -668,7 +672,17 @@ new Vue({
}) })
}, },
exportCSV: function () { exportCSV: function () {
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments) // status is important for export but it is not in paymentsTable
// because it is manually added with payment detail link and icons
// and would cause duplication in the list
let columns = this.paymentsTable.columns
columns.unshift({
name: 'pending',
align: 'left',
label: 'Pending',
field: 'pending'
})
LNbits.utils.exportCSV(columns, this.payments)
} }
}, },
watch: { watch: {

View file

@ -428,6 +428,7 @@
:options="receive.units" :options="receive.units"
></q-select> ></q-select>
<q-input <q-input
ref="setAmount"
filled filled
dense dense
v-model.number="receive.data.amount" v-model.number="receive.data.amount"

View file

@ -1,7 +1,7 @@
import asyncio import asyncio
import binascii
import hashlib import hashlib
import json import json
from binascii import unhexlify
from http import HTTPStatus from http import HTTPStatus
from io import BytesIO from io import BytesIO
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
@ -152,11 +152,23 @@ class CreateInvoiceData(BaseModel):
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
if data.description_hash: if data.description_hash:
description_hash = unhexlify(data.description_hash) try:
description_hash = binascii.unhexlify(data.description_hash)
except binascii.Error:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'description_hash' must be a valid hex string",
)
unhashed_description = b"" unhashed_description = b""
memo = "" memo = ""
elif data.unhashed_description: elif data.unhashed_description:
unhashed_description = unhexlify(data.unhashed_description) try:
unhashed_description = binascii.unhexlify(data.unhashed_description)
except binascii.Error:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'unhashed_description' must be a valid hex string",
)
description_hash = b"" description_hash = b""
memo = "" memo = ""
else: else:

View file

@ -130,10 +130,13 @@ async def get_key_type(
# 2: invalid # 2: invalid
pathname = r["path"].split("/")[1] pathname = r["path"].split("/")[1]
if not api_key_header and not api_key_query: token = api_key_header or api_key_query
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
token = api_key_header if api_key_header else api_key_query if not token:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
)
try: try:
admin_checker = WalletAdminKeyChecker(api_key=token) admin_checker = WalletAdminKeyChecker(api_key=token)
@ -180,7 +183,14 @@ async def require_admin_key(
api_key_header: str = Security(api_key_header), # type: ignore api_key_header: str = Security(api_key_header), # type: ignore
api_key_query: str = Security(api_key_query), # type: ignore api_key_query: str = Security(api_key_query), # type: ignore
): ):
token = api_key_header if api_key_header else api_key_query
token = api_key_header or api_key_query
if not token:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Admin key required.",
)
wallet = await get_key_type(r, token) wallet = await get_key_type(r, token)
@ -199,11 +209,12 @@ async def require_invoice_key(
api_key_header: str = Security(api_key_header), # type: ignore api_key_header: str = Security(api_key_header), # type: ignore
api_key_query: str = Security(api_key_query), # type: ignore api_key_query: str = Security(api_key_query), # type: ignore
): ):
token = api_key_header or api_key_query token = api_key_header or api_key_query
if token is None: if not token:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=HTTPStatus.UNAUTHORIZED,
detail="Invoice (or Admin) key required.", detail="Invoice (or Admin) key required.",
) )

View file

@ -73,7 +73,7 @@ async def lnurl_callback(
wallet_id=cp.wallet, wallet_id=cp.wallet,
amount=int(amount_received / 1000), amount=int(amount_received / 1000),
memo=cp.lnurl_title, memo=cp.lnurl_title,
description_hash=( unhashed_description=(
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]])) LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
).encode("utf-8"), ).encode("utf-8"),
extra={"tag": "copilot", "copilotid": cp.id, "comment": comment}, extra={"tag": "copilot", "copilotid": cp.id, "comment": comment},

View file

@ -1,12 +1,11 @@
#invoicePage>.row:first-child>.col-xs { #invoicePage>.row:first-child>.col-md-6 {
display: flex; display: flex;
} }
#invoicePage>.row:first-child>.col-xs>.q-card { #invoicePage>.row:first-child>.col-md-6>.q-card {
flex: 1; flex: 1;
} }
#invoicePage .clear { #invoicePage .clear {
margin-bottom: 25px; margin-bottom: 25px;
} }
@ -15,6 +14,17 @@
display: none; display: none;
} }
@media (min-width: 1024px) {
#invoicePage>.row:first-child>.col-md-6:first-child>div {
margin-right: 5px;
}
#invoicePage>.row:first-child>.col-md-6:nth-child(2)>div {
margin-left: 5px;
}
}
@media print { @media print {
* { * {
color: black !important; color: black !important;

View file

@ -19,8 +19,8 @@
block page %} block page %}
<link rel="stylesheet" href="/invoices/static/css/pay.css" /> <link rel="stylesheet" href="/invoices/static/css/pay.css" />
<div id="invoicePage"> <div id="invoicePage">
<div class="row q-gutter-y-md q-gutter-md"> <div class="row q-gutter-y-md">
<div class="col-xs"> <div class="col-md-6 col-sm-12 col-xs-12">
<q-card> <q-card>
<q-card-section> <q-card-section>
<p> <p>
@ -88,7 +88,7 @@ block page %}
</q-card> </q-card>
</div> </div>
<div class="col-xs"> <div class="col-md-6 col-sm-12 col-xs-12">
<q-card> <q-card>
<q-card-section> <q-card-section>
<p> <p>
@ -131,7 +131,7 @@ block page %}
<div class="clear"></div> <div class="clear"></div>
<div class="row q-gutter-y-md q-gutter-md"> <div class="row">
<div class="col-12 col-md"> <div class="col-12 col-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
@ -162,7 +162,7 @@ block page %}
<div class="clear"></div> <div class="clear"></div>
<div class="row q-gutter-y-md q-gutter-md"> <div class="row">
<div class="col-12 col-md"> <div class="col-12 col-md">
<q-card> <q-card>
<q-card-section> <q-card-section>

View file

@ -90,7 +90,7 @@ async def lnurl_callback(
wallet_id=ls.wallet, wallet_id=ls.wallet,
amount=int(amount_received / 1000), amount=int(amount_received / 1000),
memo=await track.fullname(), memo=await track.fullname(),
description_hash=(await track.lnurlpay_metadata()).encode("utf-8"), unhashed_description=(await track.lnurlpay_metadata()).encode("utf-8"),
extra={"tag": "livestream", "track": track.id, "comment": comment}, extra={"tag": "livestream", "track": track.id, "comment": comment},
) )

View file

@ -13,7 +13,7 @@
Charge people for using your domain name...<br /> Charge people for using your domain name...<br />
<a <a
href="https://github.com/lnbits/lnbits/tree/master/lnbits/extensions/lnaddress" href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/lnaddress"
>More details</a >More details</a
> >
<br /> <br />

View file

@ -205,7 +205,7 @@ async def lnurl_callback(
wallet_id=device.wallet, wallet_id=device.wallet,
amount=lnurldevicepayment.sats / 1000, amount=lnurldevicepayment.sats / 1000,
memo=device.title, memo=device.title,
description_hash=(await device.lnurlpay_metadata()).encode("utf-8"), unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"),
extra={"tag": "PoS"}, extra={"tag": "PoS"},
) )
lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment = await update_lnurldevicepayment(

View file

@ -87,7 +87,7 @@ async def api_lnurl_callback(request: Request, link_id):
wallet_id=link.wallet, wallet_id=link.wallet,
amount=int(amount_received / 1000), amount=int(amount_received / 1000),
memo=link.description, memo=link.description,
description_hash=link.lnurlpay_metadata.encode("utf-8"), unhashed_description=link.lnurlpay_metadata.encode("utf-8"),
extra={ extra={
"tag": "lnurlp", "tag": "lnurlp",
"link": link.id, "link": link.id,

View file

@ -296,16 +296,17 @@
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="link"
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')" @click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
>Shareable link</q-btn ><q-tooltip>Copy sharable link</q-tooltip>
> </q-btn>
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="nfc" icon="nfc"
@click="writeNfcTag(qrCodeDialog.data.lnurl)" @click="writeNfcTag(qrCodeDialog.data.lnurl)"
:disable="nfcTagWriting" :disable="nfcTagWriting"
> ><q-tooltip>Write to NFC</q-tooltip>
</q-btn> </q-btn>
<q-btn <q-btn
outline outline
@ -314,7 +315,8 @@
type="a" type="a"
:href="qrCodeDialog.data.print_url" :href="qrCodeDialog.data.print_url"
target="_blank" target="_blank"
></q-btn> ><q-tooltip>Print</q-tooltip></q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-card>

View file

@ -96,7 +96,7 @@ async def api_link_create_or_update(
data.min *= data.fiat_base_multiplier data.min *= data.fiat_base_multiplier
data.max *= data.fiat_base_multiplier data.max *= data.fiat_base_multiplier
if data.success_url is not None and data.success_url.startswith("https://"): if data.success_url is not None and not data.success_url.startswith("https://"):
raise HTTPException( raise HTTPException(
detail="Success URL must be secure https://...", detail="Success URL must be secure https://...",
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
@ -121,7 +121,7 @@ async def api_link_create_or_update(
return {**link.dict(), "lnurl": link.lnurl(request)} return {**link.dict(), "lnurl": link.lnurl(request)}
@lnurlp_ext.delete("/api/v1/links/{link_id}") @lnurlp_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(get_key_type)):
link = await get_pay_link(link_id) link = await get_pay_link(link_id)
@ -136,7 +136,7 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(get_key_type
) )
await delete_pay_link(link_id) await delete_pay_link(link_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) return {"success": True}
@lnurlp_ext.get("/api/v1/rate/{currency}", status_code=HTTPStatus.OK) @lnurlp_ext.get("/api/v1/rate/{currency}", status_code=HTTPStatus.OK)

View file

@ -73,7 +73,7 @@ async def lnurl_callback(request: Request, item_id: int):
wallet_id=shop.wallet, wallet_id=shop.wallet,
amount=int(amount_received / 1000), amount=int(amount_received / 1000),
memo=item.name, memo=item.name,
description_hash=(await item.lnurlpay_metadata()).encode("utf-8"), unhashed_description=(await item.lnurlpay_metadata()).encode("utf-8"),
extra={"tag": "offlineshop", "item": item.id}, extra={"tag": "offlineshop", "item": item.id},
) )
except Exception as exc: except Exception as exc:

View file

@ -77,7 +77,7 @@ async def api_lnurlp_callback(
wallet_id=link.wallet, wallet_id=link.wallet,
amount=int(amount_received / 1000), amount=int(amount_received / 1000),
memo="Satsdice bet", memo="Satsdice bet",
description_hash=link.lnurlpay_metadata.encode("utf-8"), unhashed_description=link.lnurlpay_metadata.encode("utf-8"),
extra={"tag": "satsdice", "link": link.id, "comment": "comment"}, extra={"tag": "satsdice", "link": link.id, "comment": "comment"},
) )

View file

@ -232,7 +232,7 @@
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="share" icon="link"
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')" @click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
><q-tooltip>Copy shareable link</q-tooltip></q-btn ><q-tooltip>Copy shareable link</q-tooltip></q-btn
> >

View file

@ -13,7 +13,7 @@
Charge people for using your subdomain name...<br /> Charge people for using your subdomain name...<br />
<a <a
href="https://github.com/lnbits/lnbits/tree/master/lnbits/extensions/subdomains" href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/subdomains"
>More details</a >More details</a
> >
<br /> <br />

View file

@ -76,10 +76,10 @@ async def get_tipjars(wallet_id: str) -> Optional[list]:
async def delete_tipjar(tipjar_id: int) -> None: async def delete_tipjar(tipjar_id: int) -> None:
"""Delete a TipJar and all corresponding Tips""" """Delete a TipJar and all corresponding Tips"""
await db.execute("DELETE FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
rows = await db.fetchall("SELECT * FROM tipjar.Tips WHERE tipjar = ?", (tipjar_id,)) rows = await db.fetchall("SELECT * FROM tipjar.Tips WHERE tipjar = ?", (tipjar_id,))
for row in rows: for row in rows:
await delete_tip(row["id"]) await delete_tip(row["id"])
await db.execute("DELETE FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
async def get_tip(tip_id: str) -> Optional[Tip]: async def get_tip(tip_id: str) -> Optional[Tip]:

View file

@ -23,3 +23,7 @@ class TPoS(BaseModel):
@classmethod @classmethod
def from_row(cls, row: Row) -> "TPoS": def from_row(cls, row: Row) -> "TPoS":
return cls(**dict(row)) return cls(**dict(row))
class PayLnurlWData(BaseModel):
lnurl: str

View file

@ -14,7 +14,7 @@
<div class="row justify-center full-width"> <div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center"> <div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3> <h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none"> <h5 class="q-mt-none q-mb-sm">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small> {% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5> </h5>
</div> </div>
@ -174,8 +174,21 @@
> >
{% endraw %} {% endraw %}
</h5> </h5>
<q-btn
outline
color="grey"
icon="nfc"
@click="readNfcTag()"
:disable="nfcTagReading"
></q-btn>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(invoiceDialog.data.payment_request)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-card>
@ -281,6 +294,7 @@
exchangeRate: null, exchangeRate: null,
stack: [], stack: [],
tipAmount: 0.0, tipAmount: 0.0,
nfcTagReading: false,
invoiceDialog: { invoiceDialog: {
show: false, show: false,
data: null, data: null,
@ -356,7 +370,7 @@
this.showInvoice() this.showInvoice()
}, },
submitForm: function () { submitForm: function () {
if (this.tip_options) { if (this.tip_options.length) {
this.showTipModal() this.showTipModal()
} else { } else {
this.showInvoice() this.showInvoice()
@ -410,6 +424,98 @@
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
}, },
readNfcTag: function () {
try {
const self = this
if (typeof NDEFReader == 'undefined') {
throw {
toString: function () {
return 'NFC not supported on this device or browser.'
}
}
}
const ndef = new NDEFReader()
const readerAbortController = new AbortController()
readerAbortController.signal.onabort = event => {
console.log('All NFC Read operations have been aborted.')
}
this.nfcTagReading = true
this.$q.notify({
message: 'Tap your NFC tag to pay this invoice with LNURLw.'
})
return ndef.scan({signal: readerAbortController.signal}).then(() => {
ndef.onreadingerror = () => {
self.nfcTagReading = false
this.$q.notify({
type: 'negative',
message: 'There was an error reading this NFC tag.'
})
readerAbortController.abort()
}
ndef.onreading = ({message}) => {
//Decode NDEF data from tag
const textDecoder = new TextDecoder('utf-8')
const record = message.records.find(el => {
const payload = textDecoder.decode(el.data)
return payload.toUpperCase().indexOf('LNURL') !== -1
})
const lnurl = textDecoder.decode(record.data)
//User feedback, show loader icon
self.nfcTagReading = false
self.payInvoice(lnurl, readerAbortController)
this.$q.notify({
type: 'positive',
message: 'NFC tag read successfully.'
})
}
})
} catch (error) {
this.nfcTagReading = false
this.$q.notify({
type: 'negative',
message: error
? error.toString()
: 'An unexpected error has occurred.'
})
}
},
payInvoice: function (lnurl, readerAbortController) {
const self = this
return axios
.post(
'/tpos/api/v1/tposs/' +
self.tposId +
'/invoices/' +
self.invoiceDialog.data.payment_request +
'/pay',
{
lnurl: lnurl
}
)
.then(response => {
if (!response.data.success) {
this.$q.notify({
type: 'negative',
message: response.data.detail
})
}
readerAbortController.abort()
})
},
getRates: function () { getRates: function () {
var self = this var self = this
axios.get('https://api.opennode.co/v1/rates').then(function (response) { axios.get('https://api.opennode.co/v1/rates').then(function (response) {

View file

@ -1,7 +1,9 @@
from http import HTTPStatus from http import HTTPStatus
import httpx
from fastapi import Query from fastapi import Query
from fastapi.params import Depends from fastapi.params import Depends
from lnurl import decode as decode_lnurl
from loguru import logger from loguru import logger
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
@ -12,7 +14,7 @@ from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import tpos_ext from . import tpos_ext
from .crud import create_tpos, delete_tpos, get_tpos, get_tposs from .crud import create_tpos, delete_tpos, get_tpos, get_tposs
from .models import CreateTposData from .models import CreateTposData, PayLnurlWData
@tpos_ext.get("/api/v1/tposs", status_code=HTTPStatus.OK) @tpos_ext.get("/api/v1/tposs", status_code=HTTPStatus.OK)
@ -79,6 +81,66 @@ async def api_tpos_create_invoice(
return {"payment_hash": payment_hash, "payment_request": payment_request} return {"payment_hash": payment_hash, "payment_request": payment_request}
@tpos_ext.post(
"/api/v1/tposs/{tpos_id}/invoices/{payment_request}/pay", status_code=HTTPStatus.OK
)
async def api_tpos_pay_invoice(
lnurl_data: PayLnurlWData, payment_request: str = None, tpos_id: str = None
):
tpos = await get_tpos(tpos_id)
if not tpos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
)
lnurl = (
lnurl_data.lnurl.replace("lnurlw://", "")
.replace("lightning://", "")
.replace("LIGHTNING://", "")
.replace("lightning:", "")
.replace("LIGHTNING:", "")
)
if lnurl.lower().startswith("lnurl"):
lnurl = decode_lnurl(lnurl)
else:
lnurl = "https://" + lnurl
async with httpx.AsyncClient() as client:
try:
r = await client.get(lnurl, follow_redirects=True)
if r.is_error:
lnurl_response = {"success": False, "detail": "Error loading"}
else:
resp = r.json()
if resp["tag"] != "withdrawRequest":
lnurl_response = {"success": False, "detail": "Wrong tag type"}
else:
r2 = await client.get(
resp["callback"],
follow_redirects=True,
params={
"k1": resp["k1"],
"pr": payment_request,
},
)
resp2 = r2.json()
if r2.is_error:
lnurl_response = {
"success": False,
"detail": "Error loading callback",
}
elif resp2["status"] == "ERROR":
lnurl_response = {"success": False, "detail": resp2["reason"]}
else:
lnurl_response = {"success": True, "detail": resp2}
except (httpx.ConnectError, httpx.RequestError):
lnurl_response = {"success": False, "detail": "Unexpected error occurred"}
return lnurl_response
@tpos_ext.get( @tpos_ext.get(
"/api/v1/tposs/{tpos_id}/invoices/{payment_hash}", status_code=HTTPStatus.OK "/api/v1/tposs/{tpos_id}/invoices/{payment_hash}", status_code=HTTPStatus.OK
) )

View file

@ -39,6 +39,10 @@ async function history(path) {
label: 'Date', label: 'Date',
field: 'date', field: 'date',
sortable: true sortable: true
},
{
name: 'txId',
field: 'txId'
} }
], ],
exportColums: [ exportColums: [

View file

@ -33,7 +33,6 @@ async function payment(path) {
signedTxHex: null, signedTxHex: null,
sentTxId: null, sentTxId: null,
signedTxId: null, signedTxId: null,
paymentTab: 'destination',
sendToList: [{address: '', amount: undefined}], sendToList: [{address: '', amount: undefined}],
changeWallet: null, changeWallet: null,
changeAddress: {}, changeAddress: {},
@ -83,12 +82,27 @@ async function payment(path) {
satBtc(val, showUnit = true) { satBtc(val, showUnit = true) {
return satOrBtc(val, showUnit, this.satsDenominated) return satOrBtc(val, showUnit, this.satsDenominated)
}, },
clearState: function () {
this.psbtBase64 = null
this.psbtBase64Signed = null
this.signedTx = null
this.signedTxHex = null
this.signedTxId = null
this.sendToList = [{address: '', amount: undefined}]
this.showChecking = false
this.showPsbt = false
this.showFinalTx = false
},
checkAndSend: async function () { checkAndSend: async function () {
this.showChecking = true this.showChecking = true
try { try {
if (!this.serialSignerRef.isConnected()) { if (!this.serialSignerRef.isConnected()) {
const portOpen = await this.serialSignerRef.openSerialPort() this.$q.notify({
if (!portOpen) return type: 'warning',
message: 'Please connect to a Signing device first!',
timeout: 10000
})
return
} }
if (!this.serialSignerRef.isAuthenticated()) { if (!this.serialSignerRef.isAuthenticated()) {
await this.serialSignerRef.hwwShowPasswordDialog() await this.serialSignerRef.hwwShowPasswordDialog()
@ -139,7 +153,6 @@ async function payment(path) {
}, },
createPsbt: async function () { createPsbt: async function () {
try { try {
console.log('### this.createPsbt')
this.tx = this.createTx() this.tx = this.createTx()
for (const input of this.tx.inputs) { for (const input of this.tx.inputs) {
input.tx_hex = await this.fetchTxHex(input.tx_id) input.tx_hex = await this.fetchTxHex(input.tx_id)
@ -233,8 +246,6 @@ async function payment(path) {
this.showChecking = true this.showChecking = true
this.psbtBase64Signed = psbtBase64 this.psbtBase64Signed = psbtBase64
console.log('### payment updateSignedPsbt psbtBase64', psbtBase64)
const data = await this.extractTxFromPsbt(psbtBase64) const data = await this.extractTxFromPsbt(psbtBase64)
this.showFinalTx = true this.showFinalTx = true
if (data) { if (data) {
@ -249,7 +260,6 @@ async function payment(path) {
} }
}, },
extractTxFromPsbt: async function (psbtBase64) { extractTxFromPsbt: async function (psbtBase64) {
console.log('### extractTxFromPsbt psbtBase64', psbtBase64)
try { try {
const {data} = await LNbits.api.request( const {data} = await LNbits.api.request(
'PUT', 'PUT',
@ -260,13 +270,12 @@ async function payment(path) {
inputs: this.tx.inputs inputs: this.tx.inputs
} }
) )
console.log('### extractTxFromPsbt data', data)
return data return data
} catch (error) { } catch (error) {
console.log('### error', error)
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
message: 'Cannot finalize PSBT!', message: 'Cannot finalize PSBT!',
caption: `${error}`,
timeout: 10000 timeout: 10000
}) })
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
@ -289,8 +298,8 @@ async function payment(path) {
timeout: 10000 timeout: 10000
}) })
// todo: event rescan with amount this.clearState()
// todo: display tx id this.$emit('broadcast-done', this.sentTxId)
} catch (error) { } catch (error) {
this.sentTxId = null this.sentTxId = null
this.$q.notify({ this.$q.notify({

View file

@ -1,4 +1,15 @@
<div> <div>
<div class="row q-mt-md">
<div class="col-12">
<q-input
filled
dense
v-model.trim="config.name"
label="Name (optional)"
></q-input>
</div>
</div>
<q-separator class="q-mt-sm"></q-separator>
<div class="row q-mt-md"> <div class="row q-mt-md">
<div class="col-12"> <div class="col-12">
<q-input <q-input
@ -64,4 +75,26 @@
></q-input> ></q-input>
</div> </div>
</div> </div>
<q-separator class="q-mt-sm"></q-separator>
<div class="row q-mt-md">
<div class="col-12">
<q-input
filled
dense
v-model.trim="config.buttonOnePin"
label="Pin Number (Button 1)"
></q-input>
</div>
</div>
<div class="row q-mt-md">
<div class="col-12">
<q-input
filled
dense
v-model.trim="config.buttonTwoPin"
label="Pin Number (Button 2)"
></q-input>
</div>
</div>
</div> </div>

View file

@ -2,23 +2,11 @@ async function serialPortConfig(path) {
const t = await loadTemplateAsync(path) const t = await loadTemplateAsync(path)
Vue.component('serial-port-config', { Vue.component('serial-port-config', {
name: 'serial-port-config', name: 'serial-port-config',
props: ['config'],
template: t, template: t,
data() { data() {
return { return {}
config: {
baudRate: 9600,
bufferSize: 255,
dataBits: 8,
flowControl: 'none',
parity: 'none',
stopBits: 1
}
}
}, },
methods: { methods: {}
getConfig: function () {
return this.config
}
}
}) })
} }

View file

@ -46,6 +46,27 @@
> >
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item
v-for="device in pairedDevices"
:key="device.id"
v-if="!selectedPort"
clickable
v-close-popup
>
<q-item-section>
<q-item-label @click="openSerialPortConfig(device.id)"
>Paired Device ({{device.config.name || 'no-name'}})
</q-item-label>
<q-item-label caption @click="openSerialPortConfig(device.id)"
>{{device.id}}
</q-item-label>
<q-item-label caption @click="removePairedDevice(device.id)">
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Forget</q-btn
>
</q-item-label>
</q-item-section>
</q-item>
<q-item <q-item
v-if="selectedPort" v-if="selectedPort"
clickable clickable
@ -123,10 +144,9 @@
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="hwwConfigAndConnect" class="q-gutter-md"> <q-form @submit="hwwConfigAndConnect" class="q-gutter-md">
<span>Enter Config</span> <span>Enter Config</span>
<serial-port-config <serial-port-config
ref="serialPortConfig" ref="serialPortConfig"
:config="hww.config" :config="config"
></serial-port-config> ></serial-port-config>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
@ -223,7 +243,7 @@
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<div class="col-12"> <div class="col-12">
<q-badge class="text-subtitle2" color="yellow" text-color="black"> <q-badge class="text-subtitle2" color="yellow" text-color="black">
<span>Check data on the display of the hardware device.</span> <span>Confirm then check the Hardware Device.</span>
</q-badge> </q-badge>
</div> </div>
</div> </div>
@ -329,6 +349,25 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="showConsole" position="top">
<q-card class="q-pa-lg q-pt-xl">
<q-input
filled
dense
for="serial-port-console"
v-model.trim="receivedData"
type="textarea"
rows="25"
cols="200"
label="Console"
></q-input>
<div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="hww.showSeedDialog" position="top"> <q-dialog v-model="hww.showSeedDialog" position="top">
<q-card class="q-pa-lg q-pt-xl"> <q-card class="q-pa-lg q-pt-xl">
<span>Check word at position {{hww.seedWordPosition}} on display</span> <span>Check word at position {{hww.seedWordPosition}} on display</span>
@ -389,6 +428,31 @@
</template> </template>
</q-input> </q-input>
<br /> <br />
<q-toggle
label="Passphrase (optional)"
color="secodary"
v-model="hww.hasPassphrase"
></q-toggle>
<br />
<q-input
v-if="hww.hasPassphrase"
v-model.trim="hww.passphrase"
filled
:type="hww.showPassphrase ? 'text' : 'password'"
filled
dense
label="Passphrase"
>
<template v-slot:append>
<q-icon
:name="hww.showPassphrase ? 'visibility' : 'visibility_off'"
class="cursor-pointer"
@click="hww.showPassphrase = !hww.showPassphrase"
/>
</template>
</q-input>
<q-separator></q-separator>
<br />
<span>Enter new password (8 numbers/letters)</span> <span>Enter new password (8 numbers/letters)</span>
<q-input <q-input
v-model.trim="hww.password" v-model.trim="hww.password"
@ -414,15 +478,7 @@
type="password" type="password"
label="Confirm Password" label="Confirm Password"
></q-input> ></q-input>
<br /><br /> <br />
<q-badge
color="pink"
text-color="black"
class="text-subtitle2"
multi-line
>
For test purposes only. Do not enter word list with real funds!!!
</q-badge>
<q-separator></q-separator> <q-separator></q-separator>
<q-badge <q-badge
color="pink" color="pink"

View file

@ -14,12 +14,17 @@ async function serialSigner(path) {
reader: null, reader: null,
receivedData: '', receivedData: '',
config: {}, config: {},
decryptionKey: null,
sharedSecret: null, // todo: store in secure local storage
hww: { hww: {
password: null, password: null,
showPassword: false, showPassword: false,
mnemonic: null, mnemonic: null,
showMnemonic: false, showMnemonic: false,
passphrase: null,
showPassphrase: false,
hasPassphrase: false,
authenticated: false, authenticated: false,
showPasswordDialog: false, showPasswordDialog: false,
showConfigDialog: false, showConfigDialog: false,
@ -34,6 +39,8 @@ async function serialSigner(path) {
xpubResolve: null, xpubResolve: null,
seedWordPosition: 1, seedWordPosition: 1,
showSeedDialog: false, showSeedDialog: false,
// config: null,
confirm: { confirm: {
outputIndex: 0, outputIndex: 0,
showFee: false showFee: false
@ -45,12 +52,30 @@ async function serialSigner(path) {
} }
}, },
computed: {
pairedDevices: {
get: function () {
return (
JSON.parse(window.localStorage.getItem('lnbits-paired-devices')) ||
[]
)
},
set: function (devices) {
window.localStorage.setItem(
'lnbits-paired-devices',
JSON.stringify(devices)
)
}
}
},
methods: { methods: {
satBtc(val, showUnit = true) { satBtc(val, showUnit = true) {
return satOrBtc(val, showUnit, this.satsDenominated) return satOrBtc(val, showUnit, this.satsDenominated)
}, },
openSerialPortDialog: async function () { openSerialPortDialog: async function () {
await this.openSerialPort() this.config = {...HWW_DEFAULT_CONFIG}
await this.openSerialPort(this.config)
}, },
openSerialPort: async function (config = {baudRate: 9600}) { openSerialPort: async function (config = {baudRate: 9600}) {
if (!this.checkSerialPortSupported()) return false if (!this.checkSerialPortSupported()) return false
@ -64,12 +89,12 @@ async function serialSigner(path) {
} }
try { try {
navigator.serial.addEventListener('connect', event => { this.selectedPort = await navigator.serial.requestPort()
console.log('### navigator.serial event: connected!', event) this.selectedPort.addEventListener('connect', event => {
// do nothing
}) })
navigator.serial.addEventListener('disconnect', () => { this.selectedPort.addEventListener('disconnect', () => {
console.log('### navigator.serial event: disconnected!')
this.selectedPort = null this.selectedPort = null
this.hww.authenticated = false this.hww.authenticated = false
this.$q.notify({ this.$q.notify({
@ -78,7 +103,7 @@ async function serialSigner(path) {
timeout: 10000 timeout: 10000
}) })
}) })
this.selectedPort = await navigator.serial.requestPort()
// Wait for the serial port to open. // Wait for the serial port to open.
await this.selectedPort.open(config) await this.selectedPort.open(config)
this.startSerialPortReading() this.startSerialPortReading()
@ -89,6 +114,9 @@ async function serialSigner(path) {
) )
this.writer = textEncoder.writable.getWriter() this.writer = textEncoder.writable.getWriter()
await this.hwwPing()
return true return true
} catch (error) { } catch (error) {
this.selectedPort = null this.selectedPort = null
@ -101,7 +129,13 @@ async function serialSigner(path) {
return false return false
} }
}, },
openSerialPortConfig: async function () { openSerialPortConfig: async function (deviceId) {
const device = this.getPairedDevice(deviceId)
if (device) {
this.config = device.config
} else {
this.config = {...HWW_DEFAULT_CONFIG}
}
this.hww.showConfigDialog = true this.hww.showConfigDialog = true
}, },
closeSerialPort: async function () { closeSerialPort: async function () {
@ -114,7 +148,6 @@ async function serialSigner(path) {
/* Ignore the error */ /* Ignore the error */
}) })
if (this.selectedPort) await this.selectedPort.close() if (this.selectedPort) await this.selectedPort.close()
this.$q.notify({ this.$q.notify({
type: 'positive', type: 'positive',
message: 'Serial port disconnected!', message: 'Serial port disconnected!',
@ -200,11 +233,17 @@ async function serialSigner(path) {
} }
} }
}, },
handleSerialPortResponse: function (value) { handleSerialPortResponse: async function (value) {
const command = value.split(' ')[0] const {command, commandData} = await this.extractCommand(value)
const commandData = value.substring(command.length).trim() this.logPublicCommandsResponse(command, commandData)
switch (command) { switch (command) {
case COMMAND_PING:
this.handlePingResponse(commandData)
break
case COMMAND_CHECK_PAIRING:
this.handleCheckPairingResponse(commandData)
break
case COMMAND_SIGN_PSBT: case COMMAND_SIGN_PSBT:
this.handleSignResponse(commandData) this.handleSignResponse(commandData)
break break
@ -223,8 +262,35 @@ async function serialSigner(path) {
case COMMAND_XPUB: case COMMAND_XPUB:
this.handleXpubResponse(commandData) this.handleXpubResponse(commandData)
break break
case COMMAND_SEED:
this.handleShowSeedResponse(commandData)
break
case COMMAND_PAIR:
this.handlePairResponse(commandData)
break
case COMMAND_LOG:
console.log(
` %c${commandData}`,
'background: #222; color: #bada55'
)
break
default: default:
console.log('### console', value) console.log(` %c${value}`, 'background: #222; color: red')
}
},
logPublicCommandsResponse: function (command, commandData) {
switch (command) {
case COMMAND_SIGN_PSBT:
case COMMAND_PASSWORD:
case COMMAND_PASSWORD_CLEAR:
case COMMAND_SEND_PSBT:
case COMMAND_WIPE:
case COMMAND_XPUB:
case COMMAND_PAIR:
console.log(
` %c${command} ${commandData}`,
'background: #222; color: yellow'
)
} }
}, },
updateSerialPortConsole: function (value) { updateSerialPortConsole: function (value) {
@ -232,11 +298,48 @@ async function serialSigner(path) {
const textArea = document.getElementById('serial-port-console') const textArea = document.getElementById('serial-port-console')
if (textArea) textArea.scrollTop = textArea.scrollHeight if (textArea) textArea.scrollTop = textArea.scrollHeight
}, },
hwwPing: async function () {
try {
await this.sendCommandClearText(COMMAND_PING, [window.location.host])
} catch (error) {
this.$q.notify({
type: 'warning',
message: 'Failed to ping Hardware Wallet!',
caption: `${error}`,
timeout: 10000
})
}
},
handlePingResponse: function (res = '') {
const [status, deviceId] = res.split(' ')
this.deviceId = deviceId
if (!this.deviceId) {
this.$q.notify({
type: 'warning',
message: 'Missing device ID for Hardware Wallet',
timeout: 10000
})
return
}
const device = this.getPairedDevice(deviceId)
if (device) {
this.sharedSecret = nobleSecp256k1.utils.hexToBytes(
device.sharedSecretHex
)
this.hwwCheckPairing()
} else {
this.hwwPair()
}
},
hwwShowPasswordDialog: async function () { hwwShowPasswordDialog: async function () {
try { try {
this.hww.showPasswordDialog = true this.hww.showPasswordDialog = true
await this.writer.write(COMMAND_PASSWORD + '\n') await this.sendCommandSecure(COMMAND_PASSWORD)
} catch (error) { } catch (error) {
console.log(error)
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
message: 'Failed to connect to Hardware Wallet!', message: 'Failed to connect to Hardware Wallet!',
@ -248,7 +351,7 @@ async function serialSigner(path) {
hwwShowWipeDialog: async function () { hwwShowWipeDialog: async function () {
try { try {
this.hww.showWipeDialog = true this.hww.showWipeDialog = true
await this.writer.write(COMMAND_WIPE + '\n') await this.sendCommandSecure(COMMAND_WIPE)
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -261,7 +364,7 @@ async function serialSigner(path) {
hwwShowRestoreDialog: async function () { hwwShowRestoreDialog: async function () {
try { try {
this.hww.showRestoreDialog = true this.hww.showRestoreDialog = true
await this.writer.write(COMMAND_WIPE + '\n') await this.sendCommandSecure(COMMAND_RESTORE)
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -276,11 +379,11 @@ async function serialSigner(path) {
if (this.hww.confirm.outputIndex >= this.tx.outputs.length) { if (this.hww.confirm.outputIndex >= this.tx.outputs.length) {
this.hww.confirm.showFee = true this.hww.confirm.showFee = true
} }
await this.writer.write(COMMAND_CONFIRM_NEXT + '\n') await this.sendCommandSecure(COMMAND_CONFIRM_NEXT)
}, },
cancelOperation: async function () { cancelOperation: async function () {
try { try {
await this.writer.write(COMMAND_CANCEL + '\n') await this.sendCommandSecure(COMMAND_CANCEL)
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -292,15 +395,15 @@ async function serialSigner(path) {
}, },
hwwConfigAndConnect: async function () { hwwConfigAndConnect: async function () {
this.hww.showConfigDialog = false this.hww.showConfigDialog = false
const config = this.$refs.serialPortConfig.getConfig() if (this.config.deviceId) {
await this.openSerialPort(config) this.updatePairedDeviceConfig(this.config.deviceId, this.config)
}
await this.openSerialPort(this.config)
return true return true
}, },
hwwLogin: async function () { hwwLogin: async function () {
try { try {
await this.writer.write( await this.sendCommandSecure(COMMAND_PASSWORD, [this.hww.password])
COMMAND_PASSWORD + ' ' + this.hww.password + '\n'
)
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -336,7 +439,7 @@ async function serialSigner(path) {
}, },
hwwLogout: async function () { hwwLogout: async function () {
try { try {
await this.writer.write(COMMAND_PASSWORD_CLEAR + '\n') await this.sendCommandSecure(COMMAND_PASSWORD_CLEAR)
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -347,22 +450,24 @@ async function serialSigner(path) {
} }
}, },
handleLogoutResponse: function (res = '') { handleLogoutResponse: function (res = '') {
this.hww.authenticated = !(res.trim() === '1') const authenticated = !(res.trim() === '1')
if (this.hww.authenticated) { if (this.hww.authenticated && !authenticated) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'positive',
message: 'Failed to logout from Hardware Wallet', message: 'Logged Out',
timeout: 10000 timeout: 10000
}) })
} }
this.hww.authenticated = authenticated
}, },
hwwSendPsbt: async function (psbtBase64, tx) { hwwSendPsbt: async function (psbtBase64, tx) {
try { try {
this.tx = tx this.tx = tx
this.hww.sendingPsbt = true this.hww.sendingPsbt = true
await this.writer.write( await this.sendCommandSecure(COMMAND_SEND_PSBT, [
COMMAND_SEND_PSBT + ' ' + this.network + ' ' + psbtBase64 + '\n' this.network,
) psbtBase64
])
this.$q.notify({ this.$q.notify({
type: 'positive', type: 'positive',
message: 'Data sent to serial port device!', message: 'Data sent to serial port device!',
@ -412,7 +517,7 @@ async function serialSigner(path) {
try { try {
this.hww.showConfirmationDialog = false this.hww.showConfirmationDialog = false
this.hww.signingPsbt = true this.hww.signingPsbt = true
await this.writer.write(COMMAND_SIGN_PSBT + '\n') await this.sendCommandSecure(COMMAND_SIGN_PSBT)
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -442,9 +547,152 @@ async function serialSigner(path) {
timeout: 10000 timeout: 10000
}) })
}, },
hwwCheckPairing: async function () {
const iv = window.crypto.getRandomValues(new Uint8Array(16))
const encrypted = await this.encryptMessage(
this.sharedSecret,
iv,
PAIRING_CONTROL_TEXT.length + ' ' + PAIRING_CONTROL_TEXT
)
const encryptedHex = nobleSecp256k1.utils.bytesToHex(encrypted)
const encryptedIvHex = nobleSecp256k1.utils.bytesToHex(iv)
try {
await this.sendCommandClearText(COMMAND_CHECK_PAIRING, [
encryptedHex + encryptedIvHex
])
} catch (error) {
this.$q.notify({
type: 'warning',
message: 'Failed to check secure connection!',
caption: `${error}`,
timeout: 10000
})
}
},
handleCheckPairingResponse: async function (res = '') {
const [statusCode, encryptedMessage] = res.split(' ')
switch (statusCode) {
case '0':
const controlText = await this.decryptData(encryptedMessage)
if (controlText == PAIRING_CONTROL_TEXT) {
this.$q.notify({
type: 'positive',
message: 'Re-paired with success!',
timeout: 10000
})
} else {
this.$q.notify({
type: 'warning',
message: 'Re-pairing failed!',
caption: 'Remove (forget) device and try again!',
timeout: 10000
})
}
break
default:
// noting to do here yet
break
}
},
hwwPair: async function () {
try {
this.decryptionKey = nobleSecp256k1.utils.randomPrivateKey()
const publicKey = nobleSecp256k1.Point.fromPrivateKey(
this.decryptionKey
)
const publicKeyHex = publicKey.toHex().slice(2)
const args = [publicKeyHex]
if (Number.isInteger(+this.config.buttonOnePin)) {
args.push(this.config.buttonOnePin)
}
if (Number.isInteger(+this.config.buttonTwoPin)) {
args.push(this.config.buttonTwoPin)
}
await this.sendCommandClearText(COMMAND_PAIR, args)
this.$q.notify({
type: 'positive',
message: 'Pairing started!',
timeout: 5000
})
} catch (error) {
this.$q.notify({
type: 'warning',
message: 'Failed to pair with device!',
caption: `${error}`,
timeout: 10000
})
}
},
handlePairResponse: async function (res = '') {
const [statusCode, data] = res.trim().split(' ')
let pubKeyHex, errorMessage, captionMessage
switch (statusCode) {
case '0':
pubKeyHex = data
if (!data) errorMessage = 'Failed to exchange DH secret!'
break
case '1':
errorMessage =
'Device pairing only possible in the first 10 seconds after start-up!'
captionMessage = 'Restart and try again'
break
default:
errorMessage = 'Unexpected error code'
break
}
if (errorMessage) {
this.$q.notify({
type: 'warning',
message: errorMessage,
caption: captionMessage || '',
timeout: 10000
})
this.closeSerialPort()
return
}
const hwwPublicKey = nobleSecp256k1.Point.fromHex('04' + pubKeyHex)
this.sharedSecret = nobleSecp256k1
.getSharedSecret(this.decryptionKey, hwwPublicKey)
.slice(1, 33)
const sharedSecretHex = nobleSecp256k1.utils.bytesToHex(
this.sharedSecret
)
const sharedSecredHash = await nobleSecp256k1.utils.sha256(
asciiToUint8Array(sharedSecretHex)
)
const fingerprint = nobleSecp256k1.utils
.bytesToHex(sharedSecredHash)
.substring(0, 5)
.toUpperCase()
LNbits.utils
.confirmDialog('Confirm code from display: ' + fingerprint)
.onOk(() => {
this.addPairedDevice(
this.deviceId,
nobleSecp256k1.utils.bytesToHex(this.sharedSecret),
this.config
)
this.$q.notify({
type: 'positive',
message: 'Paired with device!',
timeout: 5000
})
})
.onCancel(() => {
this.closeSerialPort()
})
},
hwwHelp: async function () { hwwHelp: async function () {
try { try {
await this.writer.write(COMMAND_HELP + '\n') await this.sendCommandSecure(COMMAND_HELP)
this.$q.notify({ this.$q.notify({
type: 'positive', type: 'positive',
message: 'Check display or console for details!', message: 'Check display or console for details!',
@ -462,7 +710,7 @@ async function serialSigner(path) {
hwwWipe: async function () { hwwWipe: async function () {
try { try {
this.hww.showWipeDialog = false this.hww.showWipeDialog = false
await this.writer.write(COMMAND_WIPE + ' ' + this.hww.password + '\n') await this.sendCommandSecure(COMMAND_WIPE, [this.hww.password])
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -495,13 +743,7 @@ async function serialSigner(path) {
}, },
hwwXpub: async function (path) { hwwXpub: async function (path) {
try { try {
console.log( await this.sendCommandSecure(COMMAND_XPUB, [this.network, path])
'### hwwXpub',
COMMAND_XPUB + ' ' + this.network + ' ' + path
)
await this.writer.write(
COMMAND_XPUB + ' ' + this.network + ' ' + path + '\n'
)
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -527,13 +769,15 @@ async function serialSigner(path) {
const fingerprint = args[2].trim() const fingerprint = args[2].trim()
this.xpubResolve({xpub, fingerprint}) this.xpubResolve({xpub, fingerprint})
}, },
hwwShowSeed: async function () { hwwShowSeed: async function () {
try { try {
this.hww.showSeedDialog = true this.hww.showSeedDialog = true
this.hww.seedWordPosition = 1 this.hww.seedWordPosition = 1
await this.writer.write(
COMMAND_SEED + ' ' + this.hww.seedWordPosition + '\n' await this.sendCommandSecure(COMMAND_SEED, [
) this.hww.seedWordPosition
])
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -545,37 +789,29 @@ async function serialSigner(path) {
}, },
showNextSeedWord: async function () { showNextSeedWord: async function () {
this.hww.seedWordPosition++ this.hww.seedWordPosition++
await this.writer.write( await this.sendCommandSecure(COMMAND_SEED, [this.hww.seedWordPosition])
COMMAND_SEED + ' ' + this.hww.seedWordPosition + '\n'
)
}, },
showPrevSeedWord: async function () { showPrevSeedWord: async function () {
this.hww.seedWordPosition = Math.max(1, this.hww.seedWordPosition - 1) this.hww.seedWordPosition = Math.max(1, this.hww.seedWordPosition - 1)
console.log('### this.hww.seedWordPosition', this.hww.seedWordPosition) await this.sendCommandSecure(COMMAND_SEED, [this.hww.seedWordPosition])
await this.writer.write(
COMMAND_SEED + ' ' + this.hww.seedWordPosition + '\n'
)
}, },
handleShowSeedResponse: function (res = '') { handleShowSeedResponse: function (res = '') {
const args = res.trim().split(' ') const args = res.trim().split(' ')
if (args.length < 2 || args[0].trim() !== '1') {
this.$q.notify({
type: 'warning',
message: 'Failed to show seed!',
caption: `${res}`,
timeout: 10000
})
return
}
}, },
hwwRestore: async function () { hwwRestore: async function () {
try { try {
await this.writer.write( let mnemonicWithPassphrase = this.hww.mnemonic
COMMAND_RESTORE + ' ' + this.hww.mnemonic + '\n' if (
) this.hww.hasPassphrase &&
await this.writer.write( this.hww.passphrase &&
COMMAND_PASSWORD + ' ' + this.hww.password + '\n' this.hww.passphrase.length
) ) {
mnemonicWithPassphrase += '/' + this.hww.passphrase
}
await this.sendCommandSecure(COMMAND_RESTORE, [
this.hww.password,
mnemonicWithPassphrase
])
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -586,6 +822,7 @@ async function serialSigner(path) {
} finally { } finally {
this.hww.showRestoreDialog = false this.hww.showRestoreDialog = false
this.hww.mnemonic = null this.hww.mnemonic = null
this.hww.passphrase = null
this.hww.showMnemonic = false this.hww.showMnemonic = false
this.hww.password = null this.hww.password = null
this.hww.confirmedPassword = null this.hww.confirmedPassword = null
@ -595,6 +832,116 @@ async function serialSigner(path) {
updateSignedPsbt: async function (value) { updateSignedPsbt: async function (value) {
this.$emit('signed:psbt', value) this.$emit('signed:psbt', value)
},
sendCommandSecure: async function (command, attrs = []) {
const message = [command].concat(attrs).join(' ')
const iv = window.crypto.getRandomValues(new Uint8Array(16))
const encrypted = await this.encryptMessage(
this.sharedSecret,
iv,
message.length + ' ' + message
)
const encryptedHex = nobleSecp256k1.utils.bytesToHex(encrypted)
const encryptedIvHex = nobleSecp256k1.utils.bytesToHex(iv)
await this.writer.write(encryptedHex + encryptedIvHex + '\n')
},
sendCommandClearText: async function (command, attrs = []) {
const message = [command].concat(attrs).join(' ')
await this.writer.write(message + '\n')
},
extractCommand: async function (value) {
const command = value.split(' ')[0]
const commandData = value.substring(command.length).trim()
if (
command === COMMAND_PAIR ||
command === COMMAND_LOG ||
command === COMMAND_PASSWORD_CLEAR ||
command === COMMAND_PING ||
command === COMMAND_CHECK_PAIRING
)
return {command, commandData}
const decryptedValue = await this.decryptData(value)
const decryptedCommand = decryptedValue.split(' ')[0]
const decryptedCommandData = decryptedValue
.substring(decryptedCommand.length)
.trim()
return {
command: decryptedCommand,
commandData: decryptedCommandData
}
},
decryptData: async function (value) {
if (!this.sharedSecret) {
return '/error Secure session not established!'
}
try {
const ivSize = 32
const messageHex = value.substring(0, value.length - ivSize)
const ivHex = value.substring(value.length - ivSize)
const messageBytes = nobleSecp256k1.utils.hexToBytes(messageHex)
const iv = nobleSecp256k1.utils.hexToBytes(ivHex)
const decrypted1 = await this.decryptMessage(
this.sharedSecret,
iv,
messageBytes
)
const data = new TextDecoder().decode(decrypted1)
const [len] = data.split(' ')
const command = data
.substring(len.length + 1, +len + len.length + 1)
.trim()
return command
} catch (error) {
return '/error Failed to decrypt message from device!'
}
},
encryptMessage: async function (key, iv, message) {
while (message.length % 16 !== 0) message += ' '
const encodedMessage = asciiToUint8Array(message)
const aesCbc = new aesjs.ModeOfOperation.cbc(key, iv)
const encryptedBytes = aesCbc.encrypt(encodedMessage)
return encryptedBytes
},
decryptMessage: async function (key, iv, encryptedBytes) {
const aesCbc = new aesjs.ModeOfOperation.cbc(key, iv)
const decryptedBytes = aesCbc.decrypt(encryptedBytes)
return decryptedBytes
},
getPairedDevice: function (deviceId) {
return this.pairedDevices.find(d => d.id === deviceId)
},
removePairedDevice: function (deviceId) {
const devices = this.pairedDevices
const deviceIndex = devices.findIndex(d => d.id === deviceId)
if (deviceIndex !== -1) {
devices.splice(deviceIndex, 1)
}
this.pairedDevices = devices
},
addPairedDevice: function (deviceId, sharedSecretHex, config) {
const devices = this.pairedDevices
config.deviceId = deviceId
devices.unshift({
id: deviceId,
sharedSecretHex: sharedSecretHex,
pairingDate: new Date().toISOString(),
config
})
this.pairedDevices = devices
},
updatePairedDeviceConfig(deviceId, config) {
const device = this.getPairedDevice(deviceId)
if (device) {
this.removePairedDevice(deviceId)
this.addPairedDevice(deviceId, device.sharedSecretHex, config)
}
} }
}, },
created: async function () {} created: async function () {}

View file

@ -99,6 +99,7 @@ async function walletList(path) {
async network(newNet, oldNet) { async network(newNet, oldNet) {
if (newNet !== oldNet) { if (newNet !== oldNet) {
await this.refreshWalletAccounts() await this.refreshWalletAccounts()
this.handleAddressTypeChanged(this.addressTypeOptions[1])
} }
} }
}, },
@ -255,8 +256,12 @@ async function walletList(path) {
getXpubFromDevice: async function () { getXpubFromDevice: async function () {
try { try {
if (!this.serialSignerRef.isConnected()) { if (!this.serialSignerRef.isConnected()) {
const portOpen = await this.serialSignerRef.openSerialPort() this.$q.notify({
if (!portOpen) return type: 'warning',
message: 'Please connect to a hardware Device first!',
timeout: 10000
})
return
} }
if (!this.serialSignerRef.isAuthenticated()) { if (!this.serialSignerRef.isAuthenticated()) {
await this.serialSignerRef.hwwShowPasswordDialog() await this.serialSignerRef.hwwShowPasswordDialog()

View file

@ -0,0 +1,802 @@
/*! MIT License. Copyright 2015-2018 Richard Moore <me@ricmoo.com>. See LICENSE.txt. */
(function(root) {
"use strict";
function checkInt(value) {
return (parseInt(value) === value);
}
function checkInts(arrayish) {
if (!checkInt(arrayish.length)) { return false; }
for (var i = 0; i < arrayish.length; i++) {
if (!checkInt(arrayish[i]) || arrayish[i] < 0 || arrayish[i] > 255) {
return false;
}
}
return true;
}
function coerceArray(arg, copy) {
// ArrayBuffer view
if (arg.buffer && arg.name === 'Uint8Array') {
if (copy) {
if (arg.slice) {
arg = arg.slice();
} else {
arg = Array.prototype.slice.call(arg);
}
}
return arg;
}
// It's an array; check it is a valid representation of a byte
if (Array.isArray(arg)) {
if (!checkInts(arg)) {
throw new Error('Array contains invalid value: ' + arg);
}
return new Uint8Array(arg);
}
// Something else, but behaves like an array (maybe a Buffer? Arguments?)
if (checkInt(arg.length) && checkInts(arg)) {
return new Uint8Array(arg);
}
throw new Error('unsupported array-like object');
}
function createArray(length) {
return new Uint8Array(length);
}
function copyArray(sourceArray, targetArray, targetStart, sourceStart, sourceEnd) {
if (sourceStart != null || sourceEnd != null) {
if (sourceArray.slice) {
sourceArray = sourceArray.slice(sourceStart, sourceEnd);
} else {
sourceArray = Array.prototype.slice.call(sourceArray, sourceStart, sourceEnd);
}
}
targetArray.set(sourceArray, targetStart);
}
var convertUtf8 = (function() {
function toBytes(text) {
var result = [], i = 0;
text = encodeURI(text);
while (i < text.length) {
var c = text.charCodeAt(i++);
// if it is a % sign, encode the following 2 bytes as a hex value
if (c === 37) {
result.push(parseInt(text.substr(i, 2), 16))
i += 2;
// otherwise, just the actual byte
} else {
result.push(c)
}
}
return coerceArray(result);
}
function fromBytes(bytes) {
var result = [], i = 0;
while (i < bytes.length) {
var c = bytes[i];
if (c < 128) {
result.push(String.fromCharCode(c));
i++;
} else if (c > 191 && c < 224) {
result.push(String.fromCharCode(((c & 0x1f) << 6) | (bytes[i + 1] & 0x3f)));
i += 2;
} else {
result.push(String.fromCharCode(((c & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f)));
i += 3;
}
}
return result.join('');
}
return {
toBytes: toBytes,
fromBytes: fromBytes,
}
})();
var convertHex = (function() {
function toBytes(text) {
var result = [];
for (var i = 0; i < text.length; i += 2) {
result.push(parseInt(text.substr(i, 2), 16));
}
return result;
}
// http://ixti.net/development/javascript/2011/11/11/base64-encodedecode-of-utf8-in-browser-with-js.html
var Hex = '0123456789abcdef';
function fromBytes(bytes) {
var result = [];
for (var i = 0; i < bytes.length; i++) {
var v = bytes[i];
result.push(Hex[(v & 0xf0) >> 4] + Hex[v & 0x0f]);
}
return result.join('');
}
return {
toBytes: toBytes,
fromBytes: fromBytes,
}
})();
// Number of rounds by keysize
var numberOfRounds = {16: 10, 24: 12, 32: 14}
// Round constant words
var rcon = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91];
// S-box and Inverse S-box (S is for Substitution)
var S = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16];
var Si =[0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d];
// Transformations for encryption
var T1 = [0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a];
var T2 = [0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616];
var T3 = [0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16];
var T4 = [0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c];
// Transformations for decryption
var T5 = [0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742];
var T6 = [0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857];
var T7 = [0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8];
var T8 = [0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0];
// Transformations for decryption key expansion
var U1 = [0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3];
var U2 = [0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697];
var U3 = [0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46];
var U4 = [0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d];
function convertToInt32(bytes) {
var result = [];
for (var i = 0; i < bytes.length; i += 4) {
result.push(
(bytes[i ] << 24) |
(bytes[i + 1] << 16) |
(bytes[i + 2] << 8) |
bytes[i + 3]
);
}
return result;
}
var AES = function(key) {
if (!(this instanceof AES)) {
throw Error('AES must be instanitated with `new`');
}
Object.defineProperty(this, 'key', {
value: coerceArray(key, true)
});
this._prepare();
}
AES.prototype._prepare = function() {
var rounds = numberOfRounds[this.key.length];
if (rounds == null) {
throw new Error('invalid key size (must be 16, 24 or 32 bytes)');
}
// encryption round keys
this._Ke = [];
// decryption round keys
this._Kd = [];
for (var i = 0; i <= rounds; i++) {
this._Ke.push([0, 0, 0, 0]);
this._Kd.push([0, 0, 0, 0]);
}
var roundKeyCount = (rounds + 1) * 4;
var KC = this.key.length / 4;
// convert the key into ints
var tk = convertToInt32(this.key);
// copy values into round key arrays
var index;
for (var i = 0; i < KC; i++) {
index = i >> 2;
this._Ke[index][i % 4] = tk[i];
this._Kd[rounds - index][i % 4] = tk[i];
}
// key expansion (fips-197 section 5.2)
var rconpointer = 0;
var t = KC, tt;
while (t < roundKeyCount) {
tt = tk[KC - 1];
tk[0] ^= ((S[(tt >> 16) & 0xFF] << 24) ^
(S[(tt >> 8) & 0xFF] << 16) ^
(S[ tt & 0xFF] << 8) ^
S[(tt >> 24) & 0xFF] ^
(rcon[rconpointer] << 24));
rconpointer += 1;
// key expansion (for non-256 bit)
if (KC != 8) {
for (var i = 1; i < KC; i++) {
tk[i] ^= tk[i - 1];
}
// key expansion for 256-bit keys is "slightly different" (fips-197)
} else {
for (var i = 1; i < (KC / 2); i++) {
tk[i] ^= tk[i - 1];
}
tt = tk[(KC / 2) - 1];
tk[KC / 2] ^= (S[ tt & 0xFF] ^
(S[(tt >> 8) & 0xFF] << 8) ^
(S[(tt >> 16) & 0xFF] << 16) ^
(S[(tt >> 24) & 0xFF] << 24));
for (var i = (KC / 2) + 1; i < KC; i++) {
tk[i] ^= tk[i - 1];
}
}
// copy values into round key arrays
var i = 0, r, c;
while (i < KC && t < roundKeyCount) {
r = t >> 2;
c = t % 4;
this._Ke[r][c] = tk[i];
this._Kd[rounds - r][c] = tk[i++];
t++;
}
}
// inverse-cipher-ify the decryption round key (fips-197 section 5.3)
for (var r = 1; r < rounds; r++) {
for (var c = 0; c < 4; c++) {
tt = this._Kd[r][c];
this._Kd[r][c] = (U1[(tt >> 24) & 0xFF] ^
U2[(tt >> 16) & 0xFF] ^
U3[(tt >> 8) & 0xFF] ^
U4[ tt & 0xFF]);
}
}
}
AES.prototype.encrypt = function(plaintext) {
if (plaintext.length != 16) {
throw new Error('invalid plaintext size (must be 16 bytes)');
}
var rounds = this._Ke.length - 1;
var a = [0, 0, 0, 0];
// convert plaintext to (ints ^ key)
var t = convertToInt32(plaintext);
for (var i = 0; i < 4; i++) {
t[i] ^= this._Ke[0][i];
}
// apply round transforms
for (var r = 1; r < rounds; r++) {
for (var i = 0; i < 4; i++) {
a[i] = (T1[(t[ i ] >> 24) & 0xff] ^
T2[(t[(i + 1) % 4] >> 16) & 0xff] ^
T3[(t[(i + 2) % 4] >> 8) & 0xff] ^
T4[ t[(i + 3) % 4] & 0xff] ^
this._Ke[r][i]);
}
t = a.slice();
}
// the last round is special
var result = createArray(16), tt;
for (var i = 0; i < 4; i++) {
tt = this._Ke[rounds][i];
result[4 * i ] = (S[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;
result[4 * i + 1] = (S[(t[(i + 1) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;
result[4 * i + 2] = (S[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;
result[4 * i + 3] = (S[ t[(i + 3) % 4] & 0xff] ^ tt ) & 0xff;
}
return result;
}
AES.prototype.decrypt = function(ciphertext) {
if (ciphertext.length != 16) {
throw new Error('invalid ciphertext size (must be 16 bytes)');
}
var rounds = this._Kd.length - 1;
var a = [0, 0, 0, 0];
// convert plaintext to (ints ^ key)
var t = convertToInt32(ciphertext);
for (var i = 0; i < 4; i++) {
t[i] ^= this._Kd[0][i];
}
// apply round transforms
for (var r = 1; r < rounds; r++) {
for (var i = 0; i < 4; i++) {
a[i] = (T5[(t[ i ] >> 24) & 0xff] ^
T6[(t[(i + 3) % 4] >> 16) & 0xff] ^
T7[(t[(i + 2) % 4] >> 8) & 0xff] ^
T8[ t[(i + 1) % 4] & 0xff] ^
this._Kd[r][i]);
}
t = a.slice();
}
// the last round is special
var result = createArray(16), tt;
for (var i = 0; i < 4; i++) {
tt = this._Kd[rounds][i];
result[4 * i ] = (Si[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;
result[4 * i + 1] = (Si[(t[(i + 3) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;
result[4 * i + 2] = (Si[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;
result[4 * i + 3] = (Si[ t[(i + 1) % 4] & 0xff] ^ tt ) & 0xff;
}
return result;
}
/**
* Mode Of Operation - Electonic Codebook (ECB)
*/
var ModeOfOperationECB = function(key) {
if (!(this instanceof ModeOfOperationECB)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Electronic Code Block";
this.name = "ecb";
this._aes = new AES(key);
}
ModeOfOperationECB.prototype.encrypt = function(plaintext) {
plaintext = coerceArray(plaintext);
if ((plaintext.length % 16) !== 0) {
throw new Error('invalid plaintext size (must be multiple of 16 bytes)');
}
var ciphertext = createArray(plaintext.length);
var block = createArray(16);
for (var i = 0; i < plaintext.length; i += 16) {
copyArray(plaintext, block, 0, i, i + 16);
block = this._aes.encrypt(block);
copyArray(block, ciphertext, i);
}
return ciphertext;
}
ModeOfOperationECB.prototype.decrypt = function(ciphertext) {
ciphertext = coerceArray(ciphertext);
if ((ciphertext.length % 16) !== 0) {
throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');
}
var plaintext = createArray(ciphertext.length);
var block = createArray(16);
for (var i = 0; i < ciphertext.length; i += 16) {
copyArray(ciphertext, block, 0, i, i + 16);
block = this._aes.decrypt(block);
copyArray(block, plaintext, i);
}
return plaintext;
}
/**
* Mode Of Operation - Cipher Block Chaining (CBC)
*/
var ModeOfOperationCBC = function(key, iv) {
if (!(this instanceof ModeOfOperationCBC)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Cipher Block Chaining";
this.name = "cbc";
if (!iv) {
iv = createArray(16);
} else if (iv.length != 16) {
throw new Error('invalid initialation vector size (must be 16 bytes)');
}
this._lastCipherblock = coerceArray(iv, true);
this._aes = new AES(key);
}
ModeOfOperationCBC.prototype.encrypt = function(plaintext) {
plaintext = coerceArray(plaintext);
if ((plaintext.length % 16) !== 0) {
throw new Error('invalid plaintext size (must be multiple of 16 bytes)');
}
var ciphertext = createArray(plaintext.length);
var block = createArray(16);
for (var i = 0; i < plaintext.length; i += 16) {
copyArray(plaintext, block, 0, i, i + 16);
for (var j = 0; j < 16; j++) {
block[j] ^= this._lastCipherblock[j];
}
this._lastCipherblock = this._aes.encrypt(block);
copyArray(this._lastCipherblock, ciphertext, i);
}
return ciphertext;
}
ModeOfOperationCBC.prototype.decrypt = function(ciphertext) {
ciphertext = coerceArray(ciphertext);
if ((ciphertext.length % 16) !== 0) {
throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');
}
var plaintext = createArray(ciphertext.length);
var block = createArray(16);
for (var i = 0; i < ciphertext.length; i += 16) {
copyArray(ciphertext, block, 0, i, i + 16);
block = this._aes.decrypt(block);
for (var j = 0; j < 16; j++) {
plaintext[i + j] = block[j] ^ this._lastCipherblock[j];
}
copyArray(ciphertext, this._lastCipherblock, 0, i, i + 16);
}
return plaintext;
}
/**
* Mode Of Operation - Cipher Feedback (CFB)
*/
var ModeOfOperationCFB = function(key, iv, segmentSize) {
if (!(this instanceof ModeOfOperationCFB)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Cipher Feedback";
this.name = "cfb";
if (!iv) {
iv = createArray(16);
} else if (iv.length != 16) {
throw new Error('invalid initialation vector size (must be 16 size)');
}
if (!segmentSize) { segmentSize = 1; }
this.segmentSize = segmentSize;
this._shiftRegister = coerceArray(iv, true);
this._aes = new AES(key);
}
ModeOfOperationCFB.prototype.encrypt = function(plaintext) {
if ((plaintext.length % this.segmentSize) != 0) {
throw new Error('invalid plaintext size (must be segmentSize bytes)');
}
var encrypted = coerceArray(plaintext, true);
var xorSegment;
for (var i = 0; i < encrypted.length; i += this.segmentSize) {
xorSegment = this._aes.encrypt(this._shiftRegister);
for (var j = 0; j < this.segmentSize; j++) {
encrypted[i + j] ^= xorSegment[j];
}
// Shift the register
copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);
copyArray(encrypted, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);
}
return encrypted;
}
ModeOfOperationCFB.prototype.decrypt = function(ciphertext) {
if ((ciphertext.length % this.segmentSize) != 0) {
throw new Error('invalid ciphertext size (must be segmentSize bytes)');
}
var plaintext = coerceArray(ciphertext, true);
var xorSegment;
for (var i = 0; i < plaintext.length; i += this.segmentSize) {
xorSegment = this._aes.encrypt(this._shiftRegister);
for (var j = 0; j < this.segmentSize; j++) {
plaintext[i + j] ^= xorSegment[j];
}
// Shift the register
copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);
copyArray(ciphertext, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);
}
return plaintext;
}
/**
* Mode Of Operation - Output Feedback (OFB)
*/
var ModeOfOperationOFB = function(key, iv) {
if (!(this instanceof ModeOfOperationOFB)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Output Feedback";
this.name = "ofb";
if (!iv) {
iv = createArray(16);
} else if (iv.length != 16) {
throw new Error('invalid initialation vector size (must be 16 bytes)');
}
this._lastPrecipher = coerceArray(iv, true);
this._lastPrecipherIndex = 16;
this._aes = new AES(key);
}
ModeOfOperationOFB.prototype.encrypt = function(plaintext) {
var encrypted = coerceArray(plaintext, true);
for (var i = 0; i < encrypted.length; i++) {
if (this._lastPrecipherIndex === 16) {
this._lastPrecipher = this._aes.encrypt(this._lastPrecipher);
this._lastPrecipherIndex = 0;
}
encrypted[i] ^= this._lastPrecipher[this._lastPrecipherIndex++];
}
return encrypted;
}
// Decryption is symetric
ModeOfOperationOFB.prototype.decrypt = ModeOfOperationOFB.prototype.encrypt;
/**
* Counter object for CTR common mode of operation
*/
var Counter = function(initialValue) {
if (!(this instanceof Counter)) {
throw Error('Counter must be instanitated with `new`');
}
// We allow 0, but anything false-ish uses the default 1
if (initialValue !== 0 && !initialValue) { initialValue = 1; }
if (typeof(initialValue) === 'number') {
this._counter = createArray(16);
this.setValue(initialValue);
} else {
this.setBytes(initialValue);
}
}
Counter.prototype.setValue = function(value) {
if (typeof(value) !== 'number' || parseInt(value) != value) {
throw new Error('invalid counter value (must be an integer)');
}
// We cannot safely handle numbers beyond the safe range for integers
if (value > Number.MAX_SAFE_INTEGER) {
throw new Error('integer value out of safe range');
}
for (var index = 15; index >= 0; --index) {
this._counter[index] = value % 256;
value = parseInt(value / 256);
}
}
Counter.prototype.setBytes = function(bytes) {
bytes = coerceArray(bytes, true);
if (bytes.length != 16) {
throw new Error('invalid counter bytes size (must be 16 bytes)');
}
this._counter = bytes;
};
Counter.prototype.increment = function() {
for (var i = 15; i >= 0; i--) {
if (this._counter[i] === 255) {
this._counter[i] = 0;
} else {
this._counter[i]++;
break;
}
}
}
/**
* Mode Of Operation - Counter (CTR)
*/
var ModeOfOperationCTR = function(key, counter) {
if (!(this instanceof ModeOfOperationCTR)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Counter";
this.name = "ctr";
if (!(counter instanceof Counter)) {
counter = new Counter(counter)
}
this._counter = counter;
this._remainingCounter = null;
this._remainingCounterIndex = 16;
this._aes = new AES(key);
}
ModeOfOperationCTR.prototype.encrypt = function(plaintext) {
var encrypted = coerceArray(plaintext, true);
for (var i = 0; i < encrypted.length; i++) {
if (this._remainingCounterIndex === 16) {
this._remainingCounter = this._aes.encrypt(this._counter._counter);
this._remainingCounterIndex = 0;
this._counter.increment();
}
encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++];
}
return encrypted;
}
// Decryption is symetric
ModeOfOperationCTR.prototype.decrypt = ModeOfOperationCTR.prototype.encrypt;
///////////////////////
// Padding
// See:https://tools.ietf.org/html/rfc2315
function pkcs7pad(data) {
data = coerceArray(data, true);
var padder = 16 - (data.length % 16);
var result = createArray(data.length + padder);
copyArray(data, result);
for (var i = data.length; i < result.length; i++) {
result[i] = padder;
}
return result;
}
function pkcs7strip(data) {
data = coerceArray(data, true);
if (data.length < 16) { throw new Error('PKCS#7 invalid length'); }
var padder = data[data.length - 1];
if (padder > 16) { throw new Error('PKCS#7 padding byte out of range'); }
var length = data.length - padder;
for (var i = 0; i < padder; i++) {
if (data[length + i] !== padder) {
throw new Error('PKCS#7 invalid padding byte');
}
}
var result = createArray(length);
copyArray(data, result, 0, 0, length);
return result;
}
///////////////////////
// Exporting
// The block cipher
var aesjs = {
AES: AES,
Counter: Counter,
ModeOfOperation: {
ecb: ModeOfOperationECB,
cbc: ModeOfOperationCBC,
cfb: ModeOfOperationCFB,
ofb: ModeOfOperationOFB,
ctr: ModeOfOperationCTR
},
utils: {
hex: convertHex,
utf8: convertUtf8
},
padding: {
pkcs7: {
pad: pkcs7pad,
strip: pkcs7strip
}
},
_arrayTest: {
coerceArray: coerceArray,
createArray: createArray,
copyArray: copyArray,
}
};
// node.js
if (typeof exports !== 'undefined') {
module.exports = aesjs
// RequireJS/AMD
// http://www.requirejs.org/docs/api.html
// https://github.com/amdjs/amdjs-api/wiki/AMD
} else if (typeof(define) === 'function' && define.amd) {
define([], function() { return aesjs; });
// Web Browsers
} else {
// If there was an existing library at "aesjs" make sure it's still available
if (root.aesjs) {
aesjs._aesjs = root.aesjs;
}
root.aesjs = aesjs;
}
})(this);

File diff suppressed because it is too large Load diff

View file

@ -386,6 +386,13 @@ const watchOnly = async () => {
this.addresses = addresses this.addresses = addresses
this.scanAddressWithAmount() this.scanAddressWithAmount()
} }
},
handleBroadcastSuccess: async function (txId) {
this.tab = 'history'
this.searchInTab({tab: 'history', value: txId})
this.showPayment = false
await this.refreshAddresses()
await this.scanAddressWithAmount()
} }
}, },
created: async function () { created: async function () {

View file

@ -1,4 +1,6 @@
const PSBT_BASE64_PREFIX = 'cHNidP8' const PSBT_BASE64_PREFIX = 'cHNidP8'
const COMMAND_PING = '/ping'
const COMMAND_PASSWORD = '/password' const COMMAND_PASSWORD = '/password'
const COMMAND_PASSWORD_CLEAR = '/password-clear' const COMMAND_PASSWORD_CLEAR = '/password-clear'
const COMMAND_SEND_PSBT = '/psbt' const COMMAND_SEND_PSBT = '/psbt'
@ -10,8 +12,24 @@ const COMMAND_RESTORE = '/restore'
const COMMAND_CONFIRM_NEXT = '/confirm-next' const COMMAND_CONFIRM_NEXT = '/confirm-next'
const COMMAND_CANCEL = '/cancel' const COMMAND_CANCEL = '/cancel'
const COMMAND_XPUB = '/xpub' const COMMAND_XPUB = '/xpub'
const COMMAND_PAIR = '/pair'
const COMMAND_LOG = '/log'
const COMMAND_CHECK_PAIRING = '/check-pairing'
const DEFAULT_RECEIVE_GAP_LIMIT = 20 const DEFAULT_RECEIVE_GAP_LIMIT = 20
const PAIRING_CONTROL_TEXT = 'lnbits'
const HWW_DEFAULT_CONFIG = Object.freeze({
name: '',
buttonOnePin: '',
buttonTwoPin: '',
baudRate: 9600,
bufferSize: 255,
dataBits: 8,
flowControl: 'none',
parity: 'none',
stopBits: 1
})
const blockTimeToDate = blockTime => const blockTimeToDate = blockTime =>
blockTime ? moment(blockTime * 1000).format('LLL') : '' blockTime ? moment(blockTime * 1000).format('LLL') : ''
@ -181,3 +199,11 @@ function findAccountPathIssues(path = '') {
if (isNaN(p[i])) return `${p[i]} is not a valid value` if (isNaN(p[i])) return `${p[i]} is not a valid value`
} }
} }
function asciiToUint8Array(str) {
var chars = []
for (var i = 0; i < str.length; ++i) {
chars.push(str.charCodeAt(i))
}
return new Uint8Array(chars)
}

View file

@ -136,6 +136,7 @@
:adminkey="g.user.wallets[0].adminkey" :adminkey="g.user.wallets[0].adminkey"
:serial-signer-ref="$refs.serialSigner" :serial-signer-ref="$refs.serialSigner"
:sats-denominated="config.sats_denominated" :sats-denominated="config.sats_denominated"
@broadcast-done="handleBroadcastSuccess"
></payment> ></payment>
<!-- todo: no more utxos.data --> <!-- todo: no more utxos.data -->
</div> </div>
@ -176,7 +177,7 @@
dense dense
size="ms" size="ms"
icon="content_copy" icon="content_copy"
@click="copyText(props.row.address)" @click="copyText(currentAddress.address)"
class="q-ml-sm" class="q-ml-sm"
></q-btn> ></q-btn>
@ -248,5 +249,8 @@
<script src="{{ url_for('watchonly_static', path='components/payment/payment.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/payment/payment.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/serial-signer/serial-signer.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/serial-signer/serial-signer.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/serial-port-config/serial-port-config.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/serial-port-config/serial-port-config.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='js/crypto/noble-secp256k1.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='js/crypto/aes.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='js/index.js') }}"></script> <script src="{{ url_for('watchonly_static', path='js/index.js') }}"></script>
{% endblock %} {% endblock %}

View file

@ -418,16 +418,18 @@
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="link"
@click="copyText(qrCodeDialog.data.withdraw_url, 'Link copied to clipboard!')" @click="copyText(qrCodeDialog.data.withdraw_url, 'Link copied to clipboard!')"
>Shareable link</q-btn ><q-tooltip>Copy sharable link</q-tooltip>
> </q-btn>
<q-btn <q-btn
outline outline
color="grey" color="grey"
icon="nfc" icon="nfc"
@click="writeNfcTag(qrCodeDialog.data.lnurl)" @click="writeNfcTag(qrCodeDialog.data.lnurl)"
:disable="nfcTagWriting" :disable="nfcTagWriting"
></q-btn> ><q-tooltip>Write to NFC</q-tooltip></q-btn
>
<q-btn <q-btn
outline outline
color="grey" color="grey"
@ -435,7 +437,8 @@
type="a" type="a"
:href="qrCodeDialog.data.print_url" :href="qrCodeDialog.data.print_url"
target="_blank" target="_blank"
></q-btn> ><q-tooltip>Print</q-tooltip></q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-card>

View file

@ -113,7 +113,7 @@ async def api_link_create_or_update(
return {**link.dict(), **{"lnurl": link.lnurl(req)}} return {**link.dict(), **{"lnurl": link.lnurl(req)}}
@withdraw_ext.delete("/api/v1/links/{link_id}") @withdraw_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admin_key)): async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
link = await get_withdraw_link(link_id) link = await get_withdraw_link(link_id)
@ -128,7 +128,7 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admi
) )
await delete_withdraw_link(link_id) await delete_withdraw_link(link_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) return {"success": True}
@withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}", status_code=HTTPStatus.OK) @withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}", status_code=HTTPStatus.OK)

View file

@ -179,6 +179,11 @@ Vue.component('lnbits-extension-list', {
Vue.component('lnbits-payment-details', { Vue.component('lnbits-payment-details', {
props: ['payment'], props: ['payment'],
data: function () {
return {
LNBITS_DENOMINATION: LNBITS_DENOMINATION
}
},
template: ` template: `
<div class="q-py-md" style="text-align: left"> <div class="q-py-md" style="text-align: left">
<div class="row justify-center q-mb-md"> <div class="row justify-center q-mb-md">

View file

@ -87,8 +87,10 @@ class CoreLightningWallet(Wallet):
label = "lbl{}".format(random.random()) label = "lbl{}".format(random.random())
msat: int = int(amount * 1000) msat: int = int(amount * 1000)
try: try:
if description_hash: if description_hash and not unhashed_description:
raise Unsupported("description_hash") raise Unsupported(
"'description_hash' unsupported by CLN, provide 'unhashed_description'"
)
if unhashed_description and not self.supports_description_hash: if unhashed_description and not self.supports_description_hash:
raise Unsupported("unhashed_description") raise Unsupported("unhashed_description")
r = self.ln.invoice( r = self.ln.invoice(

View file

@ -45,9 +45,16 @@ async def test_get_wallet_adminkey(client, adminkey_headers_to):
assert "id" in result assert "id" in result
# check POST /api/v1/payments: empty request # check PUT /api/v1/wallet/newwallet: empty request where admin key is needed
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_post_empty_request(client): async def test_put_empty_request_expected_admin_keys(client):
response = await client.put("/api/v1/wallet/newwallet")
assert response.status_code == 401
# check POST /api/v1/payments: empty request where invoice key is needed
@pytest.mark.asyncio
async def test_post_empty_request_expected_invoice_keys(client):
response = await client.post("/api/v1/payments") response = await client.post("/api/v1/payments")
assert response.status_code == 401 assert response.status_code == 401

Binary file not shown.

View file

@ -19,16 +19,12 @@ env.read_env()
# Change these values as needed # Change these values as needed
sqfolder = "data/" sqfolder = env.str("LNBITS_DATA_FOLDER", default=None)
LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None) LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None)
if LNBITS_DATABASE_URL is None: if LNBITS_DATABASE_URL is None:
pgdb = "lnbits" print("missing LNBITS_DATABASE_URL")
pguser = "lnbits" sys.exit(1)
pgpswd = "postgres"
pghost = "localhost"
pgport = "5432"
pgschema = ""
else: else:
# parse postgres://lnbits:postgres@localhost:5432/lnbits # parse postgres://lnbits:postgres@localhost:5432/lnbits
pgdb = LNBITS_DATABASE_URL.split("/")[-1] pgdb = LNBITS_DATABASE_URL.split("/")[-1]