🚧 Added Proof of Work on image-upload and more
This commit is contained in:
@@ -30,6 +30,7 @@ pillow = ">=9.1.1"
|
||||
authlib = "*"
|
||||
httpx = "*"
|
||||
itsdangerous = "*"
|
||||
puremagic = "*"
|
||||
|
||||
[dev-packages]
|
||||
coverage = "*"
|
||||
|
||||
Generated
+16
-8
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "259996d9979f6434bbd2a43c1146e10b485bca6df5fdb9d37319883800c56e33"
|
||||
"sha256": "fe59e46268415d1b9088e7e27b599d88913b762a99d940336ec2e01e8e122378"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -250,11 +250,11 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7",
|
||||
"sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"
|
||||
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
|
||||
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2022.5.18.1"
|
||||
"version": "==2022.6.15"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
@@ -883,6 +883,14 @@
|
||||
],
|
||||
"version": "==2.9.3"
|
||||
},
|
||||
"puremagic": {
|
||||
"hashes": [
|
||||
"sha256:3d5df26cc7ec9aebbf842a09115a2fa85dc59ea6414fa568572c44775d796cbc",
|
||||
"sha256:40e32752827f2d0cea7e6f11454fab2b4bd440582af83d8a47bf403ab42364fa"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.14"
|
||||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||
@@ -1084,11 +1092,11 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77",
|
||||
"sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91"
|
||||
"sha256:990a4f7861b31532871ab72331e755b5f14efbe52d336ea7f6118144dd478741",
|
||||
"sha256:c1848f654aea2e3526d17fc3ce6aeaa5e7e24e66e645b5be2171f3f6b4e5a178"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==62.4.0"
|
||||
"version": "==62.6.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -1750,7 +1758,7 @@
|
||||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"virtualenv": {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python2.3
|
||||
"""Implement Hashcash version 1 protocol in Python
|
||||
+-------------------------------------------------------+
|
||||
| Written by David Mertz; released to the Public Domain |
|
||||
+-------------------------------------------------------+
|
||||
|
||||
Double spend database not implemented in this module, but stub
|
||||
for callbacks is provided in the 'check()' function
|
||||
|
||||
The function 'check()' will validate hashcash v1 and v0 tokens, as well as
|
||||
'generalized hashcash' tokens generically. Future protocol version are
|
||||
treated as generalized tokens (should a future version be published w/o
|
||||
this module being correspondingly updated).
|
||||
|
||||
A 'generalized hashcash' is implemented in the '_mint()' function, with the
|
||||
public function 'mint()' providing a wrapper for actual hashcash protocol.
|
||||
The generalized form simply finds a suffix that creates zero bits in the
|
||||
hash of the string concatenating 'challenge' and 'suffix' without specifying
|
||||
any particular fields or delimiters in 'challenge'. E.g., you might get:
|
||||
|
||||
>>> from hashcash import mint, _mint
|
||||
>>> mint('foo', bits=16)
|
||||
'1:16:040922:foo::+ArSrtKd:164b3'
|
||||
>>> _mint('foo', bits=16)
|
||||
'9591'
|
||||
>>> import hashlib
|
||||
>>> hashlib.sha1(('foo9591').encode()).hexdigest()
|
||||
'0000de4c9b27cec9b20e2094785c1c58eaf23948'
|
||||
>>> hashlib.sha1(('1:16:040922:foo::+ArSrtKd:164b3').encode()).hexdigest()
|
||||
'0000a9fe0c6db2efcbcab15157735e77c0877f34'
|
||||
|
||||
Notice that '_mint()' behaves deterministically, finding the same suffix
|
||||
every time it is passed the same arguments. 'mint()' incorporates a random
|
||||
salt in stamps (as per the hashcash v.1 protocol).
|
||||
"""
|
||||
import sys
|
||||
from string import ascii_letters
|
||||
from math import ceil, floor
|
||||
import hashlib
|
||||
from random import choice
|
||||
from time import strftime, localtime, time
|
||||
|
||||
|
||||
class HCNotValid(BaseException):
|
||||
message: str
|
||||
|
||||
|
||||
DAYS = 60 * 60 * 24 # Seconds in a day
|
||||
tries = [0] # Count hashes performed for benchmark
|
||||
|
||||
|
||||
def mint(resource, bits=20, now=None, ext="", saltchars=8, stamp_seconds=False):
|
||||
"""Mint a new hashcash stamp for 'resource' with 'bits' of collision
|
||||
|
||||
20 bits of collision is the default.
|
||||
|
||||
'ext' lets you add your own extensions to a minted stamp. Specify an
|
||||
extension as a string of form 'name1=2,3;name2;name3=var1=2,2,val'
|
||||
FWIW, urllib.urlencode(dct).replace('&',';') comes close to the
|
||||
hashcash extension format.
|
||||
|
||||
'saltchars' specifies the length of the salt used; this version defaults
|
||||
8 chars, rather than the C version's 16 chars. This still provides about
|
||||
17 million salts per resource, per timestamp, before birthday paradox
|
||||
collisions occur. Really paranoid users can use a larger salt though.
|
||||
|
||||
'stamp_seconds' lets you add the option time elements to the datestamp.
|
||||
If you want more than just day, you get all the way down to seconds,
|
||||
even though the spec also allows hours/minutes without seconds.
|
||||
"""
|
||||
ver = "1"
|
||||
now = now or time()
|
||||
if stamp_seconds:
|
||||
ts = strftime("%y%m%d%H%M%S", localtime(now))
|
||||
else:
|
||||
ts = strftime("%y%m%d", localtime(now))
|
||||
challenge = "%s:" * 6 % (ver, bits, ts, resource, ext, _salt(saltchars))
|
||||
return challenge + _mint(challenge, bits)
|
||||
|
||||
|
||||
def _salt(l):
|
||||
"Return a random string of length 'l'"
|
||||
alphabet = ascii_letters + "+/="
|
||||
return "".join([choice(alphabet) for _ in [None] * l])
|
||||
|
||||
|
||||
def _mint(challenge, bits):
|
||||
"""Answer a 'generalized hashcash' challenge'
|
||||
|
||||
Hashcash requires stamps of form 'ver:bits:date:res:ext:rand:counter'
|
||||
This internal function accepts a generalized prefix 'challenge',
|
||||
and returns only a suffix that produces the requested SHA leading zeros.
|
||||
|
||||
NOTE: Number of requested bits is rounded up to the nearest multiple of 4
|
||||
"""
|
||||
counter = 0
|
||||
hex_digits = int(ceil(bits / 4.0))
|
||||
zeros = "0" * hex_digits
|
||||
while 1:
|
||||
digest = hashlib.sha1((challenge + hex(counter)[2:]).encode()).hexdigest()
|
||||
if digest[:hex_digits] == zeros:
|
||||
tries[0] = counter
|
||||
return hex(counter)[2:]
|
||||
counter += 1
|
||||
|
||||
|
||||
def check(stamp, resource=None, bits=None, check_expiration=None, ds_callback=None):
|
||||
"""Check whether a stamp is valid
|
||||
|
||||
Optionally, the stamp may be checked for a specific resource, and/or
|
||||
it may require a minimum bit value, and/or it may be checked for
|
||||
expiration, and/or it may be checked for double spending.
|
||||
|
||||
If 'check_expiration' is specified, it should contain the number of
|
||||
seconds old a date field may be. Indicating days might be easier in
|
||||
many cases, e.g.
|
||||
|
||||
>>> from hashcash import DAYS
|
||||
>>> check(stamp, check_expiration=28*DAYS)
|
||||
|
||||
NOTE: Every valid (version 1) stamp must meet its claimed bit value
|
||||
NOTE: Check floor of 4-bit multiples (overly permissive in acceptance)
|
||||
"""
|
||||
if stamp.startswith("0:"): # Version 0
|
||||
try:
|
||||
date, res, suffix = stamp[2:].split(":")
|
||||
except ValueError:
|
||||
return False
|
||||
if resource is not None and resource != res:
|
||||
return False
|
||||
elif check_expiration is not None:
|
||||
good_until = strftime("%y%m%d%H%M%S", localtime(time() - check_expiration))
|
||||
if date < good_until:
|
||||
return False
|
||||
elif callable(ds_callback) and ds_callback(stamp):
|
||||
return False
|
||||
elif type(bits) is not int:
|
||||
return True
|
||||
else:
|
||||
hex_digits = int(floor(bits / 4))
|
||||
return hashlib.sha1((stamp).encode()).hexdigest().startswith("0" * hex_digits)
|
||||
elif stamp.startswith("1:"): # Version 1
|
||||
try:
|
||||
claim, date, res, ext, rand, counter = stamp[2:].split(":")
|
||||
except ValueError:
|
||||
return False
|
||||
if resource is not None and resource != res:
|
||||
return False
|
||||
elif type(bits) is int and bits > int(claim):
|
||||
return False
|
||||
elif check_expiration is not None:
|
||||
good_until = strftime("%y%m%d%H%M%S", localtime(time() - check_expiration))
|
||||
if date < good_until:
|
||||
return False
|
||||
elif callable(ds_callback) and ds_callback(stamp):
|
||||
return False
|
||||
else:
|
||||
hex_digits = int(floor(int(claim) / 4))
|
||||
return hashlib.sha1((stamp).encode()).hexdigest().startswith("0" * hex_digits)
|
||||
else: # Unknown ver or generalized hashcash
|
||||
if type(bits) is not int:
|
||||
return True
|
||||
elif resource is not None and stamp.find(resource) < 0:
|
||||
return False
|
||||
else:
|
||||
hex_digits = int(floor(bits / 4))
|
||||
return hashlib.sha1((stamp).encode()).hexdigest().startswith("0" * hex_digits)
|
||||
@@ -11,18 +11,21 @@ from pydantic import BaseModel
|
||||
|
||||
from classquiz.config import settings, redis, storage, meilisearch
|
||||
from classquiz.db.models import Quiz, QuizInput, User
|
||||
import puremagic
|
||||
from classquiz.auth import get_current_user
|
||||
import os
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from classquiz.helpers import get_meili_data
|
||||
from classquiz.helpers import get_meili_data, check_hashcash
|
||||
from classquiz.storage.errors import DeletionFailedError
|
||||
|
||||
settings = settings()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
allowed_image_extensions = [".gif", ".jpg", ".jpeg", ".png", ".svg", ".webp"]
|
||||
|
||||
|
||||
class InitEditorResponse(BaseModel):
|
||||
token: str
|
||||
@@ -60,21 +63,44 @@ async def init_editor(edit: bool, quiz_id: Optional[UUID] = None, user: User = D
|
||||
return InitEditorResponse(token=edit_id)
|
||||
|
||||
|
||||
class GetPowData(BaseModel):
|
||||
data: str
|
||||
|
||||
|
||||
@router.get("/pow", response_model=GetPowData)
|
||||
async def get_pow_data(edit_id: str):
|
||||
session_data = await redis.get(f"edit_session:{edit_id}")
|
||||
if session_data is None:
|
||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||
random_str = os.urandom(8).hex()
|
||||
await redis.set(f"edit_session:{edit_id}:pow", random_str, ex=3800)
|
||||
return GetPowData(data=random_str)
|
||||
|
||||
|
||||
class UploadImageReturn(BaseModel):
|
||||
id: str
|
||||
|
||||
|
||||
@router.post("/image", response_model=UploadImageReturn)
|
||||
async def upload_image(edit_id: str, file: UploadFile = File()):
|
||||
async def upload_image(edit_id: str, pow_data: str, file: UploadFile = File()):
|
||||
print(pow_data)
|
||||
session_data = await redis.get(f"edit_session:{edit_id}")
|
||||
pow_data_server = await redis.get(f"edit_session:{edit_id}:pow")
|
||||
if pow_data_server is None:
|
||||
print("1")
|
||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||
if not check_hashcash(pow_data, pow_data_server):
|
||||
print("2")
|
||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||
if session_data is None:
|
||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||
file_bytes = await file.read()
|
||||
lol = puremagic.magic_string(file_bytes)
|
||||
print(lol)
|
||||
session_data = EditSessionData.parse_raw(session_data)
|
||||
file_name = f"{session_data.quiz_id}--{uuid.uuid4()}"
|
||||
print("Uploading...")
|
||||
await storage.upload(file_name=file_name, file_data=await file.read())
|
||||
print("Finished Upload")
|
||||
await redis.lpush(f"edit_session:{edit_id}:images", file_name)
|
||||
# await storage.upload(file_name=file_name, file_data=file_bytes)
|
||||
# await redis.lpush(f"edit_session:{edit_id}:images", file_name)
|
||||
return UploadImageReturn(id=file_name)
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ async def create_user(user: route_user, background_task: BackgroundTasks) -> Use
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
user.verify_key = str(os.urandom(16).hex())
|
||||
res = await User.objects.filter((User.email == user.email) | (User.username == user.username)).all()
|
||||
|
||||
if len(res) != 0:
|
||||
raise HTTPException(status_code=409, detail="User already exists")
|
||||
|
||||
@@ -162,9 +161,8 @@ class ForgotPassword(BaseModel):
|
||||
@router.post("/forgot-password")
|
||||
async def forgotten_password(forgot_password: ForgotPassword, background_task: BackgroundTasks):
|
||||
user = await User.objects.filter(email=forgot_password.email, verified=True).get_or_none()
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
background_task.add_task(send_forgotten_password_email, email=user.email)
|
||||
if user is not None:
|
||||
background_task.add_task(send_forgotten_password_email, email=user.email)
|
||||
return {"message": "Password reset email sent"}
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ class DetaStorage:
|
||||
if response.status == 201:
|
||||
return None
|
||||
else:
|
||||
print(response.status, await response.json())
|
||||
raise SavingFailedError
|
||||
|
||||
async def delete(self, file_names: [str]) -> None:
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@uppy/xhr-upload": "^2.1.2",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"cookie": "^0.5.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"cssnano": "^5.1.11",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
|
||||
Generated
+9
@@ -27,6 +27,7 @@ specifiers:
|
||||
'@uppy/xhr-upload': ^2.1.2
|
||||
autoprefixer: ^10.4.7
|
||||
cookie: ^0.5.0
|
||||
crypto-js: ^4.1.1
|
||||
cssnano: ^5.1.11
|
||||
eslint: ^8.17.0
|
||||
eslint-config-prettier: ^8.5.0
|
||||
@@ -89,6 +90,7 @@ devDependencies:
|
||||
'@uppy/xhr-upload': 2.1.2_@uppy+core@2.3.1
|
||||
autoprefixer: 10.4.7_postcss@8.4.14
|
||||
cookie: 0.5.0
|
||||
crypto-js: 4.1.1
|
||||
cssnano: 5.1.11_postcss@8.4.14
|
||||
eslint: 8.17.0
|
||||
eslint-config-prettier: 8.5.0_eslint@8.17.0
|
||||
@@ -1562,6 +1564,13 @@ packages:
|
||||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/crypto-js/4.1.1:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
|
||||
}
|
||||
dev: true
|
||||
|
||||
/css-declaration-sorter/6.3.0_postcss@8.4.14:
|
||||
resolution:
|
||||
{
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import * as yup from 'yup';
|
||||
import { mint } from '$lib/hashcash';
|
||||
import { dataSchema } from '$lib/yupSchemas';
|
||||
import type { EditorData, Question, Answer } from './quiz_types';
|
||||
import Sidebar from '$lib/editor/sidebar.svelte';
|
||||
import SettingsCard from '$lib/editor/settings-card.svelte';
|
||||
import QuizCard from '$lib/editor/card.svelte';
|
||||
import Spinner from './Spinner.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
@@ -67,6 +68,7 @@
|
||||
};
|
||||
let edit_id;
|
||||
let confirm_to_leave = true;
|
||||
let pow_data;
|
||||
|
||||
const getEditID = async () => {
|
||||
let res;
|
||||
@@ -82,7 +84,7 @@
|
||||
if (res.status === 200) {
|
||||
const json = await res.json();
|
||||
edit_id = json.token;
|
||||
console.log(edit_id, json);
|
||||
setPOWdata();
|
||||
} else {
|
||||
alert('Error!');
|
||||
}
|
||||
@@ -110,7 +112,6 @@
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
console.log('Hier');
|
||||
confirm_to_leave = false;
|
||||
console.log(confirm_to_leave);
|
||||
window.location.href = '/overview';
|
||||
@@ -118,6 +119,13 @@
|
||||
alert('Error');
|
||||
}
|
||||
};
|
||||
const setPOWdata = async () => {
|
||||
const res = await fetch(`/api/v1/editor/pow?edit_id=${edit_id}`);
|
||||
const data = (await res.json()).data;
|
||||
console.log(data);
|
||||
pow_data = await mint(data);
|
||||
console.log(pow_data);
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={confirmUnload} />
|
||||
@@ -167,7 +175,7 @@
|
||||
{#if selected_question === -1}
|
||||
<SettingsCard bind:data />
|
||||
{:else}
|
||||
<QuizCard bind:data bind:selected_question bind:edit_id />
|
||||
<QuizCard bind:data bind:selected_question bind:edit_id bind:pow_data />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
export let data: EditorData;
|
||||
export let selected_question: number;
|
||||
export let edit_id: string;
|
||||
export let pow_data;
|
||||
const empty_answer: Answer = {
|
||||
right: false,
|
||||
answer: ''
|
||||
@@ -67,6 +68,8 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if pow_data === undefined}
|
||||
<Spinner />
|
||||
{:else}
|
||||
{#await import('$lib/editor/uploader.svelte')}
|
||||
<Spinner />
|
||||
@@ -77,6 +80,7 @@
|
||||
bind:edit_id
|
||||
bind:data
|
||||
bind:selected_question
|
||||
bind:pow_data
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
export let edit_id: string;
|
||||
export let data: EditorData;
|
||||
export let selected_question: number;
|
||||
export let pow_data;
|
||||
|
||||
console.log(pow_data);
|
||||
const uppy = new Uppy()
|
||||
.use(DropTarget, {
|
||||
target: document.body
|
||||
@@ -34,7 +36,7 @@
|
||||
quality: 0.6
|
||||
})
|
||||
.use(XHRUpload, {
|
||||
endpoint: `/api/v1/editor/image?edit_id=${edit_id}`
|
||||
endpoint: `/api/v1/editor/image?edit_id=${edit_id}&pow_data=${pow_data}`
|
||||
});
|
||||
const props = { inline: true };
|
||||
let image_id;
|
||||
@@ -42,6 +44,7 @@
|
||||
image_id = response.body.id;
|
||||
});
|
||||
uppy.on('complete', (res) => {
|
||||
console.log(pow_data);
|
||||
data.questions[
|
||||
selected_question
|
||||
].image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const gen_salt = (l: number): string => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charLength = chars.length;
|
||||
let result = '';
|
||||
for (let i = 0; i < l; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * charLength));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const mint = async (
|
||||
resource: string,
|
||||
bits = 19,
|
||||
now = null,
|
||||
ext = '',
|
||||
saltchars = 8,
|
||||
stamp_seconds = false
|
||||
): Promise<string> => {
|
||||
const ver = '1';
|
||||
let ts;
|
||||
if (stamp_seconds) {
|
||||
ts = DateTime.now().toFormat('yyMMddHHmmss');
|
||||
} else {
|
||||
ts = DateTime.now().toFormat('yyMMdd');
|
||||
}
|
||||
const hex_digits = Math.ceil(bits / 4);
|
||||
const zeros = '0'.repeat(hex_digits);
|
||||
const salt = gen_salt(saltchars);
|
||||
const challenge = `${ver}:${bits}:${ts}:${resource}:${ext}:${salt}`;
|
||||
let digest: string;
|
||||
let counter = 0;
|
||||
let result: string;
|
||||
while (true) {
|
||||
const data = new TextEncoder().encode(`${challenge}:${counter.toString(16)}`);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const digest = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
if (digest.slice(0, hex_digits) == zeros) {
|
||||
result = counter.toString(16);
|
||||
break;
|
||||
}
|
||||
counter += 1;
|
||||
}
|
||||
return `${challenge}:${result}`;
|
||||
};
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
import LandingPromo from '$lib/landing/landing-promo.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { mint } from '$lib/hashcash';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user