commit 04aa38bbe4faa93c1b10a10f6a77f319d6230dec Author: Danny Morabito Date: Tue Nov 26 16:06:50 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..25caa4f --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Arx Utils + +TypeScript utilities for frontend and general-purpose typescript development. + +## Installation + +```bash +bun add git+ssh://git@git.arx-ccn.com:222/Arx/ts-utils.git +``` \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..4efdcff Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..4b41963 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "@arx/utils", + "module": "src/index.ts", + "type": "module", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*" + }, + "files": [ + "src", + "README.md" + ], + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.3.2" + }, + "dependencies": { + "@cashu/cashu-ts": "^1.2.1", + "@nostr-dev-kit/ndk": "^2.10.7" + } +} diff --git a/src/cashu.ts b/src/cashu.ts new file mode 100644 index 0000000..6321261 --- /dev/null +++ b/src/cashu.ts @@ -0,0 +1,50 @@ +import {getDecodedToken} from "@cashu/cashu-ts"; +import type {Proof, TokenEntry} from "@cashu/cashu-ts"; +import {getMailSubscriptionDurationForSats} from "./general.ts"; + +class InvalidTokenException extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidTokenException'; + } +} + +export class TokenInfo { + private static readonly ALLOWED_MINTS: readonly string[] = [ + 'https://mint.minibits.cash/Bitcoin', + 'https://stablenut.umint.cash', + 'https://mint.lnvoltz.com', + 'https://mint.coinos.io', + 'https://mint.lnwallet.app', + 'https://mint.0xchat.com' + ] as const; + private static readonly MIN_AMOUNT = 21 as const; + + public readonly token: TokenEntry; + public readonly amount: number; + public readonly mint: string; + public readonly proofs: Proof[]; + + constructor(protected readonly tokenString: string) { + const decodedTokenData = getDecodedToken(tokenString); + if (decodedTokenData.unit !== 'sat' || decodedTokenData.token.length !== 1) + throw new InvalidTokenException('Invalid token format. We only accept a single token denominated in sats'); + this.token = decodedTokenData.token[0]; + this.amount = this.token.proofs.reduce((c, x) => c + x.amount, 0); + if (this.amount < TokenInfo.MIN_AMOUNT) + throw new InvalidTokenException(`Invalid amount. Minimum required: ${TokenInfo.MIN_AMOUNT} sats`); + if (!TokenInfo.ALLOWED_MINTS.includes(this.token.mint)) + throw new InvalidTokenException('Unsupported mint'); + this.mint = this.token.mint; + this.proofs = this.token.proofs; + } +} + +export class TokenInfoWithMailSubscriptionDuration extends TokenInfo { + public readonly duration: number; + + constructor(tokenString: string) { + super(tokenString); + this.duration = getMailSubscriptionDurationForSats(this.amount); + } +} diff --git a/src/email.ts b/src/email.ts new file mode 100644 index 0000000..2f7419e --- /dev/null +++ b/src/email.ts @@ -0,0 +1,34 @@ +/** + * Parses a raw email string into its constituent parts, including subject, headers, and body. + * + * @param emailText The raw email text to parse. + * @returns An object containing the extracted email components. + */ +export function parseEmail(emailText: string) { + const lines = emailText.split('\n'); + const headers: { [key: string]: string } = {}; + let bodyLines = []; + let isBody = false; + + for (let line of lines) { + if (!isBody) { + if (line.trim() === '') { + isBody = true; + continue; + } + + const colonIndex = line.indexOf(':'); + if (colonIndex !== -1) { + const key = line.slice(0, colonIndex); + headers[key] = line.slice(colonIndex + 1).trim(); + } + } else + bodyLines.push(line); + } + + return { + subject: headers['Subject'] || 'No subject', + headers, + body: bodyLines.join('\n').trim() + }; +} diff --git a/src/general.ts b/src/general.ts new file mode 100644 index 0000000..ea413a1 --- /dev/null +++ b/src/general.ts @@ -0,0 +1,10 @@ +export function randomTimeUpTo2DaysInThePast() { + const now = Date.now(); + const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000 - 3600 * 1000; // 1 hour buffer in case of clock skew + return Math.floor((Math.floor(Math.random() * (now - twoDaysAgo)) + twoDaysAgo) / 1000); +} + +export function getMailSubscriptionDurationForSats(n: number) { + const twentyOneSatsToSeconds = 8640 / 21; + return Math.floor(n * twentyOneSatsToSeconds); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..08492a2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +export * from "./cashu"; +export * from "./general"; +export * from "./nostr.ts"; +export * from "./email.ts"; \ No newline at end of file diff --git a/src/nostr.ts b/src/nostr.ts new file mode 100644 index 0000000..4b4cd80 --- /dev/null +++ b/src/nostr.ts @@ -0,0 +1,27 @@ +import NDK, {NDKEvent, NDKPrivateKeySigner, NDKUser} from "@nostr-dev-kit/ndk"; +import {generateSecretKey} from "nostr-tools"; +import {randomTimeUpTo2DaysInThePast} from "./general.ts"; + +export async function encryptEventForRecipient( + ndk: NDK, + event: NDKEvent, + recipient: NDKUser +): Promise { + await ndk.connect(); + let randomKey = generateSecretKey(); + const randomKeySinger = new NDKPrivateKeySigner(randomKey); + const seal = new NDKEvent(); + seal.pubkey = recipient.pubkey; + seal.kind = 13; + seal.content = await ndk.signer!.nip44Encrypt(recipient, JSON.stringify(event)); + seal.created_at = randomTimeUpTo2DaysInThePast(); + await seal.sign(ndk.signer); + const giftWrap = new NDKEvent(); + giftWrap.kind = 1059; + giftWrap.created_at = randomTimeUpTo2DaysInThePast(); + giftWrap.content = await randomKeySinger.nip44Encrypt(recipient, JSON.stringify(seal)); + giftWrap.tags.push(['p', recipient.pubkey]); + await giftWrap.sign(randomKeySinger); + giftWrap.ndk = ndk; + return giftWrap; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0aba36f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "allowJs": false, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": false + } +}