Compare commits
10
Commits
bbf46d78ea
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
422fccfa88 | ||
|
|
2ed9a60216 | ||
|
|
89a11bfe48 | ||
|
|
46263f7c86 | ||
|
|
708b4bc612 | ||
|
|
aab6f34ba6 | ||
|
|
f9d7ea14be | ||
|
|
feac310ef1 | ||
|
|
402c758f0a | ||
|
|
4be9626a43 |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{type, library}.
|
{type, library}.
|
||||||
{realm, local}.
|
{realm, local}.
|
||||||
{name, jrx}.
|
{name, jrx}.
|
||||||
{version, "0.1.2"}.
|
{version, "0.1.5"}.
|
||||||
{deps, ["local-tweetnacl-1.0.3",
|
{deps, ["local-tweetnacl-1.0.3",
|
||||||
"local-awcp-0.2.3",
|
"local-awcp-0.2.3",
|
||||||
"local-vdk_aecrypt-0.1.2",
|
"local-vdk_aecrypt-0.1.2",
|
||||||
|
|||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
{"manifest_version" : 3,
|
{"manifest_version" : 3,
|
||||||
"name" : "Jack Russell",
|
"name" : "Jack Russell",
|
||||||
"version" : "0.1.2",
|
"version" : "0.1.5",
|
||||||
"description" : "Browser wallet extension for Aeternity",
|
"description" : "Browser wallet extension for Aeternity",
|
||||||
"icons" : {"48" : "art/jrx-icon-48.png",
|
"icons" : {"48" : "art/jrx-icon-48.png",
|
||||||
"96" : "art/jrx-icon-96.png"},
|
"96" : "art/jrx-icon-96.png"},
|
||||||
"content_scripts" : [{"matches" : ["*://*/*"],
|
"content_scripts" : [{"matches" : ["*://*/*"],
|
||||||
"js" : ["dist/content.js"]}],
|
"js" : ["dist/content.js"]}],
|
||||||
"action" : {"default_icon" : "art/jrx-icon-32.png",
|
"action" : {"default_icon" : "art/jrx-icon-32.png",
|
||||||
"default_title" : "Jæck Russell",
|
"default_title" : "Jack Russell (Gajumaru Wallet)",
|
||||||
"default_popup" : "pages/popup.html"},
|
"default_popup" : "pages/popup.html"},
|
||||||
"background" : {"page" : "pages/background.html"},
|
"background" : {"page" : "pages/background.html"},
|
||||||
"permissions" : ["storage", "scripting"],
|
"permissions" : ["storage", "scripting"],
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
:root {
|
||||||
|
--b1: #FBFCFB;
|
||||||
|
--b2: #D6DAD8;
|
||||||
|
--b3: #B9BCb9;
|
||||||
|
--b4: #969A98;
|
||||||
|
--b5: #7C7C7C;
|
||||||
|
--b6: #545454;
|
||||||
|
--b7: #343434;
|
||||||
|
--b8: #0A0B0A;
|
||||||
|
|
||||||
|
--g1: #c1d8c5;
|
||||||
|
--g2: #9ab6a1;
|
||||||
|
--g3: #88a28d;
|
||||||
|
--g4: #607e68;
|
||||||
|
--g5: #41664b;
|
||||||
|
--g6: #345c3e;
|
||||||
|
--g7: #224d2d;
|
||||||
|
--g8: #0a2f14;
|
||||||
|
}
|
||||||
+48
-6
@@ -4,16 +4,58 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<style type="text/css">
|
||||||
|
html, body {
|
||||||
|
background-color: #FBFCFB;
|
||||||
|
font-family: monospace, sans-serif;
|
||||||
|
font-size: '0.7em';
|
||||||
|
height: 78%;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #EBECEB;
|
||||||
|
overflow: scroll;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#miscinfo-wrapper1 {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#miscinfo-wrapper3 {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title-h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 id="title-h1"></h1>
|
<h3 id="title-h1"></h3>
|
||||||
|
|
||||||
<pre id="miscinfo"></pre>
|
<div>
|
||||||
|
<button id="good">Yes</button>
|
||||||
|
<span width="1em">
|
||||||
|
<button id="bad">No</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- dumbass css hack: https://www.w3docs.com/snippets/html/how-to-make-a-div-fill-the-height-of-the-remaining-space.html -->
|
||||||
|
<div id='miscinfo-wrapper3'>
|
||||||
|
<div id='miscinfo-wrapper1'>
|
||||||
|
<pre id="miscinfo">
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br>
|
|
||||||
<button id="good">Yes</button>
|
|
||||||
<br>
|
|
||||||
<button id="bad">No</button>
|
|
||||||
|
|
||||||
<script type="module" src="./gb.js"></script>
|
<script type="module" src="./gb.js"></script>
|
||||||
|
|
||||||
|
|||||||
+90
-13
@@ -3,17 +3,59 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
@import "gaju-colors.css";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: #d6efff;
|
background: var(--b1);
|
||||||
|
color: --var(g8);
|
||||||
width: 400px;
|
width: 400px;
|
||||||
font-family: monospace, sans-serif;
|
font-family: monospace, sans-serif;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#jr-hdr {
|
||||||
|
border: 2px solid var(--g4);
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--g2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psection {
|
||||||
|
background: var(--b2);
|
||||||
|
border: 1px solid var(--g4);
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 97%;
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-active {
|
||||||
|
background: var(--g1);
|
||||||
|
color: var(--g7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-inactive {
|
||||||
|
background: var(--b4);
|
||||||
|
color: var(--b7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-found {
|
||||||
|
color: var(--g5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-not-found {
|
||||||
|
color: var(--b7);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--g5);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<table width="100%" style="background: #dad1ff; border: 2px solid #6c5d96; border-radius: 5px;">
|
<table id="jr-hdr">
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="../art/jrx-icon-48.png" style="width: 32px; height: 32px;"></td>
|
<td><img src="../art/jrx-icon-48.png" style="width: 32px; height: 32px;"></td>
|
||||||
<td style="font-size: 32px; text-align: center; font-weight: bold">
|
<td style="font-size: 32px; text-align: center; font-weight: bold">
|
||||||
@@ -22,7 +64,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div style="margin-top: 5px; width: 95%; background: #d9ffd6; border: 1px solid #6c5d96; border-radius: 5px; padding: 5px;">
|
<div class="psection">
|
||||||
<table >
|
<table >
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
|
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
|
||||||
@@ -44,8 +86,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- generate new keypair -->
|
<!-- generate new keypair -->
|
||||||
<div style="margin-top: 5px; width: 95%; background: #d6ffd9; border: 1px solid #6c5d96; border-radius: 5px; padding: 5px;">
|
<div class="psection">
|
||||||
<table >
|
<table >
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
|
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
|
||||||
@@ -61,8 +103,9 @@
|
|||||||
<button id="generate-btn">Generate New Keypair</button>
|
<button id="generate-btn">Generate New Keypair</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- faert recovery -->
|
<!-- faert recovery -->
|
||||||
<div style="margin-top: 5px; width: 95%; background: #ffd6d9; border: 1px solid #6c5d96; border-radius: 5px; padding: 5px;">
|
<div class="psection">
|
||||||
<table >
|
<table >
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
|
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
|
||||||
@@ -81,15 +124,49 @@
|
|||||||
<br>
|
<br>
|
||||||
<button id="faert-btn">Recover Keypair from GajuPhrase</button>
|
<button id="faert-btn">Recover Keypair from GajuPhrase</button>
|
||||||
</form>
|
</form>
|
||||||
<!-- recover from seed phrase
|
|
||||||
<form>
|
|
||||||
<label for="seed-input">Seed Phrase:</label>
|
|
||||||
<input id="seed-input"></input>
|
|
||||||
<button id="seed-btn">Recover Keypair from Seed Phrase</button>
|
|
||||||
</form>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- network id -->
|
||||||
|
<div class="psection">
|
||||||
|
<table >
|
||||||
|
<tr>
|
||||||
|
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
|
||||||
|
<td style="justify-content: center; font-size: 28px; text-align: center; font-weight: bold">
|
||||||
|
NETWORK ID
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form id='netids'></form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- spend -->
|
||||||
|
<div class="psection">
|
||||||
|
<table >
|
||||||
|
<tr>
|
||||||
|
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
|
||||||
|
<td style="justify-content: center; font-size: 28px; text-align: center; font-weight: bold">
|
||||||
|
SEND SOMEONE MONEY
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form id="spend-params">
|
||||||
|
<label for="spend-recipient">Recipient Address:</label>
|
||||||
|
<input id="spend-recipient"></input>
|
||||||
|
<br>
|
||||||
|
<label for="spend-amount">Amount (gattos):</label>
|
||||||
|
<input id="spend-amount"></input>
|
||||||
|
<br>
|
||||||
|
<button type="button" id="spend-btn">Send</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p id='spend-result'></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<pre id="scratch"></pre>
|
<pre id="scratch"></pre>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
+108
-8
@@ -75,6 +75,7 @@ export {
|
|||||||
p_delete_key,
|
p_delete_key,
|
||||||
p_get_gphrase,
|
p_get_gphrase,
|
||||||
p_gphrase_recover,
|
p_gphrase_recover,
|
||||||
|
p_netid_switch,
|
||||||
// background script api
|
// background script api
|
||||||
b_main
|
b_main
|
||||||
};
|
};
|
||||||
@@ -85,6 +86,10 @@ let GB_ITERS = 1;
|
|||||||
let GB_SEC = 200*GB_ITERS; // [ITER]; 1iter / 5ms = 200iter / sec
|
let GB_SEC = 200*GB_ITERS; // [ITER]; 1iter / 5ms = 200iter / sec
|
||||||
let GB_MIN = 60*GB_SEC; // [ITER]
|
let GB_MIN = 60*GB_SEC; // [ITER]
|
||||||
|
|
||||||
|
// size of confirm windows
|
||||||
|
let GB_W_PX = 500;
|
||||||
|
let GB_H_PX = 300;
|
||||||
|
|
||||||
let MSG_SIGN_TIMEOUT = 30*GB_MIN;
|
let MSG_SIGN_TIMEOUT = 30*GB_MIN;
|
||||||
let TX_SIGN_TIMEOUT = 30*GB_MIN;
|
let TX_SIGN_TIMEOUT = 30*GB_MIN;
|
||||||
|
|
||||||
@@ -97,6 +102,7 @@ let NETIDS_ALT_DEF : Array<bi_netid> = [NETID_AETEST, NETID_AEMAIN];
|
|||||||
let NETIDS_ALL : Array<bi_netid> = [NETID_GDEV, NETID_AETEST, NETID_AEMAIN];
|
let NETIDS_ALL : Array<bi_netid> = [NETID_GDEV, NETID_AETEST, NETID_AEMAIN];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// API: GLOBAL TYPES
|
// API: GLOBAL TYPES
|
||||||
@@ -243,6 +249,18 @@ p_gphrase_recover
|
|||||||
name : name}}}) as void;
|
name : name}}}) as void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function
|
||||||
|
p_netid_switch
|
||||||
|
(new_netid : string)
|
||||||
|
: Promise<void>
|
||||||
|
{
|
||||||
|
console.log('b_lib p_netid_switch', {new_netid: new_netid});
|
||||||
|
|
||||||
|
return await browser.runtime.sendMessage({frum: 'popup',
|
||||||
|
data: {method : 'p_netid_switch',
|
||||||
|
args : {new_netid : new_netid}}}) as void;
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// API: BACKGROUND SCRIPT
|
// API: BACKGROUND SCRIPT
|
||||||
@@ -483,7 +501,7 @@ bi_msg_handler_content
|
|||||||
case "connection.open":
|
case "connection.open":
|
||||||
return w2a_ok({id : "jr",
|
return w2a_ok({id : "jr",
|
||||||
name : "JR",
|
name : "JR",
|
||||||
networkId : "gajumaru_devnet",
|
networkId : i_state.netid_cur,
|
||||||
origin : browser.runtime.getURL('/'),
|
origin : browser.runtime.getURL('/'),
|
||||||
type : "extension"});
|
type : "extension"});
|
||||||
|
|
||||||
@@ -514,13 +532,13 @@ bi_msg_handler_content
|
|||||||
console.log('jr bg content message handler transaction sign');
|
console.log('jr bg content message handler transaction sign');
|
||||||
let tx_str : string = msg.data.params.tx;
|
let tx_str : string = msg.data.params.tx;
|
||||||
console.log('transaction: ', tx_str);
|
console.log('transaction: ', tx_str);
|
||||||
let result = await tx_sign(tx_str, secret_key)
|
let result = await tx_sign(i_state.netid_cur, tx_str, secret_key)
|
||||||
if
|
if
|
||||||
('good' === result.result) {
|
('good' === result.result) {
|
||||||
return w2a_ok({signedTransaction: result.signedTransaction});
|
return w2a_ok({signedTransaction: result.signedTransaction});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return w2a_err(awcp.ERROR_CODE_RpcRejectedByUserError, "your not pretty enough");
|
return w2a_err(awcp.ERROR_CODE_RpcRejectedByUserError, "user rejected tx");
|
||||||
}
|
}
|
||||||
//if (result.result === 'good') {
|
//if (result.result === 'good') {
|
||||||
// return w2a_ok({signedTransaction: result.signedTransaction});
|
// return w2a_ok({signedTransaction: result.signedTransaction});
|
||||||
@@ -701,7 +719,8 @@ msg_sign
|
|||||||
*/
|
*/
|
||||||
async function
|
async function
|
||||||
tx_sign
|
tx_sign
|
||||||
(tx_str : string,
|
(netid_cur : bi_netid,
|
||||||
|
tx_str : string,
|
||||||
secret_key : Uint8Array)
|
secret_key : Uint8Array)
|
||||||
: Promise< {result : 'good',
|
: Promise< {result : 'good',
|
||||||
signedTransaction : string}
|
signedTransaction : string}
|
||||||
@@ -722,8 +741,7 @@ tx_sign
|
|||||||
// we sign <<NetworkId, SerializedObject>>
|
// we sign <<NetworkId, SerializedObject>>
|
||||||
// SerializedObject can either be the object or the hash of the object
|
// SerializedObject can either be the object or the hash of the object
|
||||||
// let's stick with hash for now
|
// let's stick with hash for now
|
||||||
// FIXME: get network id from application state
|
let network_id : Uint8Array = vdk_binary.encode_utf8(netid_cur);
|
||||||
let network_id : Uint8Array = vdk_binary.encode_utf8('gajumaru_devnet');
|
|
||||||
// let tx_hash_bytes : Uint8Array = hash(tx_bytes);
|
// let tx_hash_bytes : Uint8Array = hash(tx_bytes);
|
||||||
let sign_data : Uint8Array = vdk_binary.bytes_concat(network_id, tx_bytes);
|
let sign_data : Uint8Array = vdk_binary.bytes_concat(network_id, tx_bytes);
|
||||||
// @ts-ignore yes nacl is stupid
|
// @ts-ignore yes nacl is stupid
|
||||||
@@ -833,8 +851,10 @@ gb
|
|||||||
browser.runtime.onConnect.addListener(port_talker_toer_lambda);
|
browser.runtime.onConnect.addListener(port_talker_toer_lambda);
|
||||||
|
|
||||||
// does the user want to sign the message
|
// does the user want to sign the message
|
||||||
let confirm_window = await browser.windows.create({url : '../pages/gb.html',
|
let confirm_window = await browser.windows.create({url : '../pages/gb.html',
|
||||||
type : 'popup'});
|
type : 'popup',
|
||||||
|
width : GB_W_PX,
|
||||||
|
height : GB_H_PX});
|
||||||
|
|
||||||
// @ts-ignore shut the fuck up
|
// @ts-ignore shut the fuck up
|
||||||
let tabid : number = confirm_window.tabs[0].id;
|
let tabid : number = confirm_window.tabs[0].id;
|
||||||
@@ -990,6 +1010,7 @@ type bi_popup_calldata
|
|||||||
| bi_pc_p_delete_key
|
| bi_pc_p_delete_key
|
||||||
| bi_pc_p_get_gphrase
|
| bi_pc_p_get_gphrase
|
||||||
| bi_pc_p_gphrase_recover
|
| bi_pc_p_gphrase_recover
|
||||||
|
| bi_pc_p_netid_switch
|
||||||
;
|
;
|
||||||
|
|
||||||
type bi_pc_p_get_data
|
type bi_pc_p_get_data
|
||||||
@@ -1021,6 +1042,10 @@ type bi_pc_p_gphrase_recover
|
|||||||
args : {gphrase : string,
|
args : {gphrase : string,
|
||||||
name : string}};
|
name : string}};
|
||||||
|
|
||||||
|
type bi_pc_p_netid_switch
|
||||||
|
= {method : 'p_netid_switch',
|
||||||
|
args : {new_netid : string}};
|
||||||
|
|
||||||
|
|
||||||
type bi_popup_return_data
|
type bi_popup_return_data
|
||||||
= p_data
|
= p_data
|
||||||
@@ -1058,6 +1083,8 @@ bi_msg_handler_popup
|
|||||||
return await bi_p_get_gphrase(msg.args);
|
return await bi_p_get_gphrase(msg.args);
|
||||||
case 'p_gphrase_recover':
|
case 'p_gphrase_recover':
|
||||||
return await bi_p_gphrase_recover(msg.args);
|
return await bi_p_gphrase_recover(msg.args);
|
||||||
|
case 'p_netid_switch':
|
||||||
|
return await bi_p_netid_switch(msg.args);
|
||||||
default:
|
default:
|
||||||
throw new Error('unsupported popup call method');
|
throw new Error('unsupported popup call method');
|
||||||
}
|
}
|
||||||
@@ -1361,6 +1388,79 @@ bi_p_gphrase_recover
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function
|
||||||
|
bi_p_netid_switch
|
||||||
|
(args: {new_netid : string})
|
||||||
|
: Promise<void>
|
||||||
|
{
|
||||||
|
console.log('b_lib bi_p_netid_switch', {new_netid: args.new_netid});
|
||||||
|
|
||||||
|
// get state
|
||||||
|
let istate : bi_state = await bi_get_state();
|
||||||
|
|
||||||
|
// type bi_state
|
||||||
|
// = {rich_keypairs : Array<bi_rich_keypair>,
|
||||||
|
// netid_cur : bi_netid,
|
||||||
|
// netids_alt : Array<bi_netid>};
|
||||||
|
//
|
||||||
|
// let NETIDS_ALL : Array<bi_netid> = [NETID_GDEV, NETID_AETEST, NETID_AEMAIN];
|
||||||
|
|
||||||
|
// if this is already the network id, do nothing
|
||||||
|
if
|
||||||
|
(istate.netid_cur === args.new_netid)
|
||||||
|
return;
|
||||||
|
// next make sure it's allowed
|
||||||
|
else if
|
||||||
|
(!elem(args.new_netid, NETIDS_ALL))
|
||||||
|
throw new Error('invalid network id: ' + args.new_netid);
|
||||||
|
// ok now we know we're definitely switching to a valid network id
|
||||||
|
else {
|
||||||
|
// set the current network id to the one the user asked for
|
||||||
|
istate.netid_cur = args.new_netid as bi_netid;
|
||||||
|
// new alts are all of them minus the new current one
|
||||||
|
istate.netids_alt = exclude(args.new_netid, NETIDS_ALL) as Array<bi_netid>;
|
||||||
|
|
||||||
|
await bi_set_state(istate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function
|
||||||
|
elem
|
||||||
|
<t>
|
||||||
|
(item : t,
|
||||||
|
list : Array<t>)
|
||||||
|
: boolean
|
||||||
|
{
|
||||||
|
for (let im of list)
|
||||||
|
if (item === im)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function
|
||||||
|
exclude
|
||||||
|
<t>
|
||||||
|
(item : t,
|
||||||
|
list : Array<t>)
|
||||||
|
: Array<t>
|
||||||
|
{
|
||||||
|
// accumulator
|
||||||
|
let new_list : Array<t> = [];
|
||||||
|
|
||||||
|
for (let im of list) {
|
||||||
|
if (im === item)
|
||||||
|
continue;
|
||||||
|
else
|
||||||
|
new_list.push(im);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
+338
-45
@@ -6,6 +6,10 @@ import * as b_lib from './b_lib.js';
|
|||||||
|
|
||||||
p_main();
|
p_main();
|
||||||
|
|
||||||
|
let EXPLORER_URL = 'https://explorer.qpq.swiss';
|
||||||
|
|
||||||
|
let active_account_next_nonce = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* runs whenever the popup opens
|
* runs whenever the popup opens
|
||||||
*
|
*
|
||||||
@@ -33,6 +37,8 @@ p_main
|
|||||||
console.log('popup main');
|
console.log('popup main');
|
||||||
await pi_repop();
|
await pi_repop();
|
||||||
|
|
||||||
|
let pdata = await b_lib.p_get_data();
|
||||||
|
|
||||||
|
|
||||||
// add unique name for default
|
// add unique name for default
|
||||||
let unique_name: string = await b_lib.p_get_unique_name();
|
let unique_name: string = await b_lib.p_get_unique_name();
|
||||||
@@ -42,6 +48,7 @@ p_main
|
|||||||
document.getElementById('faert-name')!.value = unique_name;
|
document.getElementById('faert-name')!.value = unique_name;
|
||||||
|
|
||||||
// add hooks for each button
|
// add hooks for each button
|
||||||
|
// for generating new keypair
|
||||||
let onclick_generate =
|
let onclick_generate =
|
||||||
async function () {
|
async function () {
|
||||||
console.log('popup p_main onclick_generate');
|
console.log('popup p_main onclick_generate');
|
||||||
@@ -69,6 +76,89 @@ p_main
|
|||||||
document
|
document
|
||||||
.getElementById('faert-btn')!
|
.getElementById('faert-btn')!
|
||||||
.addEventListener('click', onclick_gphrase);
|
.addEventListener('click', onclick_gphrase);
|
||||||
|
|
||||||
|
// spend
|
||||||
|
let onclick_spend =
|
||||||
|
async function () {
|
||||||
|
console.log('onclick_spend');
|
||||||
|
// grab recipient address
|
||||||
|
// @ts-ignore fuck off it's my website
|
||||||
|
let recip_address = document.getElementById('spend-recipient').value;
|
||||||
|
// grab recipient amount and parse
|
||||||
|
// @ts-ignore fuck off it's my website
|
||||||
|
let amount = parseInt(document.getElementById('spend-amount').value);
|
||||||
|
let source = active_account(pdata);
|
||||||
|
|
||||||
|
console.log('onclick_spend params', {recipient: recip_address, amount: amount, source: source});
|
||||||
|
|
||||||
|
// hack
|
||||||
|
if ('' === source) {
|
||||||
|
console.log('onclick_spend no active key');
|
||||||
|
// HACK: otherwise populate field with no key
|
||||||
|
let spend_result_p = document.getElementById('spend-result')!;
|
||||||
|
spend_result_p.innerHTML = 'ERROR: no active key';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let maybe_hash = await do_spend(source, recip_address, amount);
|
||||||
|
console.log('onclick_spend maybe_hash', maybe_hash);
|
||||||
|
if (maybe_hash.ok) {
|
||||||
|
// @ts-ignore fuck off
|
||||||
|
let tx_hash = maybe_hash.tx_hash;
|
||||||
|
// @ts-ignore fuck off
|
||||||
|
let spend_result_p = document.getElementById('spend-result')!;
|
||||||
|
let spend_result_a = document.createElement('a')!;
|
||||||
|
spend_result_a.href = EXPLORER_URL + '/transactions/' + tx_hash;
|
||||||
|
spend_result_a.innerHTML = tx_hash;
|
||||||
|
spend_result_p.appendChild(spend_result_a);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// @ts-ignore fuck off
|
||||||
|
let reason = maybe_hash.reason;
|
||||||
|
let spend_result_p = document.getElementById('spend-result')!;
|
||||||
|
spend_result_p.innerHTML = 'ERROR: ' + reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document
|
||||||
|
.getElementById('spend-btn')!
|
||||||
|
.addEventListener('click', onclick_spend);
|
||||||
|
}
|
||||||
|
|
||||||
|
function
|
||||||
|
active_account
|
||||||
|
(pdata: b_lib.p_data)
|
||||||
|
: string
|
||||||
|
{
|
||||||
|
// type p_data
|
||||||
|
// = {rich_pubkeys: Array<p_rich_pubkey>,
|
||||||
|
// netid_cur : string,
|
||||||
|
// netids_alt : Array<string>};
|
||||||
|
// type p_rich_pubkey
|
||||||
|
// = {akstr : string,
|
||||||
|
// active : boolean,
|
||||||
|
// name : string};
|
||||||
|
for (let rkp of pdata.rich_pubkeys) {
|
||||||
|
if (rkp.active)
|
||||||
|
return rkp.akstr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// kill all the children
|
||||||
|
function
|
||||||
|
pfizer
|
||||||
|
(elt : HTMLElement)
|
||||||
|
: void
|
||||||
|
{
|
||||||
|
// from: https://www.geeksforgeeks.org/remove-all-the-child-elements-of-a-dom-node-in-javascript/
|
||||||
|
// kill the last child repeatedly
|
||||||
|
let child = elt.lastElementChild;
|
||||||
|
while (child) {
|
||||||
|
elt.removeChild(child);
|
||||||
|
child = elt.lastElementChild;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -84,26 +174,68 @@ pi_repop
|
|||||||
let popup_data: b_lib.p_data = await b_lib.p_get_data();
|
let popup_data: b_lib.p_data = await b_lib.p_get_data();
|
||||||
console.log('popup pi_repop popup_data', popup_data);
|
console.log('popup pi_repop popup_data', popup_data);
|
||||||
|
|
||||||
|
// network ids
|
||||||
|
// grab the relevent element
|
||||||
|
let netid_form = document.getElementById('netids')!;
|
||||||
|
// eliminate children
|
||||||
|
pfizer(netid_form);
|
||||||
|
|
||||||
|
// add radio button for current network id
|
||||||
|
let netid_cur_input = document.createElement('input');
|
||||||
|
netid_cur_input.setAttribute('type', 'radio');
|
||||||
|
netid_cur_input.setAttribute('value', popup_data.netid_cur);
|
||||||
|
netid_cur_input.setAttribute('id', 'r-' + popup_data.netid_cur);
|
||||||
|
netid_cur_input.checked = true;
|
||||||
|
|
||||||
|
let netid_cur_label = document.createElement('label');
|
||||||
|
netid_cur_label.setAttribute('for', 'r-' + popup_data.netid_cur);
|
||||||
|
netid_cur_label.innerHTML = popup_data.netid_cur;
|
||||||
|
|
||||||
|
// add elements to form
|
||||||
|
netid_form.appendChild(netid_cur_input);
|
||||||
|
netid_form.appendChild(netid_cur_label);
|
||||||
|
|
||||||
|
// add alt ids
|
||||||
|
for (let netid_alt of popup_data.netids_alt) {
|
||||||
|
// add brs here
|
||||||
|
netid_form.appendChild(document.createElement('br'));
|
||||||
|
|
||||||
|
// add radio button for this network id
|
||||||
|
let netid_alt_input = document.createElement('input');
|
||||||
|
netid_alt_input.setAttribute('type', 'radio');
|
||||||
|
netid_alt_input.setAttribute('value', netid_alt);
|
||||||
|
netid_alt_input.setAttribute('id', 'r-' + netid_alt);
|
||||||
|
|
||||||
|
let netid_alt_label = document.createElement('label');
|
||||||
|
netid_alt_label.setAttribute('for', 'r-' + netid_alt);
|
||||||
|
netid_alt_label.innerHTML = netid_alt;
|
||||||
|
|
||||||
|
// add elements to form
|
||||||
|
netid_form.appendChild(netid_alt_input);
|
||||||
|
netid_form.appendChild(netid_alt_label);
|
||||||
|
|
||||||
|
// function to call background script
|
||||||
|
let switchnet =
|
||||||
|
async function() {
|
||||||
|
await b_lib.p_netid_switch(netid_alt);
|
||||||
|
pi_repop();
|
||||||
|
};
|
||||||
|
|
||||||
|
// add netid switch things
|
||||||
|
netid_alt_input.onclick = switchnet;
|
||||||
|
netid_alt_label.onclick = switchnet;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// show keypairs
|
// show keypairs
|
||||||
// step 1: delete the existing list of keypairs
|
// step 1: delete the existing list of keypairs
|
||||||
let kill_all_children =
|
|
||||||
function (elt : HTMLElement) {
|
|
||||||
// from: https://www.geeksforgeeks.org/remove-all-the-child-elements-of-a-dom-node-in-javascript/
|
|
||||||
// kill the last child repeatedly
|
|
||||||
let child = elt.lastElementChild;
|
|
||||||
while (child) {
|
|
||||||
elt.removeChild(child);
|
|
||||||
child = elt.lastElementChild;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// get the ul
|
// get the ul
|
||||||
let pubkeys_ul = document.getElementById('keypairs')!;
|
let pubkeys_ul = document.getElementById('keypairs')!;
|
||||||
// delete the existing list of keypairs
|
// delete the existing list of keypairs
|
||||||
// we will repopulate it in a minute
|
// we will repopulate it in a minute
|
||||||
// this isn't necessary right now but in the future we will add a delete
|
// this isn't necessary right now but in the future we will add a delete
|
||||||
// keypair function so this is prophylaxis
|
// keypair function so this is prophylaxis
|
||||||
kill_all_children(pubkeys_ul);
|
pfizer(pubkeys_ul);
|
||||||
|
|
||||||
// type p_data
|
// type p_data
|
||||||
// = {rich_pubkeys: Array<p_rich_pubkey>};
|
// = {rich_pubkeys: Array<p_rich_pubkey>};
|
||||||
@@ -150,6 +282,49 @@ pi_repop
|
|||||||
// add
|
// add
|
||||||
addr_li.appendChild(addr_a);
|
addr_li.appendChild(addr_a);
|
||||||
|
|
||||||
|
// balance
|
||||||
|
let bal_li = document.createElement('li');
|
||||||
|
bal_li.innerHTML += 'Balance: ';
|
||||||
|
// old school async/await
|
||||||
|
pi_balance(this_rkp.akstr).then(
|
||||||
|
function(maybe_balance) {
|
||||||
|
// if there is a balance, show it in green
|
||||||
|
// also make nonce nonce if account is active
|
||||||
|
if (maybe_balance.ok)
|
||||||
|
{
|
||||||
|
if (this_rkp.active) {
|
||||||
|
// @ts-ignore fuck off i know what i'm doing
|
||||||
|
let nonce = maybe_balance.nonce;
|
||||||
|
active_account_next_nonce = nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let greentext = document.createElement('span');
|
||||||
|
greentext.classList.add('balance-found');
|
||||||
|
// @ts-ignore fuck off i know what i'm doing
|
||||||
|
greentext.innerHTML = '' + maybe_balance.balance;
|
||||||
|
|
||||||
|
bal_li.appendChild(greentext);
|
||||||
|
}
|
||||||
|
// if not, show reason and make it red
|
||||||
|
// also make nonce 0 if account is active
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (this_rkp.active) {
|
||||||
|
active_account_next_nonce = 0;
|
||||||
|
}
|
||||||
|
let redtext = document.createElement('span');
|
||||||
|
redtext.classList.add('balance-not-found');
|
||||||
|
// @ts-ignore fuck off i know what i'm doing
|
||||||
|
redtext.innerHTML = '' + maybe_balance.reason;
|
||||||
|
|
||||||
|
bal_li.appendChild(redtext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// add delete button
|
// add delete button
|
||||||
let del_li = document.createElement('li');
|
let del_li = document.createElement('li');
|
||||||
|
|
||||||
@@ -187,19 +362,20 @@ pi_repop
|
|||||||
|
|
||||||
|
|
||||||
// add everything inside-out
|
// add everything inside-out
|
||||||
rkp_ul.appendChild(addr_li);
|
|
||||||
rkp_ul.appendChild(name_li);
|
rkp_ul.appendChild(name_li);
|
||||||
rkp_ul.appendChild(del_li);
|
rkp_ul.appendChild(addr_li);
|
||||||
|
rkp_ul.appendChild(bal_li);
|
||||||
rkp_ul.appendChild(gphrase_li);
|
rkp_ul.appendChild(gphrase_li);
|
||||||
|
rkp_ul.appendChild(del_li);
|
||||||
this_li.appendChild(rkp_ul);
|
this_li.appendChild(rkp_ul);
|
||||||
|
|
||||||
|
|
||||||
// if active, make background green
|
// if active, make background "active"
|
||||||
if (this_rkp.active)
|
if (this_rkp.active)
|
||||||
this_li.style.background = 'green';
|
this_li.classList.add('account-active');
|
||||||
// otherwise make background red and add activate li
|
// otherwise make background "inactive" and add activate li
|
||||||
else {
|
else {
|
||||||
this_li.style.background = 'red';
|
this_li.classList.add('account-inactive');
|
||||||
let activate_li = document.createElement('li');
|
let activate_li = document.createElement('li');
|
||||||
let activate_a = document.createElement('a');
|
let activate_a = document.createElement('a');
|
||||||
activate_a.innerHTML = 'Switch to this key';
|
activate_a.innerHTML = 'Switch to this key';
|
||||||
@@ -223,36 +399,116 @@ pi_repop
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shorten the ak_... str
|
|
||||||
*/
|
|
||||||
function
|
|
||||||
pi_shakstr
|
|
||||||
(akstr : string)
|
|
||||||
: string
|
|
||||||
{
|
|
||||||
console.log('popup shakstr');
|
|
||||||
return akstr.slice(0, 10) + '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get account balance of ak_... str
|
|
||||||
*/
|
|
||||||
async function
|
async function
|
||||||
pi_balance
|
do_spend
|
||||||
(akstr : string)
|
(source : string,
|
||||||
|
recipient : string,
|
||||||
|
amount : number)
|
||||||
: Promise< {ok : true,
|
: Promise< {ok : true,
|
||||||
balance : number}
|
tx_hash : string}
|
||||||
| {ok : false,
|
| {ok : false,
|
||||||
reason : string}
|
reason : string}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
let endpoint_init_url = 'http://demonet.qpq.swiss:3013';
|
console.log('popup do_spend', {source: source, recipient: recipient, amount: amount});
|
||||||
let this_endpoint = endpoint_init_url + '/accounts/' + akstr;
|
let maybe_spendtx = await mk_spend(source, recipient, amount);
|
||||||
let input_params = {pubkey: akstr};
|
if (maybe_spendtx.ok) {
|
||||||
|
return await post_spend(maybe_spendtx.tx);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// @ts-ignore fuck off
|
||||||
|
return maybe_spendtx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function
|
||||||
|
post_spend
|
||||||
|
(tx: string)
|
||||||
|
: Promise< {ok : true,
|
||||||
|
tx_hash : string}
|
||||||
|
| {ok : false,
|
||||||
|
reason : string}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
console.log('popup post_spend', tx);
|
||||||
|
let req_obj = {tx: tx};
|
||||||
|
let req_body = JSON.stringify(req_obj, undefined, 4);
|
||||||
|
let endpoint_init_url = 'https://demonet.qpq.swiss/v2';
|
||||||
|
let this_endpoint = endpoint_init_url + '/transactions';
|
||||||
|
// fetch: https://developer.mozilla.org/en-US/docs/Web/API/fetch
|
||||||
|
let response = await fetch(this_endpoint, {method: 'POST', body: req_body});
|
||||||
|
let return_data = await response.json();
|
||||||
|
console.log('popup post_spend return data', return_data);
|
||||||
|
// 200 = ok
|
||||||
|
// 400 = bad tx
|
||||||
|
switch (response.status) {
|
||||||
|
case 200:
|
||||||
|
// get the hash
|
||||||
|
// @ts-ignore talking to foreign api
|
||||||
|
return {ok: true, tx_hash: return_data.tx_hash};
|
||||||
|
// wtf
|
||||||
|
default:
|
||||||
|
// @ts-ignore talking to foreign api
|
||||||
|
return {ok: false, reason: return_data.reason};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let MIN_FEE = 17000;
|
||||||
|
|
||||||
|
async function
|
||||||
|
mk_spend
|
||||||
|
(source : string,
|
||||||
|
recipient : string,
|
||||||
|
amount : number)
|
||||||
|
: Promise< {ok : true,
|
||||||
|
tx : string}
|
||||||
|
| {ok : false,
|
||||||
|
reason : string}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
console.log('popup mk_spend', {source: source, recipient: recipient, amount: amount});
|
||||||
|
let nonce: number = active_account_next_nonce;
|
||||||
|
let req_obj = {recipient_id : recipient,
|
||||||
|
amount : amount,
|
||||||
|
fee : MIN_FEE,
|
||||||
|
ttl : 0,
|
||||||
|
sender_id : source,
|
||||||
|
nonce : nonce,
|
||||||
|
payload : ''};
|
||||||
|
console.log('popup mk_spend req_obj', req_obj);
|
||||||
|
let req_body = JSON.stringify(req_obj, undefined, 4);
|
||||||
|
console.log('popup mk_spend req_body', req_body);
|
||||||
|
|
||||||
|
let endpoint_init_url = 'https://demonet.qpq.swiss/v2';
|
||||||
|
let this_endpoint = endpoint_init_url + '/debug/transactions/spend';
|
||||||
|
|
||||||
|
// fetch: https://developer.mozilla.org/en-US/docs/Web/API/fetch
|
||||||
|
let response = await fetch(this_endpoint, {method: 'POST', body: req_body, headers: [['Content-Type', 'application/json']]});
|
||||||
|
let return_data = await response.json();
|
||||||
|
console.log('popup mk_spend return data', return_data);
|
||||||
|
|
||||||
|
// 200 = ok
|
||||||
|
// 400 = pubkey malformed
|
||||||
|
// 404 = account dne
|
||||||
|
switch (response.status) {
|
||||||
|
case 200:
|
||||||
|
// get the nonce
|
||||||
|
// @ts-ignore talking to foreign api
|
||||||
|
return {ok: true, tx: return_data.tx};
|
||||||
|
// wtf
|
||||||
|
default:
|
||||||
|
// @ts-ignore talking to foreign api
|
||||||
|
return {ok: false, reason: return_data.reason};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function
|
||||||
|
get_nonce
|
||||||
|
(source: string)
|
||||||
|
: Promise<number>
|
||||||
|
{
|
||||||
|
console.log('popup get_nonce', source);
|
||||||
|
let endpoint_init_url = 'https://demonet.qpq.swiss/v2';
|
||||||
|
let this_endpoint = endpoint_init_url + '/accounts/' + source;
|
||||||
// fetch: https://developer.mozilla.org/en-US/docs/Web/API/fetch
|
// fetch: https://developer.mozilla.org/en-US/docs/Web/API/fetch
|
||||||
let response = await fetch(this_endpoint);
|
let response = await fetch(this_endpoint);
|
||||||
// 200 = ok
|
// 200 = ok
|
||||||
@@ -261,12 +517,48 @@ pi_balance
|
|||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 200:
|
case 200:
|
||||||
// unstringify the body
|
// unstringify the body
|
||||||
let return_data = response.json();
|
let return_data = await response.json();
|
||||||
|
console.log('popup get_nonce return data', return_data);
|
||||||
|
// get the nonce
|
||||||
|
// @ts-ignore talking to foreign api
|
||||||
|
return return_data.nonce;
|
||||||
|
// wtf
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get account balance of ak_... str
|
||||||
|
*/
|
||||||
|
async function
|
||||||
|
pi_balance
|
||||||
|
(akstr : string)
|
||||||
|
: Promise< {ok : true,
|
||||||
|
balance : string,
|
||||||
|
nonce : number}
|
||||||
|
| {ok : false,
|
||||||
|
reason : string}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
let endpoint_init_url = 'https://demonet.qpq.swiss/v2';
|
||||||
|
let this_endpoint = endpoint_init_url + '/accounts/' + akstr;
|
||||||
|
// fetch: https://developer.mozilla.org/en-US/docs/Web/API/fetch
|
||||||
|
let response = await fetch(this_endpoint);
|
||||||
|
// 200 = ok
|
||||||
|
// 400 = pubkey malformed
|
||||||
|
// 404 = account dne
|
||||||
|
switch (response.status) {
|
||||||
|
case 200:
|
||||||
|
// unstringify the body
|
||||||
|
let return_data = await response.json();
|
||||||
|
console.log('return data', return_data);
|
||||||
// get the balance
|
// get the balance
|
||||||
// @ts-ignore talking to foreign api
|
// @ts-ignore talking to foreign api
|
||||||
let balance = return_data.balance;
|
let balance = return_data.balance.toString();
|
||||||
return {ok : true,
|
return {ok : true,
|
||||||
balance : balance};
|
balance : balance,
|
||||||
|
nonce : return_data.nonce};
|
||||||
// pubkey malformed
|
// pubkey malformed
|
||||||
case 400:
|
case 400:
|
||||||
return {ok : false,
|
return {ok : false,
|
||||||
@@ -274,10 +566,11 @@ pi_balance
|
|||||||
// account dne
|
// account dne
|
||||||
case 404:
|
case 404:
|
||||||
return {ok : false,
|
return {ok : false,
|
||||||
reason : 'Account DNE'};
|
reason : 'Account does not exist yet'};
|
||||||
// wtf
|
// wtf
|
||||||
default:
|
default:
|
||||||
return {ok : false,
|
return {ok : false,
|
||||||
reason : 'Unexpected response from node'};
|
reason : 'Unexpected response from node'};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user