genesis
This commit is contained in:
commit
01fcbbf3fc
1
.txo/txo.txt
Normal file
1
.txo/txo.txt
Normal file
|
@ -0,0 +1 @@
|
|||
txo:tbtc4:e0a84ec838712ddec78c57a7dc1ce65b777cbf58bfa861f289e3533a60399493:0 10000
|
11
bin/post.sh
Executable file
11
bin/post.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
HASH=$(rad inspect)
|
||||
|
||||
cd /home/melvin/npub.info/pages/multi-hash/"${HASH:4}" || exit
|
||||
|
||||
git pull
|
||||
|
||||
cd - || exit
|
||||
|
||||
/home/melvin/go/bin/nak event -c "deployed" --kind 613 --sec $(git config nostr.privkey) wss://npub.info/ &
|
28
bin/u2.sh
Executable file
28
bin/u2.sh
Executable file
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
PUBKEY=$(cat nostr.json | jq -r .pubkey)
|
||||
|
||||
echo "[\"REQ\", \"x\", { \"kinds\" : [611], \"limit\" : 0, \"#p\" : [\"${PUBKEY}\"] }]" | websocat -n --ping-interval 20 wss://npub.info | while read -r line; do
|
||||
# Process each line received here
|
||||
echo "Received: $line"
|
||||
|
||||
echo "$line" | jq
|
||||
|
||||
TYPE=$(echo "$line" | jq -r ".[0]")
|
||||
|
||||
echo "type: --${TYPE}--"
|
||||
|
||||
if [ "$TYPE" = "EVENT" ]
|
||||
then
|
||||
echo event
|
||||
bash -x bin/validate.sh
|
||||
|
||||
time bash -x bin/post.sh
|
||||
|
||||
# /home/melvin/go/bin/nak event -c "deployed" --kind 612 --sec $(git config nostr.privkey) wss://npub.info/ &
|
||||
|
||||
fi
|
||||
|
||||
|
||||
done
|
||||
|
49
bin/validate.sh
Executable file
49
bin/validate.sh
Executable file
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
EVENT=$(echo '["REQ", "x", { "kinds": [611] }]' | websocat --ping-interval 20 wss://npub.info | tail -2 | head -1)
|
||||
|
||||
ID=$(echo "$EVENT" | jq -r .[2].id)
|
||||
|
||||
echo "$ID"
|
||||
|
||||
CONTENT=$(echo "$EVENT" | jq -r .[2].content)
|
||||
|
||||
echo "$CONTENT"
|
||||
|
||||
echo "$EVENT" | jq -r .[2] > ./contract/calls/"$ID".json
|
||||
|
||||
PROOF=$(echo "$EVENT" | jq -r .[2].tags[0][1])
|
||||
|
||||
echo "$PROOF"
|
||||
|
||||
# check unspent
|
||||
TX=$(echo "$PROOF" | cut -d : -f 5)
|
||||
VOUT=$(echo "$PROOF" | cut -d : -f 6)
|
||||
|
||||
# check keys
|
||||
SPENT=$(curl -sSL "https://mempool.space/testnet4/api/tx/$TX/outspend/${VOUT}" | jq .spent)
|
||||
|
||||
echo SPENT "$SPENT"
|
||||
|
||||
if [ "$SPENT" = 'false' ]
|
||||
then
|
||||
|
||||
/home/melvin/go/bin/nak event -c "accepted" --kind 612 --sec $(git config nostr.privkey) wss://npub.info/ &
|
||||
|
||||
contract/contract.js "$CONTENT"
|
||||
|
||||
~/bin/gitmark/bin/gitmark.sh call-"$ID"
|
||||
|
||||
/home/melvin/go/bin/nak event -c "marked" --kind 612 --sec $(git config nostr.privkey) wss://npub.info/ &
|
||||
|
||||
|
||||
# TODO: spend it
|
||||
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# jq ".hue = $CONTENT" contract/state.json > .git/state
|
||||
# mv .git/state contract/state.json
|
||||
|
||||
|
216
call.html
Normal file
216
call.html
Normal file
|
@ -0,0 +1,216 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="refresh" content="3600" />
|
||||
<title>Ledgr</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
h2 {
|
||||
color: #3498db;
|
||||
}
|
||||
input {
|
||||
width: 95%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
textarea {
|
||||
width: 95%;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" id="root"></div>
|
||||
|
||||
<script type="module">
|
||||
import {
|
||||
html,
|
||||
render,
|
||||
Component
|
||||
} from 'https://unpkg.com/htm/preact/standalone.module.js'
|
||||
|
||||
class App extends Component {
|
||||
constructor() {
|
||||
super()
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const token = params.get('token')
|
||||
const status = ['Click Submit to call contract']
|
||||
if (token) {
|
||||
this.state = { fileContent: token, hue: '', status }
|
||||
}
|
||||
this.state = { fileContent: token, hue: '', status }
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
var file = './nostr.json'
|
||||
var response = await fetch(file) // Fetch the file
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch text file ${file}: ${response.statusText}`
|
||||
)
|
||||
}
|
||||
var txt = await response.text()
|
||||
this.setState({ nostrContent: txt }) // Update state with file content
|
||||
}
|
||||
|
||||
handleTextareaChange = event => {
|
||||
this.setState({ fileContent: event.target.value })
|
||||
}
|
||||
|
||||
handleHueChange = event => {
|
||||
this.setState({ hue: event.target.value })
|
||||
}
|
||||
|
||||
handleButtonClick = async () => {
|
||||
const { fileContent, hue, status, nostrContent } = this.state
|
||||
|
||||
if (!fileContent || !hue) {
|
||||
alert('Both fields must be filled out!')
|
||||
return
|
||||
}
|
||||
|
||||
status.push('Processing')
|
||||
this.setState(status)
|
||||
|
||||
// Establish WebSocket connection and send the event
|
||||
const relay = 'wss://npub.info'
|
||||
const ws = new WebSocket(relay)
|
||||
|
||||
status.push('Opening socket to ' + relay)
|
||||
this.setState(status)
|
||||
|
||||
ws.onopen = async () => {
|
||||
status.push('connected to ' + relay)
|
||||
this.setState(status)
|
||||
const message = JSON.stringify({
|
||||
content: 'sethue',
|
||||
data: { fileContent, hue }
|
||||
})
|
||||
var nostr = JSON.parse(nostrContent)
|
||||
var ev = await window.nostr.signEvent({
|
||||
kind: 611,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['proof', fileContent],
|
||||
['p', nostr?.pubkey]
|
||||
],
|
||||
content: hue
|
||||
})
|
||||
|
||||
var req = JSON.stringify(['EVENT', ev])
|
||||
ws.send(req)
|
||||
console.log('Message sent:', req)
|
||||
status.push('message sent ' + req)
|
||||
this.setState(status)
|
||||
|
||||
let now = new Date().getTime()
|
||||
now = Math.floor(now / 1000.0)
|
||||
let subscribe = `["REQ", "tail", {"kinds": [612, 613], "since": ${now} }]`
|
||||
// if (qs.pubkey) {
|
||||
// subscribe = `["REQ", "tail", { "authors": ["${qs.pubkey}"], "since": ${now} }]`
|
||||
// }
|
||||
console.log(subscribe)
|
||||
status.push('subscribe ' + subscribe)
|
||||
this.setState(status)
|
||||
ws.send(subscribe)
|
||||
|
||||
// ws.close()
|
||||
}
|
||||
|
||||
ws.onmessage = async event => {
|
||||
const json = JSON.parse(event?.data)
|
||||
if (json[0] === 'EOSE') {
|
||||
console.log('EOSE')
|
||||
return
|
||||
}
|
||||
if (event) {
|
||||
status.push(event?.data)
|
||||
this.setState(status)
|
||||
console.log('event', event)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = error => {
|
||||
console.error('WebSocket Error:', error)
|
||||
}
|
||||
|
||||
// Additional actions can be taken here if needed
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
<h2>Enter Ecash Token</h2>
|
||||
<textarea
|
||||
value=${this.state.fileContent}
|
||||
onInput=${this.handleTextareaChange}
|
||||
></textarea>
|
||||
|
||||
<h2>Sethue</h2>
|
||||
<input
|
||||
type="number"
|
||||
value=${this.state.hue}
|
||||
onInput=${this.handleHueChange}
|
||||
placeholder="Enter hue value (0-360)"
|
||||
/>
|
||||
|
||||
<button onClick=${this.handleButtonClick}>Submit</button>
|
||||
|
||||
<pre>${this.state.stateContent}</pre>
|
||||
|
||||
<h2>Status</h2>
|
||||
|
||||
<pre>
|
||||
|
||||
|
||||
${this.state.status
|
||||
.map(status =>
|
||||
typeof status === 'object' ? JSON.stringify(status) : status
|
||||
)
|
||||
.map(status => html`${status}<br />`)}
|
||||
|
||||
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
render(html`<${App} />`, document.getElementById('root'))
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
3
contract/README.md
Normal file
3
contract/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
`sethue` allows you to set a value between 0 and 360 that will be used as the "hue" of the background-color of this website in an HSL formula.
|
||||
|
||||
Be sure to pay at least 1000 sat as the price for having this amazing color-changing opportunity!
|
76
contract/contract.js
Executable file
76
contract/contract.js
Executable file
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
class SmartContract {
|
||||
constructor() {
|
||||
this.stateFilePath = path.resolve('./contract/state.json')
|
||||
this.state = { hue: 0 }
|
||||
this.loadState()
|
||||
}
|
||||
|
||||
async loadState() {
|
||||
try {
|
||||
const data = await fs.readFile(this.stateFilePath, 'utf-8')
|
||||
this.state = JSON.parse(data)
|
||||
} catch (error) {
|
||||
console.error('Error loading state:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async saveState() {
|
||||
try {
|
||||
await fs.writeFile(this.stateFilePath, JSON.stringify(this.state, null, 2))
|
||||
} catch (error) {
|
||||
console.error('Error saving state:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async setHue(call) {
|
||||
if (call.msatoshi < 1000000) {
|
||||
throw new Error('pay at least 1000 sats!')
|
||||
}
|
||||
|
||||
if (typeof call.payload.hue !== 'number') {
|
||||
throw new Error('hue is not a number!')
|
||||
}
|
||||
|
||||
if (call.payload.hue < 0 || call.payload.hue > 360) {
|
||||
throw new Error('hue is out of the 0~360 range!')
|
||||
}
|
||||
|
||||
this.state.hue = call.payload.hue
|
||||
await this.saveState()
|
||||
|
||||
this.send('86fa35fef8ab4f5906cedfcd966a69e0126973097fd4ea270ddefc505a1a824e', call.msatoshi)
|
||||
}
|
||||
|
||||
send(address, msatoshi) {
|
||||
console.log(`Sending ${msatoshi} msatoshi to ${address}`)
|
||||
// Here you would implement the actual logic to transfer the msatoshi
|
||||
}
|
||||
}
|
||||
|
||||
export default SmartContract
|
||||
|
||||
let hue = process.argv[2] || 99
|
||||
hue = parseInt(hue)
|
||||
|
||||
const contract = new SmartContract()
|
||||
|
||||
const call = {
|
||||
msatoshi: 1500000, // Example value
|
||||
payload: {
|
||||
hue: hue // Example value within the valid range
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await contract.setHue(call)
|
||||
console.log('Hue set successfully:', contract.state.hue)
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
}
|
||||
})()
|
20
contract/contract.lua
Normal file
20
contract/contract.lua
Normal file
|
@ -0,0 +1,20 @@
|
|||
function __init__ ()
|
||||
return {hue=0}
|
||||
end
|
||||
|
||||
function sethue ()
|
||||
if call.msatoshi < 1000000 then
|
||||
error('pay at least 1000 sats!')
|
||||
end
|
||||
|
||||
if type(call.payload.hue) ~= 'number' then
|
||||
error('hue is not a number!')
|
||||
end
|
||||
|
||||
if call.payload.hue < 0 or call.payload.hue > 360 then
|
||||
error('hue is out of the 0~360 range!')
|
||||
end
|
||||
|
||||
contract.state.hue = call.payload.hue
|
||||
contract.send('86fa35a3d26f3c0f332e3e812420057cf0e6cf997c5be1c548066a09c634dafe', call.msatoshi)
|
||||
end
|
1
contract/ledgr.json
Normal file
1
contract/ledgr.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
3
contract/state.json
Normal file
3
contract/state.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"hue": 130
|
||||
}
|
239
index.html
Normal file
239
index.html
Normal file
|
@ -0,0 +1,239 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<!-- Refresh every hour (3600 seconds) -->
|
||||
<meta http-equiv="refresh" content="3600" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<title>Ledgr</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="module">
|
||||
import {
|
||||
html, // Import html from htm instead of h
|
||||
render,
|
||||
Component
|
||||
} from 'https://unpkg.com/htm/preact/standalone.module.js'
|
||||
|
||||
import 'https://cdn.skypack.dev/nostrefresh'
|
||||
|
||||
class App extends Component {
|
||||
constructor() {
|
||||
super()
|
||||
this.state = { fileContent: '', readmeContent: '' } // Initialize state to store file content
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
try {
|
||||
var response = await fetch('./.txo/txo.txt') // Fetch the file
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch text file: ${response.statusText}`
|
||||
)
|
||||
}
|
||||
var txt = await response.text()
|
||||
this.setState({ fileContent: txt }) // Update state with file content
|
||||
|
||||
var file = './nostr.json'
|
||||
var response = await fetch(file) // Fetch the file
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch text file ${file}: ${response.statusText}`
|
||||
)
|
||||
}
|
||||
var txt = await response.text()
|
||||
this.setState({ nostrContent: txt }) // Update state with file content
|
||||
|
||||
response = await fetch('./contract/ledgr.json') // Fetch the file
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch text file: ${response.statusText}`
|
||||
)
|
||||
}
|
||||
txt = await response.text()
|
||||
this.setState({ ledgrContent: txt }) // Update state with file content
|
||||
|
||||
response = await fetch('./contract/state.json') // Fetch the file
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch text file: ${response.statusText}`
|
||||
)
|
||||
}
|
||||
txt = await response.text()
|
||||
this.setState({ stateContent: txt }) // Update state with file content
|
||||
|
||||
response = await fetch('./contract/README.md') // Fetch the file
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch text file: ${response.statusText}`
|
||||
)
|
||||
}
|
||||
txt = await response.text()
|
||||
this.setState({ readmeContent: txt }) // Update state with file content
|
||||
} catch (error) {
|
||||
console.error('Error fetching file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var lines = this.state.fileContent.split('\n')
|
||||
// Use the html function for creating elements
|
||||
var assets = 0
|
||||
var ledgr
|
||||
var nostr
|
||||
var chain = 'tbtc4'
|
||||
console.log('ledgr', this.state.ledgrContent)
|
||||
if (this.state.ledgrContent) {
|
||||
ledgr = JSON.parse(this.state.ledgrContent)
|
||||
ledgr = Object.entries(ledgr)
|
||||
}
|
||||
if (this.state.nostrContent) {
|
||||
nostr = JSON.parse(this.state.nostrContent)
|
||||
}
|
||||
if (this?.state?.stateContent) {
|
||||
var hue = JSON.parse(this.state.stateContent)
|
||||
console.log('hue', hue.hue)
|
||||
document.body.style.backgroundColor = `hsl(${hue.hue}, 66%, 89%)`
|
||||
}
|
||||
console.log('ledgr', ledgr)
|
||||
var href
|
||||
return html`
|
||||
<div>
|
||||
<h2>README</h2>
|
||||
<pre>${this.state.readmeContent}</pre>
|
||||
|
||||
<h2>State</h2>
|
||||
|
||||
<pre>
|
||||
${this.state.stateContent}
|
||||
</pre
|
||||
>
|
||||
|
||||
<h2>Contract</h2>
|
||||
|
||||
<a target="_blank" href="./contract/contract.js">view source</a>
|
||||
<br />
|
||||
<a target="_blank" href="./call.html">call contract</a>
|
||||
|
||||
<h2>Nostr</h2>
|
||||
|
||||
Pubkey: ${nostr?.pubkey}
|
||||
|
||||
<h2>Proofs</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Transaction</th>
|
||||
<th>Reserves</th>
|
||||
<th>Proof</th>
|
||||
<th>Verified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${lines.map((l, i) => {
|
||||
var double = l.split(' ')
|
||||
console.log(double)
|
||||
if (double.length !== 2) return
|
||||
chain = double[0].split(':')[1]
|
||||
assets = double[1]
|
||||
if (chain === 'tbtc4') {
|
||||
href =
|
||||
'https://mempool.space/testnet4/tx/' +
|
||||
double[0].split(':')[2]
|
||||
}
|
||||
if (chain === 'vtc') {
|
||||
href =
|
||||
'https://vtc5.trezor.io/tx/' + double[0].split(':')[2]
|
||||
}
|
||||
return html`<tr>
|
||||
<td>
|
||||
<a target="_blank" href="${href}">${double[0]}</a>
|
||||
</td>
|
||||
<td>${assets}</td>
|
||||
<td><a href="${href}">Proof</a></td>
|
||||
<td>✅</td>
|
||||
</tr>`
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 style="display:none">Reserves</h2>
|
||||
<table style="display:none">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Reserves</th>
|
||||
<th>Chain</th>
|
||||
<th>Proof</th>
|
||||
<th>Verified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>${assets}</td>
|
||||
<td>${chain}</td>
|
||||
<td><a href="${href}">Proof</a></td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 style="display:none">Ledgr</h2>
|
||||
|
||||
<table style="display:none">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Amount</th>
|
||||
<th>Proof</th>
|
||||
<th>Verified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${ledgr?.map((i, l) => {
|
||||
return html`<tr>
|
||||
<td>${i[0]}</td>
|
||||
<td>${i[1]}</td>
|
||||
<td><a href="${href}">Proof</a></td>
|
||||
<td>✅</td>
|
||||
</tr>`
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
render(html`<${App} />`, document.getElementById('root'))
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
1
ledgr.json
Normal file
1
ledgr.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
nostr.json
Normal file
1
nostr.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"id":"100c04247b3881f4c80615c80abb426ed27f803aa7efef39ae9ff9f8970c63a7","pubkey":"86fa35a3d26f3c0f332e3e812420057cf0e6cf997c5be1c548066a09c634dafe","created_at":1720642724,"kind":30617,"tags":[["d","sethue"],["relays","wss://npub.info/"],["c","txo:tbtc4:e0a84ec838712ddec78c57a7dc1ce65b777cbf58bfa861f289e3533a60399493:0"],["clone","http://git.melvincarvalho.com/smart/32.git"]],"content":"","sig":"0fb78e651f8ebe33a864dd7cf6e0de851220da4de062334e654564ef9db7c5235df7a9f95188769e2a4ec4047c71f6c28cdef0049684f631ac894ecfb8f31081"}
|
Loading…
Reference in New Issue
Block a user