Compare commits

..

No commits in common. "772e73f48d9ea8ec3439f0d6f79b6e49c62ceec9" and "54fedbc528651d78d065c7ef3fc4292eeea43beb" have entirely different histories.

23 changed files with 793 additions and 700 deletions

View File

@ -1,7 +1,13 @@
{ {
"trailingComma": "none", "singleQuote": true,
"tabWidth": 2, "trailingComma": "all",
"semi": false, "tabWidth": 4,
"singleQuote": true, "overrides": [
"endOfLine": "auto" {
"files": ["*.ts", "*.mts"],
"options": {
"parser": "typescript"
}
}
]
} }

213
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nostr-dev-kit/ndk": "^0.8.1", "@nostr-dev-kit/ndk": "^0.7.5",
"axios": "^1.4.0", "axios": "^1.4.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"file-type": "^18.5.0", "file-type": "^18.5.0",
@ -29,7 +29,7 @@
"eslint-config-prettier": "~8.8", "eslint-config-prettier": "~8.8",
"eslint-plugin-jest": "~27.2", "eslint-plugin-jest": "~27.2",
"jest": "~29.5", "jest": "~29.5",
"prettier": "3.2.5", "prettier": "~2.8",
"rimraf": "~5.0", "rimraf": "~5.0",
"ts-api-utils": "~0.0.44", "ts-api-utils": "~0.0.44",
"ts-jest": "~29.1", "ts-jest": "~29.1",
@ -1644,9 +1644,9 @@
} }
}, },
"node_modules/@nostr-dev-kit/ndk": { "node_modules/@nostr-dev-kit/ndk": {
"version": "0.8.23", "version": "0.7.7",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.8.23.tgz", "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.7.7.tgz",
"integrity": "sha512-wX/9Cl02gCR0Kz25C/1xxGO47K13Ve1x8IISbkF/M3RTTXftYBvzB7bL8qwLaFaeqb02cMU0YVL+oKDrYzH/Ng==", "integrity": "sha512-IRTW16q40zzuSBkpYpDUcZJRrbw26JTeicfZN6O/2Gw7D2w6Pe42VqFwpbcP9xOnFPEGP2eNV6SwXQ3y0tjBtw==",
"dependencies": { "dependencies": {
"@noble/hashes": "^1.3.1", "@noble/hashes": "^1.3.1",
"@noble/secp256k1": "^2.0.0", "@noble/secp256k1": "^2.0.0",
@ -1663,9 +1663,10 @@
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"light-bolt11-decoder": "^3.0.0", "light-bolt11-decoder": "^3.0.0",
"node-fetch": "^3.3.1", "node-fetch": "^3.3.1",
"nostr-tools": "^1.14.0", "nostr-tools": "^1.11.2",
"tsd": "^0.28.1", "tsd": "^0.28.1",
"utf8-buffer": "^1.0.0" "utf8-buffer": "^1.0.0",
"websocket-polyfill": "^0.0.3"
} }
}, },
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
@ -2551,6 +2552,18 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true "dev": true
}, },
"node_modules/bufferutil": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@ -2779,6 +2792,18 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"dependencies": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/data-uri-to-buffer": { "node_modules/data-uri-to-buffer": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@ -3152,6 +3177,43 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
"esniff": "^2.0.1",
"next-tick": "^1.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"dependencies": {
"d": "^1.0.2",
"ext": "^1.7.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.17.19", "version": "0.17.19",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
@ -3499,6 +3561,20 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"dependencies": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/espree": { "node_modules/espree": {
"version": "9.6.1", "version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@ -3582,6 +3658,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"dependencies": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
@ -3635,6 +3720,14 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"dependencies": {
"type": "^2.7.2"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -4609,6 +4702,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
},
"node_modules/is-unicode-supported": { "node_modules/is-unicode-supported": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@ -5786,6 +5884,11 @@
"resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="
}, },
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
},
"node_modules/nice-try": { "node_modules/nice-try": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -5826,6 +5929,16 @@
"url": "https://opencollective.com/node-fetch" "url": "https://opencollective.com/node-fetch"
} }
}, },
"node_modules/node-gyp-build": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
"integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-int64": { "node_modules/node-int64": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@ -6465,15 +6578,15 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.2.5", "version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true, "dev": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin-prettier.js"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=10.13.0"
}, },
"funding": { "funding": {
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
@ -7546,6 +7659,11 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
"integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==" "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w=="
}, },
"node_modules/tstl": {
"version": "2.5.16",
"resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.16.tgz",
"integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw=="
},
"node_modules/tsutils": { "node_modules/tsutils": {
"version": "3.21.0", "version": "3.21.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
@ -7565,6 +7683,11 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}, },
"node_modules/type": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
"integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -7665,6 +7788,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"dependencies": {
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
@ -7735,6 +7866,18 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/utf8-buffer": { "node_modules/utf8-buffer": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/utf8-buffer/-/utf8-buffer-1.0.0.tgz", "resolved": "https://registry.npmjs.org/utf8-buffer/-/utf8-buffer-1.0.0.tgz",
@ -7788,6 +7931,44 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/websocket": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz",
"integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==",
"dependencies": {
"bufferutil": "^4.0.1",
"debug": "^2.2.0",
"es5-ext": "^0.10.50",
"typedarray-to-buffer": "^3.1.5",
"utf-8-validate": "^5.0.2",
"yaeti": "^0.0.6"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/websocket-polyfill": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/websocket-polyfill/-/websocket-polyfill-0.0.3.tgz",
"integrity": "sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg==",
"dependencies": {
"tstl": "^2.0.7",
"websocket": "^1.0.28"
}
},
"node_modules/websocket/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/websocket/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -7905,6 +8086,14 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==",
"engines": {
"node": ">=0.10.32"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -1,60 +1,58 @@
{ {
"name": "data-vending-machine-skeleton", "name": "data-vending-machine-skeleton",
"version": "0.1.0", "version": "0.1.0",
"description": "nostr data vending machine skeleton", "description": "nostr data vending machine skeleton",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "~19" "node": "~19"
}, },
"keywords": [ "keywords": [
"nostr", "nostr",
"ai", "ai",
"bitcoin" "bitcoin"
], ],
"devDependencies": { "devDependencies": {
"@types/jest": "~29.5", "@types/jest": "~29.5",
"@types/node": "~18", "@types/node": "~18",
"@typescript-eslint/eslint-plugin": "~5.59", "@typescript-eslint/eslint-plugin": "~5.59",
"@typescript-eslint/parser": "~5.59", "@typescript-eslint/parser": "~5.59",
"eslint": "~8.38", "eslint": "~8.38",
"eslint-config-prettier": "~8.8", "eslint-config-prettier": "~8.8",
"eslint-plugin-jest": "~27.2", "eslint-plugin-jest": "~27.2",
"jest": "~29.5", "jest": "~29.5",
"prettier": "3.2.5", "prettier": "~2.8",
"rimraf": "~5.0", "rimraf": "~5.0",
"ts-api-utils": "~0.0.44", "ts-api-utils": "~0.0.44",
"ts-jest": "~29.1", "ts-jest": "~29.1",
"typescript": "~5.0" "typescript": "~5.0"
}, },
"scripts": { "scripts": {
"start": "node build/src/main.js", "start": "node build/src/main.js",
"clean": "rimraf coverage build tmp", "clean": "rimraf coverage build tmp",
"_prebuild": "npm run lint", "_prebuild": "npm run lint",
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"build:watch": "tsc -w -p tsconfig.json", "build:watch": "tsc -w -p tsconfig.json",
"build:release": "npm run clean && tsc -p tsconfig.release.json", "build:release": "npm run clean && tsc -p tsconfig.release.json",
"lint": "eslint . --ext .ts --ext .mts", "lint": "eslint . --ext .ts --ext .mts",
"test": "jest --coverage", "test": "jest --coverage",
"prettier": "prettier --config .prettierrc --write .", "prettier": "prettier --config .prettierrc --write .",
"test:watch": "jest --watch", "test:watch": "jest --watch"
"formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", },
"formatter:fix": "prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"" "author": "pablof7z",
}, "license": "MIT",
"author": "pablof7z", "dependencies": {
"license": "MIT", "@nostr-dev-kit/ndk": "^0.8.1",
"dependencies": { "axios": "^1.4.0",
"@nostr-dev-kit/ndk": "^0.8.1", "debug": "^4.3.4",
"axios": "^1.4.0", "file-type": "^18.5.0",
"debug": "^4.3.4", "form-data": "^4.0.0",
"file-type": "^18.5.0", "form-data-encoder": "^3.0.0",
"form-data": "^4.0.0", "formdata-node": "^5.0.1",
"form-data-encoder": "^3.0.0", "light-bolt11-decoder": "^3.0.0",
"formdata-node": "^5.0.1", "lnbits": "^1.1.5",
"light-bolt11-decoder": "^3.0.0", "tslib": "~2.5"
"lnbits": "^1.1.5", },
"tslib": "~2.5" "volta": {
}, "node": "18.12.1"
"volta": { }
"node": "18.12.1"
}
} }

