This commit is contained in:
npub1smartg7jdu7q7vew86qjggq90ncwdnue03d7r32gqe4qn335mtlqpqs7zu 2024-07-10 22:21:29 +02:00
commit 01fcbbf3fc
No known key found for this signature in database
13 changed files with 649 additions and 0 deletions

1
.txo/txo.txt Normal file
View File

@ -0,0 +1 @@
txo:tbtc4:e0a84ec838712ddec78c57a7dc1ce65b777cbf58bfa861f289e3533a60399493:0 10000

11
bin/post.sh Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
{}

3
contract/state.json Normal file
View File

@ -0,0 +1,3 @@
{
"hue": 130
}

239
index.html Normal file
View 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
View File

@ -0,0 +1 @@
{}

1
nostr.json Normal file
View 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"}