Compare commits

...

4 Commits

23 changed files with 704 additions and 797 deletions

View File

@ -1,13 +1,7 @@
{ {
"singleQuote": true, "trailingComma": "none",
"trailingComma": "all", "tabWidth": 2,
"tabWidth": 4, "semi": false,
"overrides": [ "singleQuote": true,
{ "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.7.5", "@nostr-dev-kit/ndk": "^0.8.1",
"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": "~2.8", "prettier": "3.2.5",
"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.7.7", "version": "0.8.23",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.7.7.tgz", "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.8.23.tgz",
"integrity": "sha512-IRTW16q40zzuSBkpYpDUcZJRrbw26JTeicfZN6O/2Gw7D2w6Pe42VqFwpbcP9xOnFPEGP2eNV6SwXQ3y0tjBtw==", "integrity": "sha512-wX/9Cl02gCR0Kz25C/1xxGO47K13Ve1x8IISbkF/M3RTTXftYBvzB7bL8qwLaFaeqb02cMU0YVL+oKDrYzH/Ng==",
"dependencies": { "dependencies": {
"@noble/hashes": "^1.3.1", "@noble/hashes": "^1.3.1",
"@noble/secp256k1": "^2.0.0", "@noble/secp256k1": "^2.0.0",
@ -1663,10 +1663,9 @@
"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.11.2", "nostr-tools": "^1.14.0",
"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": {
@ -2552,18 +2551,6 @@
"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",
@ -2792,18 +2779,6 @@
"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",
@ -3177,43 +3152,6 @@
"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",
@ -3561,20 +3499,6 @@
"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",
@ -3658,15 +3582,6 @@
"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",
@ -3720,14 +3635,6 @@
"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",
@ -4702,11 +4609,6 @@
"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",
@ -5884,11 +5786,6 @@
"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",
@ -5929,16 +5826,6 @@
"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",
@ -6578,15 +6465,15 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "2.8.8", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true, "dev": true,
"bin": { "bin": {
"prettier": "bin-prettier.js" "prettier": "bin/prettier.cjs"
}, },
"engines": { "engines": {
"node": ">=10.13.0" "node": ">=14"
}, },
"funding": { "funding": {
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
@ -7659,11 +7546,6 @@
"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",
@ -7683,11 +7565,6 @@
"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",
@ -7788,14 +7665,6 @@
"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",
@ -7866,18 +7735,6 @@
"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",
@ -7931,44 +7788,6 @@
"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",
@ -8086,14 +7905,6 @@
"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,58 +1,60 @@
{ {
"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": "~2.8", "prettier": "3.2.5",
"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}\"",
"author": "pablof7z", "formatter:fix": "prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\""
"license": "MIT", },
"dependencies": { "author": "pablof7z",
"@nostr-dev-kit/ndk": "^0.8.1", "license": "MIT",
"axios": "^1.4.0", "dependencies": {
"debug": "^4.3.4", "@nostr-dev-kit/ndk": "^0.8.1",
"file-type": "^18.5.0", "axios": "^1.4.0",
"form-data": "^4.0.0", "debug": "^4.3.4",
"form-data-encoder": "^3.0.0", "file-type": "^18.5.0",
"formdata-node": "^5.0.1", "form-data": "^4.0.0",
"light-bolt11-decoder": "^3.0.0", "form-data-encoder": "^3.0.0",
"lnbits": "^1.1.5", "formdata-node": "^5.0.1",
"tslib": "~2.5" "light-bolt11-decoder": "^3.0.0",
}, "lnbits": "^1.1.5",
"volta": { "tslib": "~2.5"
"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: ~2.8 specifier: 3.2.5
version: 2.8.8 version: 3.2.5
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@2.8.8: prettier@3.2.5:
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=14'}
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@2.8.8: {} prettier@3.2.5: {}
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
} }
if (!config) { const signer = NDKPrivateKeySigner.generate()
log('Generating new config') config.key = signer.privateKey
config = {
key: NDKPrivateKeySigner.generate().privateKey,
};
const signer = NDKPrivateKeySigner.generate(); saveConfig(config)
config.key = signer.privateKey; }
saveConfig(config); return 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

@ -0,0 +1,57 @@
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,142 +1,144 @@
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 })
if (!fs.existsSync(file)) { formData.append('file', a)
log(`file does not exist: ${file}`); formData.append('model', 'whisper-1')
return undefined;
}
const formData = new FormData();
const a = fs.createReadStream(file);
console.log({a});
formData.append('file', a); axios
formData.append('model', 'whisper-1'); .post('http://127.0.0.1:10111/v1/audio/transcriptions', formData, {
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)
}
axios.post('http://127.0.0.1:10111/v1/audio/transcriptions', formData, { // const input = event.tagValue('i');
headers: { const range = event.getMatchingTags('param').find((tag) => tag[1] === 'range')
'Authorization': 'Bearer fsdfdsf', return new Promise((resolve) => whisperCommand(resolve, file))
...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'); if (range) {
const range = event.getMatchingTags('param').find((tag) => tag[1] === 'range'); return new Promise((resolve) => {
return new Promise((resolve) => whisperCommand(resolve, file)); const startTime = range[2]
const endTime = range[3]
if (range) { const randomName = Math.random().toString(36).substring(7) + '.mp3'
return new Promise((resolve) => { const command = `ffmpeg -ss ${startTime} -to ${endTime} -i ${file} -vn -acodec copy ${randomName}`
const startTime = range[2]; console.log({ startTime, endTime, randomName, command })
const endTime = range[3]; exec(command, (error, stderr, stdout) => {
const randomName = Math.random().toString(36).substring(7)+'.mp3'; console.log('ffmpeg', { error, stderr, stdout })
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); whisperCommand(resolve, randomName)
}); })
}); })
} else { } else {
return new Promise((resolve) => whisperCommand(resolve, file)); return new Promise((resolve) => whisperCommand(resolve, file))
} }
} }
interface ICompleteParams { interface ICompleteParams {
output: string; output: string
} }
export async function addAmount(event: NDKEvent, amount: number, includeInvoice = true): Promise<void> { export async function addAmount(
const tag = ['amount', amount.toString()]; event: NDKEvent,
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: [ tags: [['status', 'success']]
[ 'status', 'success' ] } as NostrEvent)
]
} 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.sign(); await jobResult.publish()
log(jobResult.rawEvent()); return jobResult
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,19 +1,17 @@
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: [ tags: [['status', 'started']]
[ "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,25 +1,23 @@
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[] = []): Promise<NDKEvent> { extraTags: NDKTag[] = []
log("marking job as finished"); ): Promise<NDKEvent> {
log('marking job as finished')
const reactEvent = new NDKEvent(ndk, { const reactEvent = new NDKEvent(ndk, {
kind: 68003, kind: 68003,
content: "👍", content: '👍',
tags: [ tags: [['status', status], ...extraTags]
[ "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,240 +1,249 @@
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': ['summarize', 'explain'], '#j': [JobTypes.Summarize, JobTypes.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': ['speech-to-text'], '#j': [JobTypes.SpeechToText]
}, },
{ 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': ['blockChain-block-number'], '#j': [JobTypes.BlockChainBlockNumber]
}, },
{ 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, 'summarize')); subs.on('event', (e) => processJobEvent(e, JobTypes.Summarize))
speechToTextSub.on('event', (e) => processJobEvent(e, 'speech-to-text')); speechToTextSub.on('event', (e) => processJobEvent(e, JobTypes.SpeechToText))
blockChainBlockNumberSub.on('event', (e) => blockChainBlockNumberSub.on('event', (e) =>
processJobEvent(e, 'blockChain-block-number'), processJobEvent(e, JobTypes.BlockChainBlockNumber)
); )
relayInfoSub.on('event', (e) => processJobEvent(e, JobTypes.RelayInfo))
type JobType = const freeJobs: string[] = [JobTypes.BlockChainBlockNumber, JobTypes.RelayInfo]
| '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 = let jobAmount = freeJobs.includes(type) ? 0 : await priceJob(event)
type === 'blockChain-block-number' ? 0 : await priceJob(event); let output: any
let output: any; let payReqEvent: NDKEvent
let payReqEvent: NDKEvent; let paidAmount = 0
let paidAmount = 0;
const waitForPaymentBeforeProcessing = (): boolean => { const waitForPaymentBeforeProcessing = (): boolean => {
const processWithoutPaymentLimit = const processWithoutPaymentLimit = config.processWithoutPaymentLimit ?? 500
config.processWithoutPaymentLimit ?? 500; log('waitForPaymentBeforeProcessing', {
log('waitForPaymentBeforeProcessing', { jobAmount,
jobAmount, processWithoutPaymentLimit
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> => {
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);
} }
}
await startProcessing(); 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
await publishStatus(event, 'finished').catch((err) => // check if this is a payment request
console.log('err :>> ', err), const amountValue = e.tagValue('amount')
); if (!amountValue) return
if (jobAmount > paidAmount && waitForPaymentBeforePublishingResult()) { log(`found someone else's bid`, amountValue)
await reqPayment();
await waitForPayment(payReqEvent); // check if it's more-or-less than the current bid
log(`done with wait for publish`); 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> => {
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 JobTypes.Summarize: {
output = await onNewSummarizationJob(event)
break
}
case JobTypes.SpeechToText: {
output = await speechToTextJob(event)
break
}
case JobTypes.BlockChainBlockNumber: {
output = await blockChainBlockNumberJob(event)
break
}
case JobTypes.RelayInfo: {
output = await relayInfoJob(event)
break
}
} }
}
await publishResult(); const publishResult = (): void => {
log('publishResult')
complete(event, missingAmount(), { output })
}
await validateJobRequest(event)
if (jobAmount > paidAmount && waitForPaymentBeforeProcessing()) {
await reqPayment()
await waitForPayment(payReqEvent)
}
await startProcessing()
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()
} }

2
src/types/index.ts Normal file
View File

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

14
src/types/job.ts Normal file
View File

@ -0,0 +1,14 @@
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'
}

9
src/types/relay.ts Normal file
View File

@ -0,0 +1,9 @@
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,40 +1,42 @@
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(event: NDKEvent, amount?: number, publish?: boolean): Promise<NDKEvent> { export async function requirePayment(
if (!amount) { event: NDKEvent,
const bidTag = event.tagValue('bid'); amount?: number,
amount = bidTag ? parseInt(bidTag) : undefined; publish?: boolean
} ): 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: [ tags: [['status', 'payment-required']]
["status", "payment-required"], } as NostrEvent)
] await addAmount(payReq, amount)
} as NostrEvent); payReq.tag(event, 'job')
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,13 +1,18 @@
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(event: NDKEvent): Promise<void> { export default async function validateNoRecentResults(
const results = ndk.fetchEvents({ event: NDKEvent
kinds: [68002 as number], ): Promise<void> {
"#e": [event.id], const results = ndk.fetchEvents(
}, { 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,5 +1,9 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk'
export default async function validateRequester(event: NDKEvent): Promise<void> { export default async function validateRequester(
if (event) { /* empty */ } event: NDKEvent
} ): Promise<void> {
if (event) {
/* empty */
}
}