View File

@ -64,8 +64,8 @@ importers:
specifier: ~29.5 specifier: ~29.5
version: 29.5.0(@types/node@18.16.19) version: 29.5.0(@types/node@18.16.19)
prettier: prettier:
specifier: 3.2.5 specifier: ~2.8
version: 3.2.5 version: 2.8.8
rimraf: rimraf:
specifier: ~5.0 specifier: ~5.0
version: 5.0.1 version: 5.0.1
@ -2171,9 +2171,9 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prettier@3.2.5: prettier@2.8.8:
resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
engines: {node: '>=14'} engines: {node: '>=10.13.0'}
hasBin: true hasBin: true
pretty-format@29.6.1: pretty-format@29.6.1:
@ -5246,7 +5246,7 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier@3.2.5: {} prettier@2.8.8: {}
pretty-format@29.6.1: pretty-format@29.6.1:
dependencies: dependencies:

View File

@ -1,37 +1,37 @@
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk' import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import fs from 'fs' import fs from 'fs';
import { log, configFile } from '../main.js' import { log, configFile } from '../main.js';
type IConfig = { type IConfig = {
key: string key: string;
discount?: number discount?: number;
undercut?: number undercut?: number;
processWithoutPaymentLimit?: number processWithoutPaymentLimit?: number;
serveResultsWithoutPaymentLimit?: number serveResultsWithoutPaymentLimit?: number;
} }
export function getConfig(): IConfig { export function getConfig(): IConfig {
let config: IConfig let config: IConfig;
if (fs.existsSync(configFile)) { if (fs.existsSync(configFile)) {
config = JSON.parse(fs.readFileSync(configFile, 'utf8')) config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
}
if (!config) {
log('Generating new config')
config = {
key: NDKPrivateKeySigner.generate().privateKey
} }
const signer = NDKPrivateKeySigner.generate() if (!config) {
config.key = signer.privateKey log('Generating new config')
config = {
key: NDKPrivateKeySigner.generate().privateKey,
};
saveConfig(config) const signer = NDKPrivateKeySigner.generate();
} config.key = signer.privateKey;
return config saveConfig(config);
}
return config;
} }
export function saveConfig(config: IConfig) { export function saveConfig(config: IConfig) {
fs.writeFileSync(configFile, JSON.stringify(config)) fs.writeFileSync(configFile, JSON.stringify(config));
} }

View File

@ -1,43 +1,43 @@
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk';
import { log } from '../main.js' import { log } from '../main.js';
import axios from 'axios' import axios from 'axios';
export async function blockChainBlockNumberJob( export async function blockChainBlockNumberJob(
event: NDKEvent event: NDKEvent,
): Promise<string> { ): Promise<string> {
log('New blockChain-block-number job', event.rawEvent()) log('New blockChain-block-number job', event.rawEvent());
const input = event.tagValue('i') const input = event.tagValue('i');
const blockChainUrl = `https://blockchain.info/blocks/${input}?format=json` const blockChainUrl = `https://blockchain.info/blocks/${input}?format=json`;
const output = await axios const output = await axios
.get(blockChainUrl) .get(blockChainUrl)
.then((res) => { .then((res) => {
const closestObject = findClosestObject(res.data, input) const closestObject = findClosestObject(res.data, input);
return closestObject.block_index.toString() return closestObject.block_index.toString();
}) })
.catch((err) => { .catch((err) => {
console.log('err in blockChain request :>> ', err) console.log('err in blockChain request :>> ', err);
return Promise.reject(err) return Promise.reject(err);
}) });
return output return output;
} }
// todo: define type for array // todo: define type for array
// Function to find the object with the closest timestamp // Function to find the object with the closest timestamp
function findClosestObject(array, timestamp) { function findClosestObject(array, timestamp) {
let closestObject = null let closestObject = null;
let minDifference = Infinity let minDifference = Infinity;
array.forEach((obj) => { array.forEach((obj) => {
const difference = Math.abs(obj.time - timestamp) const difference = Math.abs(obj.time - timestamp);
if (difference < minDifference) { if (difference < minDifference) {
minDifference = difference minDifference = difference;
closestObject = obj closestObject = obj;
} }
}) });
return closestObject return closestObject;
} }

View File

@ -1,57 +0,0 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'
import { log } from '../main.js'
import axios from 'axios'
import { RelayInfo } from '../types/index.js'
export const relayInfoJob = async (event: NDKEvent): Promise<string> => {
log('New relay-info job', event.rawEvent())
const input: string = event.tagValue('i')
let relays: string[] = []
try {
const relaysArr = JSON.parse(input)
// input is a string containing an array of strings representing multiple relay URIs
relays = relaysArr
} catch (err) {
// input is a string containing a string representing single relay URI
relays.push(input)
}
const prefixes = { wss: 'wss://', https: 'https://', http: 'http://' }
const headers = {
headers: { Accept: 'application/nostr+json' }
}
const requests = relays.map((relay) => {
if (relay.startsWith(prefixes.wss)) {
relay = relay.replace(prefixes.wss, '')
}
return axios
.get<RelayInfo>((relay = `${prefixes.https}${relay}`), headers)
.catch(() => {
return axios
.get<RelayInfo>(`${prefixes.http}${relay}`, headers)
.catch(() => undefined)
})
})
let responses = await Promise.all(requests)
responses = responses.filter((response) => response !== undefined)
const relaysInfo: { [key: string]: RelayInfo } = {}
responses.forEach((response) => {
const relayURI = response.config.url
.replace(prefixes.https, prefixes.wss)
.replace(prefixes.http, prefixes.wss)
relaysInfo[relayURI] = response.data
})
return Promise.resolve(JSON.stringify(relaysInfo))
}

View File

@ -1,144 +1,142 @@
import { NDKEvent, type NostrEvent } from '@nostr-dev-kit/ndk' import { NDKEvent, type NostrEvent } from '@nostr-dev-kit/ndk';
import axios from 'axios' import axios from 'axios';
import FormData from 'form-data' import FormData from 'form-data';
import { log } from '../main.js' import { log } from '../main.js';
import { fetchFileFromInput } from '../utils/fetch-file-from-input.js' import { fetchFileFromInput } from '../utils/fetch-file-from-input.js';
import { ndk } from '../main.js' import { ndk } from '../main.js';
// import { fileTypeFromFile, type FileTypeResult } from 'file-type'; // import { fileTypeFromFile, type FileTypeResult } from 'file-type';
import { exec } from 'child_process' import { exec } from 'child_process';
import fs from 'fs' import fs from 'fs';
import { createInvoice } from '../utils/lnbits.js' import { createInvoice } from '../utils/lnbits.js';
export async function speechToTextJob(event: NDKEvent): Promise<string> { export async function speechToTextJob(event: NDKEvent): Promise<string> {
log('New speech-to-text job', event.rawEvent()) log("New speech-to-text job", event.rawEvent());
// fetch the input file // fetch the input file
const file = await fetchFile(event) const file = await fetchFile(event);
log(`file: ${file}`) log(`file: ${file}`);
if (!file) { if (!file) {
return undefined return undefined;
} }
// check if file exists // check if file exists
// if (!fs.existsSync(file)) { // if (!fs.existsSync(file)) {
// log(`file does not exist: ${file}`); // log(`file does not exist: ${file}`);
// return undefined; // return undefined;
// } // }
// let fileType: FileTypeResult; // let fileType: FileTypeResult;
// try { // try {
// fileType = await fileTypeFromFile(file); // fileType = await fileTypeFromFile(file);
// log(`fileType: ${fileType}`); // log(`fileType: ${fileType}`);
// } catch (error) { // } catch (error) {
// log(error); // log(error);
// } // }
const whisperCommand = (resolve: any, file: string) => { const whisperCommand = (resolve: any, file: string) => {
setTimeout(() => { setTimeout(() => {
if (!fs.existsSync(file)) {
log(`file does not exist: ${file}`)
return undefined
}
const formData = new FormData()
const a = fs.createReadStream(file)
console.log({ a })
formData.append('file', a) if (!fs.existsSync(file)) {
formData.append('model', 'whisper-1') log(`file does not exist: ${file}`);
return undefined;
}
const formData = new FormData();
const a = fs.createReadStream(file);
console.log({a});
axios formData.append('file', a);
.post('http://127.0.0.1:10111/v1/audio/transcriptions', formData, { formData.append('model', 'whisper-1');
headers: {
Authorization: 'Bearer fsdfdsf',
...formData.getHeaders()
}
})
.then((response) => {
const { text } = response.data
console.log({ text })
resolve(text)
// Handle response
})
.catch((error) => {
console.log({ error })
// Handle error
})
}, 1000)
}
// const input = event.tagValue('i'); axios.post('http://127.0.0.1:10111/v1/audio/transcriptions', formData, {
const range = event.getMatchingTags('param').find((tag) => tag[1] === 'range') headers: {
return new Promise((resolve) => whisperCommand(resolve, file)) 'Authorization': 'Bearer fsdfdsf',
...formData.getHeaders(),
},
}).then(response => {
const { text } = response.data;
console.log({text});
resolve(text);
// Handle response
})
.catch(error => {
console.log({error});
// Handle error
});
}, 1000);
}
if (range) { // const input = event.tagValue('i');
return new Promise((resolve) => { const range = event.getMatchingTags('param').find((tag) => tag[1] === 'range');
const startTime = range[2] return new Promise((resolve) => whisperCommand(resolve, file));
const endTime = range[3]
const randomName = Math.random().toString(36).substring(7) + '.mp3'
const command = `ffmpeg -ss ${startTime} -to ${endTime} -i ${file} -vn -acodec copy ${randomName}`
console.log({ startTime, endTime, randomName, command })
exec(command, (error, stderr, stdout) => {
console.log('ffmpeg', { error, stderr, stdout })
whisperCommand(resolve, randomName) if (range) {
}) return new Promise((resolve) => {
}) const startTime = range[2];
} else { const endTime = range[3];
return new Promise((resolve) => whisperCommand(resolve, file)) const randomName = Math.random().toString(36).substring(7)+'.mp3';
} const command = `ffmpeg -ss ${startTime} -to ${endTime} -i ${file} -vn -acodec copy ${randomName}`;
console.log({startTime, endTime, randomName, command});
exec(command, (error, stderr, stdout) => {
console.log('ffmpeg', {error, stderr, stdout});
whisperCommand(resolve, randomName);
});
});
} else {
return new Promise((resolve) => whisperCommand(resolve, file));
}
} }
interface ICompleteParams { interface ICompleteParams {
output: string output: string;
} }
export async function addAmount( export async function addAmount(event: NDKEvent, amount: number, includeInvoice = true): Promise<void> {
event: NDKEvent, const tag = ['amount', amount.toString()];
amount: number,
includeInvoice = true
): Promise<void> {
const tag = ['amount', amount.toString()]
if (includeInvoice) { if (includeInvoice) {
const invoice = await createInvoice(amount / 1000) const invoice = await createInvoice(amount / 1000);
tag.push(invoice.payment_request) tag.push(invoice.payment_request);
} }
event.tags.push(tag) event.tags.push(tag);
} }
export async function complete( export async function complete(
jobRequest: NDKEvent, jobRequest: NDKEvent,
amount: number, amount: number,
completeParams: ICompleteParams, completeParams: ICompleteParams,
includeInvoice?: boolean includeInvoice?: boolean
): Promise<void> { ): Promise<void> {
const jobResult = new NDKEvent(ndk, { const jobResult = new NDKEvent(ndk, {
kind: 68002, kind: 68002,
content: completeParams.output, content: completeParams.output,
tags: [['status', 'success']] tags: [
} as NostrEvent) [ 'status', 'success' ]
]
} as NostrEvent);
if (amount > 0) { if (amount > 0) {
await addAmount(jobResult, amount, includeInvoice) await addAmount(jobResult, amount, includeInvoice);
} }
jobResult.tag(jobRequest) jobResult.tag(jobRequest);
await jobResult.sign()
log(jobResult.rawEvent())
await jobResult.publish() await jobResult.sign();
return jobResult log(jobResult.rawEvent());
await jobResult.publish();
return jobResult;
} }
export async function fetchFile(event: NDKEvent): Promise<string> { export async function fetchFile(event: NDKEvent): Promise<string> {
const inputTags = event.getMatchingTags('i') const inputTags = event.getMatchingTags("i");
if (inputTags.length !== 1) { if (inputTags.length !== 1) {
throw new Error(`Incorrect number of inputs: ${inputTags.length}`) throw new Error(`Incorrect number of inputs: ${inputTags.length}`);
} }
return fetchFileFromInput(inputTags[0]) return fetchFileFromInput(inputTags[0]);
} }

View File

@ -1,11 +1,11 @@
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk';
import { validateJobRequest } from '../validations/index.js' import { validateJobRequest } from '../validations/index.js';
import { inProgress } from '../jobs/reactions/in-progress.js' import { inProgress } from '../jobs/reactions/in-progress.js';
export async function onNewSummarizationJob(event: NDKEvent): Promise<void> { export async function onNewSummarizationJob(event: NDKEvent): Promise<void> {
console.log('New summarization job') console.log("New summarization job");
await validateJobRequest(event) await validateJobRequest(event);
inProgress(event) inProgress(event);
} }

View File

@ -1,17 +1,17 @@
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk';
import { log } from '../main.js' import { log } from '../main.js';
import { getConfig } from '../config/index.js' import { getConfig } from '../config/index.js';
export async function priceJob(event: NDKEvent): Promise<number> { export async function priceJob(event: NDKEvent): Promise<number> {
const config = getConfig() const config = getConfig();
const bidTag = event.tagValue('bid') const bidTag = event.tagValue('bid');
let bidAmount = bidTag ? parseInt(bidTag) : 1001 * 1000 let bidAmount = bidTag ? parseInt(bidTag) : 1001 * 1000;
if (config.discount) { if (config.discount) {
bidAmount = bidAmount * config.discount bidAmount = bidAmount * config.discount;
} }
log(`bid amount: ${bidAmount} (${config.discount ?? 'no'} discount)`) log(`bid amount: ${bidAmount} (${config.discount??'no'} discount)`);
return bidAmount return bidAmount;
} }

View File

@ -1,17 +1,19 @@
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk';
import { log, ndk } from '../../main.js' import { log, ndk } from '../../main.js';
export async function inProgress(event: NDKEvent): Promise<NDKEvent> { export async function inProgress(event: NDKEvent): Promise<NDKEvent> {
log('marking job as in progress') log("marking job as in progress");
const reactEvent = new NDKEvent(ndk, { const reactEvent = new NDKEvent(ndk, {
kind: 68003, kind: 68003,
content: '👍', content: "👍",
tags: [['status', 'started']] tags: [
}) [ "status", "started" ],
]
})
reactEvent.tag(event, 'job') reactEvent.tag(event, "job");
await reactEvent.sign() await reactEvent.sign();
await reactEvent.publish() await reactEvent.publish();
return reactEvent return reactEvent;
} }

View File

@ -1,23 +1,25 @@
import { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk' import { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk';
import { log, ndk } from '../../main.js' import { log, ndk } from '../../main.js';
export async function publishStatus( export async function publishStatus(
event: NDKEvent, event: NDKEvent,
status: string, status: string,
extraTags: NDKTag[] = [] extraTags: NDKTag[] = []): Promise<NDKEvent> {
): Promise<NDKEvent> { log("marking job as finished");
log('marking job as finished')
const reactEvent = new NDKEvent(ndk, { const reactEvent = new NDKEvent(ndk, {
kind: 68003, kind: 68003,
content: '👍', content: "👍",
tags: [['status', status], ...extraTags] tags: [
}) [ "status", status ],
...extraTags
]
})
console.log({ extraTags }) console.log({extraTags})
reactEvent.tag(event, 'job') reactEvent.tag(event, "job");
await reactEvent.sign() await reactEvent.sign();
await reactEvent.publish() await reactEvent.publish();
return reactEvent return reactEvent;
} }

View File

@ -1,8 +1,8 @@
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk' import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { getConfig } from './config/index.js' import { getConfig } from './config/index.js';
export default function getSigner(): NDKPrivateKeySigner { export default function getSigner(): NDKPrivateKeySigner {
const config = getConfig() const config = getConfig();
return new NDKPrivateKeySigner(config.key!) return new NDKPrivateKeySigner(config.key!);
} }

View File

@ -1,249 +1,240 @@
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk' import NDK, { NDKEvent } from '@nostr-dev-kit/ndk';
import debug from 'debug' import debug from 'debug';
import { onNewSummarizationJob } from './job-types/summarization.js' import { onNewSummarizationJob } from './job-types/summarization.js';
import { complete, speechToTextJob } from './job-types/speech-to-text.js' import { complete, speechToTextJob } from './job-types/speech-to-text.js';
import getSigner from './local-signer.js' import getSigner from './local-signer.js';
import { requirePayment, validateJobRequest } from './validations/index.js' import { requirePayment, validateJobRequest } from './validations/index.js';
import { inProgress } from './jobs/reactions/in-progress.js' import { inProgress } from './jobs/reactions/in-progress.js';
import { publishStatus } from './jobs/reactions/status.js' import { publishStatus } from './jobs/reactions/status.js';
import { priceJob } from './jobs/price.js' import { priceJob } from './jobs/price.js';
import { getConfig } from './config/index.js' import { getConfig } from './config/index.js';
import { decode } from 'light-bolt11-decoder' import { decode } from 'light-bolt11-decoder';
import { checkInvoiceStatus } from './utils/lnbits.js' import { checkInvoiceStatus } from './utils/lnbits.js';
import { blockChainBlockNumberJob } from './job-types/blockChain-block-number.js' import { blockChainBlockNumberJob } from './job-types/blockChain-block-number.js';
import { relayInfoJob } from './job-types/relay-info.js'
import { JobType, JobTypes } from './types/index.js'
export const log = debug('fool-me-once-dvm') export const log = debug('fool-me-once-dvm');
export const configFile = export const configFile =
process.argv[2] || `${process.env.HOME}/.fool-me-once.json` process.argv[2] || `${process.env.HOME}/.fool-me-once.json`;
log('configFile', { configFile }) log('configFile', { configFile });
export const ndk = new NDK({ export const ndk = new NDK({
explicitRelayUrls: [ explicitRelayUrls: [
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://relay.primal.net', 'wss://relay.primal.net',
'wss://relayable.org' 'wss://relayable.org',
], ],
signer: getSigner() signer: getSigner(),
}) });
await ndk.connect(2000) await ndk.connect(2000);
log('connected') log('connected');
const subs = ndk.subscribe( const subs = ndk.subscribe(
{ {
kinds: [68001 as number], kinds: [68001 as number],
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
'#j': [JobTypes.Summarize, JobTypes.Explain] '#j': ['summarize', 'explain'],
}, },
{ closeOnEose: false } { closeOnEose: false },
) );
const speechToTextSub = ndk.subscribe( const speechToTextSub = ndk.subscribe(
{ {
kinds: [68001 as number], kinds: [68001 as number],
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
'#j': [JobTypes.SpeechToText] '#j': ['speech-to-text'],
}, },
{ closeOnEose: false } { closeOnEose: false },
) );
const blockChainBlockNumberSub = ndk.subscribe( const blockChainBlockNumberSub = ndk.subscribe(
{ {
kinds: [68001 as number], kinds: [68001 as number],
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
'#j': [JobTypes.BlockChainBlockNumber] '#j': ['blockChain-block-number'],
}, },
{ closeOnEose: false } { closeOnEose: false },
) );
const relayInfoSub = ndk.subscribe(
{
kinds: [68001 as number],
since: Math.floor(Date.now() / 1000),
'#j': [JobTypes.RelayInfo]
},
{ closeOnEose: false }
)
subs.on('event', (e) => processJobEvent(e, JobTypes.Summarize)) subs.on('event', (e) => processJobEvent(e, 'summarize'));
speechToTextSub.on('event', (e) => processJobEvent(e, JobTypes.SpeechToText)) speechToTextSub.on('event', (e) => processJobEvent(e, 'speech-to-text'));
blockChainBlockNumberSub.on('event', (e) => blockChainBlockNumberSub.on('event', (e) =>
processJobEvent(e, JobTypes.BlockChainBlockNumber) processJobEvent(e, 'blockChain-block-number'),
) );
relayInfoSub.on('event', (e) => processJobEvent(e, JobTypes.RelayInfo))
const freeJobs: string[] = [JobTypes.BlockChainBlockNumber, JobTypes.RelayInfo] type JobType =
| 'summarize'
| 'explain'
| 'speech-to-text'
| 'blockChain-block-number';
async function processJobEvent(event: NDKEvent, type: JobType): Promise<void> { async function processJobEvent(event: NDKEvent, type: JobType): Promise<void> {
const config = getConfig() const config = getConfig();
let jobAmount = freeJobs.includes(type) ? 0 : await priceJob(event) let jobAmount =
let output: any type === 'blockChain-block-number' ? 0 : await priceJob(event);
let payReqEvent: NDKEvent let output: any;
let paidAmount = 0 let payReqEvent: NDKEvent;
let paidAmount = 0;
const waitForPaymentBeforeProcessing = (): boolean => { const waitForPaymentBeforeProcessing = (): boolean => {
const processWithoutPaymentLimit = config.processWithoutPaymentLimit ?? 500 const processWithoutPaymentLimit =
log('waitForPaymentBeforeProcessing', { config.processWithoutPaymentLimit ?? 500;
jobAmount, log('waitForPaymentBeforeProcessing', {
processWithoutPaymentLimit jobAmount,
}) processWithoutPaymentLimit,
return jobAmount && jobAmount > processWithoutPaymentLimit * 1000 });
} return jobAmount && jobAmount > processWithoutPaymentLimit * 1000;
};
const waitForPaymentBeforePublishingResult = (): boolean => { const waitForPaymentBeforePublishingResult = (): boolean => {
const serveResultsWithoutPaymentLimit = const serveResultsWithoutPaymentLimit =
config.serveResultsWithoutPaymentLimit ?? 1000 config.serveResultsWithoutPaymentLimit ?? 1000;
log('waitForPaymentBeforeProcessing', { jobAmount }) log('waitForPaymentBeforeProcessing', { jobAmount });
return jobAmount && jobAmount > serveResultsWithoutPaymentLimit * 1000 return jobAmount && jobAmount > serveResultsWithoutPaymentLimit * 1000;
} };
const missingAmount = (): number => jobAmount - paidAmount const missingAmount = (): number => jobAmount - paidAmount;
const reqPayment = async (): Promise<void> => { const reqPayment = async (): Promise<void> => {
payReqEvent = await requirePayment(event, missingAmount(), true) payReqEvent = await requirePayment(event, missingAmount(), true);
if (config.undercut) { if (config.undercut) {
startUndercutting() startUndercutting();
}
}
const startUndercutting = async (): Promise<void> => {
const undercutSub = ndk.subscribe(
{
kinds: [68002 as number, 68003 as number],
...event.filter()
},
{ closeOnEose: false, groupable: false }
)
undercutSub.on('event', async (e) => {
if (e.pubkey === payReqEvent.pubkey) return
// check if this is a payment request
const amountValue = e.tagValue('amount')
if (!amountValue) return
log(`found someone else's bid`, amountValue)
// check if it's more-or-less than the current bid
const amount = parseInt(amountValue)
if (amount > jobAmount) return
// if so, undercut
jobAmount = Math.round(amount * config.undercut)
log(`undercutting to ${jobAmount}`)
setTimeout(async () => {
payReqEvent = await requirePayment(event, jobAmount, true)
}, 5000)
})
}
const waitForPaymentViaZap = (payReqEvent, resolve, reject) => {
const zapmon = ndk.subscribe(
{
kinds: [68002, 9735],
...payReqEvent.filter()
},
{ closeOnEose: false }
)
zapmon.on('event', (e) => {
log(`received a ${e.kind} for the payment request`, e.rawEvent())
// TODO: validate amount, zapper, etc
if (e.kind === 9735) {
// TODO: This needs to check the actual zap
paidAmount = jobAmount
// zapmon.close();
resolve()
} else if (e.kind === 68003) {
zapmon.stop()
reject()
}
})
}
const waitForPaymentViaLNInvoice = (payReqEvent, resolve) => {
const amountTag = payReqEvent.getMatchingTags('amount')[0]
const bolt11 = amountTag[2]
if (!bolt11) return
const invoice = decode(bolt11)
const pr = invoice.payment_hash
log({ invoice })
log({ pr })
const checkInterval = setInterval(() => {
checkInvoiceStatus(invoice).then((status) => {
if (status.paid) {
log('invoice paid')
paidAmount = jobAmount
clearInterval(checkInterval)
resolve()
} }
}) };
}, 2000)
}
const waitForPayment = async (payReqEvent: NDKEvent): Promise<void> => { const startUndercutting = async (): Promise<void> => {
log('waitForPayment') const undercutSub = ndk.subscribe(
const promise = new Promise<void>((resolve, reject) => { {
waitForPaymentViaZap(payReqEvent, resolve, reject) kinds: [68002 as number, 68003 as number],
waitForPaymentViaLNInvoice(payReqEvent, resolve) ...event.filter(),
}) },
{ closeOnEose: false, groupable: false },
);
undercutSub.on('event', async (e) => {
if (e.pubkey === payReqEvent.pubkey) return;
return promise // check if this is a payment request
} const amountValue = e.tagValue('amount');
if (!amountValue) return;
const startProcessing = async () => { log(`found someone else's bid`, amountValue);
log('startProcessing')
await inProgress(event)
switch (type) { // check if it's more-or-less than the current bid
case JobTypes.Summarize: { const amount = parseInt(amountValue);
output = await onNewSummarizationJob(event) if (amount > jobAmount) return;
break
} // if so, undercut
case JobTypes.SpeechToText: { jobAmount = Math.round(amount * config.undercut);
output = await speechToTextJob(event) log(`undercutting to ${jobAmount}`);
break setTimeout(async () => {
} payReqEvent = await requirePayment(event, jobAmount, true);
case JobTypes.BlockChainBlockNumber: { }, 5000);
output = await blockChainBlockNumberJob(event) });
break };
}
case JobTypes.RelayInfo: { const waitForPaymentViaZap = (payReqEvent, resolve, reject) => {
output = await relayInfoJob(event) const zapmon = ndk.subscribe(
break {
} kinds: [68002, 9735],
...payReqEvent.filter(),
},
{ closeOnEose: false },
);
zapmon.on('event', (e) => {
log(`received a ${e.kind} for the payment request`, e.rawEvent());
// TODO: validate amount, zapper, etc
if (e.kind === 9735) {
// TODO: This needs to check the actual zap
paidAmount = jobAmount;
// zapmon.close();
resolve();
} else if (e.kind === 68003) {
zapmon.stop();
reject();
}
});
};
const waitForPaymentViaLNInvoice = (payReqEvent, resolve) => {
const amountTag = payReqEvent.getMatchingTags('amount')[0];
const bolt11 = amountTag[2];
if (!bolt11) return;
const invoice = decode(bolt11);
const pr = invoice.payment_hash;
log({ invoice });
log({ pr });
const checkInterval = setInterval(() => {
checkInvoiceStatus(invoice).then((status) => {
if (status.paid) {
log('invoice paid');
paidAmount = jobAmount;
clearInterval(checkInterval);
resolve();
}
});
}, 2000);
};
const waitForPayment = async (payReqEvent: NDKEvent): Promise<void> => {
log('waitForPayment');
const promise = new Promise<void>((resolve, reject) => {
waitForPaymentViaZap(payReqEvent, resolve, reject);
waitForPaymentViaLNInvoice(payReqEvent, resolve);
});
return promise;
};
const startProcessing = async () => {
log('startProcessing');
await inProgress(event);
switch (type) {
case 'summarize': {
output = await onNewSummarizationJob(event);
break;
}
case 'speech-to-text': {
output = await speechToTextJob(event);
break;
}
case 'blockChain-block-number': {
output = await blockChainBlockNumberJob(event);
break;
}
}
};
const publishResult = (): void => {
log('publishResult');
complete(event, missingAmount(), { output });
};
await validateJobRequest(event);
if (jobAmount > paidAmount && waitForPaymentBeforeProcessing()) {
await reqPayment();
await waitForPayment(payReqEvent);
} }
}
const publishResult = (): void => { await startProcessing();
log('publishResult')
complete(event, missingAmount(), { output })
}
await validateJobRequest(event) await publishStatus(event, 'finished').catch((err) =>
console.log('err :>> ', err),
);
if (jobAmount > paidAmount && waitForPaymentBeforeProcessing()) { if (jobAmount > paidAmount && waitForPaymentBeforePublishingResult()) {
await reqPayment() await reqPayment();
await waitForPayment(payReqEvent) await waitForPayment(payReqEvent);
} log(`done with wait for publish`);
}
await startProcessing() await publishResult();
await publishStatus(event, 'finished').catch((err) =>
console.log('err :>> ', err)
)
if (jobAmount > paidAmount && waitForPaymentBeforePublishingResult()) {
await reqPayment()
await waitForPayment(payReqEvent)
log(`done with wait for publish`)
}
await publishResult()
} }

View File

@ -1,2 +0,0 @@
export * from './relay.js'
export * from './job.js'

View File

@ -1,14 +0,0 @@
export type JobType =
| 'summarize'
| 'explain'
| 'speech-to-text'
| 'blockChain-block-number'
| 'relay-info'
export enum JobTypes {
Summarize = 'summarize',
Explain = 'explain',
SpeechToText = 'speech-to-text',
BlockChainBlockNumber = 'blockChain-block-number',
RelayInfo = 'relay-info'
}

View File

@ -1,9 +0,0 @@
export interface RelayInfo {
name: string
description: string
pubkey: string
contact: string
supported_nips: number[]
software: string
version: string
}

View File

@ -1,28 +1,28 @@
import { NDKTag } from '@nostr-dev-kit/ndk' import { NDKTag } from '@nostr-dev-kit/ndk';
import axios from 'axios' import axios from 'axios';
import fs from 'fs' import fs from 'fs';
export async function fetchFileFromInput(input: NDKTag): Promise<string> { export async function fetchFileFromInput(input: NDKTag): Promise<string> {
switch (input[2]) { switch (input[2]) {
case 'url': { case 'url': {
const url = input[1] const url = input[1];
const fileExtension = url.split('.').pop() const fileExtension = url.split('.').pop();
// download the file // download the file
// save it to the local filesystem // save it to the local filesystem
// return the path to the file // return the path to the file
const response = await axios.get(url, { const response = await axios.get(url, {
responseType: 'stream' responseType: 'stream'
}) });
const randomName = Math.random().toString(36).substring(7) const randomName = Math.random().toString(36).substring(7);
const path = `./${randomName}.${fileExtension}` const path = `./${randomName}.${fileExtension}`;
await response.data.pipe(fs.createWriteStream(path)) await response.data.pipe(fs.createWriteStream(path));
return path return path;
}
} }
}
return undefined return undefined;
} }

View File

@ -1,38 +1,38 @@
import _LNBits from 'lnbits' import _LNBits from "lnbits";
let LNBits: any let LNBits: any;
if (_LNBits.default) { if (_LNBits.default) {
LNBits = _LNBits.default LNBits = _LNBits.default;
} else { } else {
LNBits = _LNBits LNBits = _LNBits;
} }
function getWallet(): any { function getWallet (): any {
return LNBits({ return LNBits({
adminKey: '', adminKey: "",
invoiceReadKey: 'bfbcb5b116c04179bc618ab78265a939', invoiceReadKey: 'bfbcb5b116c04179bc618ab78265a939',
endpoint: 'https://legend.lnbits.com' endpoint: 'https://legend.lnbits.com'
}) });
} }
export async function createInvoice(amount: number): Promise<any> { export async function createInvoice(amount: number): Promise<any> {
const { wallet } = getWallet() const { wallet } = getWallet();
const newInvoice = await wallet.createInvoice({ const newInvoice = await wallet.createInvoice({
amount: amount, amount: amount,
memo: 'data vending machine', memo: 'data vending machine',
out: false out: false,
}) });
return newInvoice return newInvoice;
} }
export async function checkInvoiceStatus(invoice): Promise<any> { export async function checkInvoiceStatus(invoice): Promise<any> {
const { wallet } = getWallet() const { wallet } = getWallet();
const invoiceStatus = await wallet.checkInvoice({ const invoiceStatus = await wallet.checkInvoice({
payment_hash: invoice.payment_hash payment_hash: invoice.payment_hash,
}) });
return invoiceStatus return invoiceStatus;
} }

View File

@ -1,12 +1,12 @@
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk';
export default async function validateExpiration(event: NDKEvent) { export default async function validateExpiration(event: NDKEvent) {
const expTag = event.tagValue('exp') const expTag = event.tagValue('exp');
const timeNow = Math.floor(Date.now() / 1000) const timeNow = Math.floor(Date.now() / 1000);
if (expTag && parseInt(expTag) < timeNow - 10) { if (expTag && parseInt(expTag) < timeNow - 10) {
throw new Error('Job expired') throw new Error('Job expired');
} }
return return
} }

View File

@ -1,42 +1,40 @@
import { NDKEvent, type NostrEvent } from '@nostr-dev-kit/ndk' import { NDKEvent, type NostrEvent } from '@nostr-dev-kit/ndk';
import validateExpiration from './expiration.js' import validateExpiration from './expiration.js';
import validateRequester from './requester.js' import validateRequester from './requester.js';
import validateNoRecentResults from './no-recent-results.js' import validateNoRecentResults from './no-recent-results.js';
import { ndk } from '../main.js' import { ndk } from '../main.js';
import { addAmount } from '../job-types/speech-to-text.js' import { addAmount } from '../job-types/speech-to-text.js';
export async function validateJobRequest(event: NDKEvent): Promise<void> { export async function validateJobRequest(event: NDKEvent): Promise<void> {
await validateExpiration(event) await validateExpiration(event);
await validateRequester(event) await validateRequester(event);
await validateNoRecentResults(event) await validateNoRecentResults(event);
} }
export async function requirePayment( export async function requirePayment(event: NDKEvent, amount?: number, publish?: boolean): Promise<NDKEvent> {
event: NDKEvent, if (!amount) {
amount?: number, const bidTag = event.tagValue('bid');
publish?: boolean amount = bidTag ? parseInt(bidTag) : undefined;
): Promise<NDKEvent> { }
if (!amount) {
const bidTag = event.tagValue('bid')
amount = bidTag ? parseInt(bidTag) : undefined
}
if (!amount) { if (!amount) {
throw new Error('No amount specified') throw new Error("No amount specified");
} }
const payReq = new NDKEvent(ndk, { const payReq = new NDKEvent(ndk, {
kind: 68003, kind: 68003,
content: '`Please pay for this job`', content: "`Please pay for this job`",
tags: [['status', 'payment-required']] tags: [
} as NostrEvent) ["status", "payment-required"],
await addAmount(payReq, amount) ]
payReq.tag(event, 'job') } as NostrEvent);
await addAmount(payReq, amount);
payReq.tag(event, "job");
await payReq.sign() await payReq.sign();
console.log(payReq.rawEvent()) console.log(payReq.rawEvent());
if (publish !== false) await payReq.publish() if (publish !== false) await payReq.publish();
return payReq return payReq;
} }

View File

@ -1,18 +1,13 @@
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk';
import { ndk } from '../main.js' import { ndk } from '../main.js';
export default async function validateNoRecentResults( export default async function validateNoRecentResults(event: NDKEvent): Promise<void> {
event: NDKEvent const results = ndk.fetchEvents({
): Promise<void> { kinds: [68002 as number],
const results = ndk.fetchEvents( "#e": [event.id],
{ }, { groupable: false })
kinds: [68002 as number],
'#e': [event.id]
},
{ groupable: false }
)
if (results.length > 0) { if (results.length > 0) {
throw new Error(`This job already has ${results.length} results`) throw new Error(`This job already has ${results.length} results`);
} }
} }

View File

@ -1,9 +1,5 @@
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk';
export default async function validateRequester( export default async function validateRequester(event: NDKEvent): Promise<void> {
event: NDKEvent if (event) { /* empty */ }
): Promise<void> {
if (event) {
/* empty */
}
} }