Fix fee calculation and improve token spendability detection
- Fix critical fee calculation bug: Now gets exact melt quote before creating invoice - Improve spent token detection: Only marks as spent with clear indicators - Add spent field to decode endpoint response (always boolean) - Add informative root endpoint with API documentation - Update documentation examples to use cashuB format - Install bolt11 library for proper Lightning invoice verification - Enhanced error handling and logging throughout This fixes the issue where users lost sats due to fee estimation errors and ensures accurate token spendability detection.
This commit is contained in:
@@ -34,7 +34,7 @@ Decode a Cashu token and return its content. Supports both v1 and v3 token forma
|
|||||||
**Request:**
|
**Request:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "cashuAeyJhbGciOi..."
|
"token": "cashuB..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -47,7 +47,8 @@ Decode a Cashu token and return its content. Supports both v1 and v3 token forma
|
|||||||
"totalAmount": 21000,
|
"totalAmount": 21000,
|
||||||
"numProofs": 3,
|
"numProofs": 3,
|
||||||
"denominations": [1000, 10000, 10000],
|
"denominations": [1000, 10000, 10000],
|
||||||
"format": "cashuA"
|
"format": "cashuA",
|
||||||
|
"spent": false
|
||||||
},
|
},
|
||||||
"mint_url": "https://mint.azzamo.net"
|
"mint_url": "https://mint.azzamo.net"
|
||||||
}
|
}
|
||||||
@@ -59,7 +60,7 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if
|
|||||||
**Request:**
|
**Request:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "cashuAeyJhbGciOi...",
|
"token": "cashuB...",
|
||||||
"lightningAddress": "user@ln.tips"
|
"lightningAddress": "user@ln.tips"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -67,7 +68,7 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if
|
|||||||
**Request (using default address):**
|
**Request (using default address):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "cashuAeyJhbGciOi..."
|
"token": "cashuB..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
466
package-lock.json
generated
466
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cashu/cashu-ts": "^1.1.0",
|
"@cashu/cashu-ts": "^1.1.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
"bolt11": "^1.4.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
@@ -431,6 +432,14 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bn.js": {
|
||||||
|
"version": "4.11.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
|
||||||
|
"integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||||
@@ -444,6 +453,14 @@
|
|||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "24.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz",
|
||||||
|
"integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -545,6 +562,20 @@
|
|||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/available-typed-arrays": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"possible-typed-array-names": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||||
@@ -562,6 +593,11 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base-x": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw=="
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -582,6 +618,11 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bech32": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -595,6 +636,40 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bip174": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bitcoinjs-lib": {
|
||||||
|
"version": "6.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz",
|
||||||
|
"integrity": "sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "^1.2.0",
|
||||||
|
"bech32": "^2.0.0",
|
||||||
|
"bip174": "^2.1.1",
|
||||||
|
"bs58check": "^3.0.1",
|
||||||
|
"typeforce": "^1.11.3",
|
||||||
|
"varuint-bitcoin": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bitcoinjs-lib/node_modules/bech32": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
|
||||||
|
},
|
||||||
|
"node_modules/bn.js": {
|
||||||
|
"version": "4.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||||
|
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
@@ -619,6 +694,21 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bolt11": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bolt11/-/bolt11-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-jR0Y+MO+CK2at1Cg5mltLJ+6tdOwNKoTS/DJOBDdzVkQ+R9D6UgZMayTWOsuzY7OgV1gEqlyT5Tzk6t6r4XcNQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/bn.js": "^4.11.3",
|
||||||
|
"bech32": "^1.1.2",
|
||||||
|
"bitcoinjs-lib": "^6.0.0",
|
||||||
|
"bn.js": "^4.11.8",
|
||||||
|
"create-hash": "^1.2.0",
|
||||||
|
"lodash": "^4.17.11",
|
||||||
|
"safe-buffer": "^5.1.1",
|
||||||
|
"secp256k1": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@@ -642,6 +732,28 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/brorand": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
|
||||||
|
},
|
||||||
|
"node_modules/bs58": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bs58check": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "^1.2.0",
|
||||||
|
"bs58": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
@@ -675,6 +787,23 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.0",
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"set-function-length": "^1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind-apply-helpers": {
|
"node_modules/call-bind-apply-helpers": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
@@ -775,6 +904,18 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cipher-base": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"safe-buffer": "^5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -871,6 +1012,18 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/create-hash": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
|
||||||
|
"dependencies": {
|
||||||
|
"cipher-base": "^1.0.1",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"md5.js": "^1.3.4",
|
||||||
|
"ripemd160": "^2.0.1",
|
||||||
|
"sha.js": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -902,6 +1055,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/define-data-property": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -974,6 +1143,20 @@
|
|||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/elliptic": {
|
||||||
|
"version": "6.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
|
||||||
|
"integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^4.11.9",
|
||||||
|
"brorand": "^1.1.0",
|
||||||
|
"hash.js": "^1.0.0",
|
||||||
|
"hmac-drbg": "^1.0.1",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"minimalistic-assert": "^1.0.1",
|
||||||
|
"minimalistic-crypto-utils": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
@@ -1404,6 +1587,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/for-each": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-callable": "^1.2.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||||
@@ -1573,6 +1770,17 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-property-descriptors": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-symbols": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
@@ -1600,6 +1808,28 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hash-base": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.6.0",
|
||||||
|
"safe-buffer": "^5.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hash.js": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"minimalistic-assert": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
@@ -1612,6 +1842,16 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hmac-drbg": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
|
||||||
|
"dependencies": {
|
||||||
|
"hash.js": "^1.0.3",
|
||||||
|
"minimalistic-assert": "^1.0.0",
|
||||||
|
"minimalistic-crypto-utils": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
@@ -1743,6 +1983,17 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-callable": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -1776,6 +2027,25 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-typed-array": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"which-typed-array": "^1.1.16"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
|
||||||
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -1856,6 +2126,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
"node_modules/lodash.get": {
|
"node_modules/lodash.get": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
@@ -1892,6 +2167,16 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/md5.js": {
|
||||||
|
"version": "1.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||||
|
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
|
||||||
|
"dependencies": {
|
||||||
|
"hash-base": "^3.0.0",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/media-typer": {
|
"node_modules/media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
@@ -1952,6 +2237,16 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimalistic-assert": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||||
|
},
|
||||||
|
"node_modules/minimalistic-crypto-utils": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -1986,6 +2281,21 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
|
||||||
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
||||||
@@ -2242,6 +2552,14 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/possible-typed-array-names": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -2327,6 +2645,19 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -2350,6 +2681,15 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ripemd160": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
|
||||||
|
"dependencies": {
|
||||||
|
"hash-base": "^3.0.0",
|
||||||
|
"inherits": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -2376,6 +2716,20 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/secp256k1": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"elliptic": "^6.5.7",
|
||||||
|
"node-addon-api": "^5.0.0",
|
||||||
|
"node-gyp-build": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
@@ -2443,12 +2797,47 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-function-length": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.1.4",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"gopd": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sha.js": {
|
||||||
|
"version": "2.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
|
||||||
|
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"safe-buffer": "^5.2.1",
|
||||||
|
"to-buffer": "^1.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sha.js": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -2566,6 +2955,14 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
@@ -2648,6 +3045,19 @@
|
|||||||
"express": ">=4.0.0 || >=5.0.0-beta"
|
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/to-buffer": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"isarray": "^2.0.5",
|
||||||
|
"safe-buffer": "^5.2.1",
|
||||||
|
"typed-array-buffer": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -2706,6 +3116,24 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typed-array-buffer": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.3",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"is-typed-array": "^1.1.14"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typeforce": {
|
||||||
|
"version": "1.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
|
||||||
|
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||||
|
},
|
||||||
"node_modules/undefsafe": {
|
"node_modules/undefsafe": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||||
@@ -2713,6 +3141,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||||
|
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
@@ -2732,6 +3165,11 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
@@ -2763,6 +3201,14 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/varuint-bitcoin": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@@ -2788,6 +3234,26 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-typed-array": {
|
||||||
|
"version": "1.1.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
||||||
|
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
|
||||||
|
"dependencies": {
|
||||||
|
"available-typed-arrays": "^1.0.7",
|
||||||
|
"call-bind": "^1.0.8",
|
||||||
|
"call-bound": "^1.0.4",
|
||||||
|
"for-each": "^0.3.5",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-tostringtag": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cashu/cashu-ts": "^1.1.0",
|
"@cashu/cashu-ts": "^1.1.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
"bolt11": "^1.4.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
@@ -34,8 +35,8 @@
|
|||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.4",
|
"eslint": "^9.9.1",
|
||||||
"eslint": "^9.9.1"
|
"nodemon": "^3.1.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0",
|
"node": ">=18.0.0",
|
||||||
|
|||||||
120
server.js
120
server.js
@@ -79,6 +79,78 @@ function asyncHandler(fn) {
|
|||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /:
|
||||||
|
* get:
|
||||||
|
* summary: API Information
|
||||||
|
* description: Get basic information about the Cashu Redeem API
|
||||||
|
* tags: [General]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: API information
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* example: "Cashu Redeem API"
|
||||||
|
* version:
|
||||||
|
* type: string
|
||||||
|
* example: "1.0.0"
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* example: "A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses"
|
||||||
|
* documentation:
|
||||||
|
* type: string
|
||||||
|
* example: "/docs"
|
||||||
|
* endpoints:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* decode:
|
||||||
|
* type: string
|
||||||
|
* example: "POST /api/decode"
|
||||||
|
* redeem:
|
||||||
|
* type: string
|
||||||
|
* example: "POST /api/redeem"
|
||||||
|
* validate:
|
||||||
|
* type: string
|
||||||
|
* example: "POST /api/validate-address"
|
||||||
|
* health:
|
||||||
|
* type: string
|
||||||
|
* example: "GET /api/health"
|
||||||
|
* github:
|
||||||
|
* type: string
|
||||||
|
* example: "https://github.com/yourusername/cashu-redeem-api"
|
||||||
|
*/
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
name: 'Cashu Redeem API',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses using the cashu-ts library and LNURLp protocol',
|
||||||
|
documentation: '/docs',
|
||||||
|
endpoints: {
|
||||||
|
decode: 'POST /api/decode',
|
||||||
|
redeem: 'POST /api/redeem',
|
||||||
|
validate: 'POST /api/validate-address',
|
||||||
|
health: 'GET /api/health'
|
||||||
|
},
|
||||||
|
features: [
|
||||||
|
'Decode Cashu tokens',
|
||||||
|
'Redeem tokens to Lightning addresses',
|
||||||
|
'Lightning address validation',
|
||||||
|
'Domain restrictions',
|
||||||
|
'Rate limiting',
|
||||||
|
'Comprehensive error handling'
|
||||||
|
],
|
||||||
|
github: 'https://github.com/yourusername/cashu-redeem-api'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API Routes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/decode:
|
* /api/decode:
|
||||||
@@ -128,6 +200,44 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
|
|||||||
const decoded = await cashuService.parseToken(token);
|
const decoded = await cashuService.parseToken(token);
|
||||||
const mintUrl = await cashuService.getTokenMintUrl(token);
|
const mintUrl = await cashuService.getTokenMintUrl(token);
|
||||||
|
|
||||||
|
// Check if token is spent
|
||||||
|
let spent = false;
|
||||||
|
try {
|
||||||
|
const spendabilityCheck = await cashuService.checkTokenSpendable(token);
|
||||||
|
// Token is spent if no proofs are spendable
|
||||||
|
spent = !spendabilityCheck.spendable || spendabilityCheck.spendable.length === 0;
|
||||||
|
} catch (error) {
|
||||||
|
// If spendability check fails, analyze the error to determine if token is spent
|
||||||
|
console.warn('Spendability check failed:', error.message);
|
||||||
|
|
||||||
|
// Check if error indicates proofs are already spent
|
||||||
|
const errorString = error.message || error.toString();
|
||||||
|
|
||||||
|
// Check for specific error indicators
|
||||||
|
if (errorString.includes('TOKEN_SPENT:')) {
|
||||||
|
// CashuService has determined the token is spent based on clear indicators
|
||||||
|
console.log('Token determined to be spent by CashuService');
|
||||||
|
spent = true;
|
||||||
|
} else if (errorString.includes('Token validation failed at mint:')) {
|
||||||
|
// This is a 422 error but not clearly indicating the token is spent
|
||||||
|
// It might be invalid/malformed but not necessarily spent
|
||||||
|
console.log('Token validation failed at mint - assuming token is still valid (might be invalid format)');
|
||||||
|
spent = false;
|
||||||
|
} else if (errorString.includes('not supported') ||
|
||||||
|
errorString.includes('endpoint not found') ||
|
||||||
|
errorString.includes('may still be valid') ||
|
||||||
|
errorString.includes('does not support spendability checking')) {
|
||||||
|
// Mint doesn't support spendability checking - assume token is still valid
|
||||||
|
console.log('Mint does not support spendability checking - assuming token is valid');
|
||||||
|
spent = false;
|
||||||
|
} else {
|
||||||
|
// For other errors (network, server issues), assume token is still valid
|
||||||
|
// This is safer than assuming it's spent
|
||||||
|
console.log('Unknown error - assuming token is valid');
|
||||||
|
spent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
decoded: {
|
decoded: {
|
||||||
@@ -135,7 +245,8 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
|
|||||||
totalAmount: decoded.totalAmount,
|
totalAmount: decoded.totalAmount,
|
||||||
numProofs: decoded.numProofs,
|
numProofs: decoded.numProofs,
|
||||||
denominations: decoded.denominations,
|
denominations: decoded.denominations,
|
||||||
format: decoded.format
|
format: decoded.format,
|
||||||
|
spent: spent
|
||||||
},
|
},
|
||||||
mint_url: mintUrl
|
mint_url: mintUrl
|
||||||
});
|
});
|
||||||
@@ -157,13 +268,14 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
|
|||||||
*
|
*
|
||||||
* The redemption process includes:
|
* The redemption process includes:
|
||||||
* 1. Token validation and parsing
|
* 1. Token validation and parsing
|
||||||
* 2. Fee calculation (NUT-05: 2% of amount, minimum 1 satoshi)
|
* 2. Getting exact melt quote from mint to determine precise fees
|
||||||
* 3. Invoice creation for net amount (token amount - fees)
|
* 3. Invoice creation for net amount (token amount - exact fees)
|
||||||
* 4. Spendability checking at the mint
|
* 4. Spendability checking at the mint
|
||||||
* 5. Token melting and Lightning payment
|
* 5. Token melting and Lightning payment
|
||||||
*
|
*
|
||||||
* **Important**: Fees are subtracted from the token amount before creating the Lightning invoice.
|
* **Important**: The system gets the exact fee from the mint before creating the invoice.
|
||||||
* The `invoiceAmount` field shows the actual amount sent to the Lightning address.
|
* The `invoiceAmount` field shows the actual amount sent to the Lightning address.
|
||||||
|
* No sats are lost to fee estimation errors.
|
||||||
* tags: [Token Operations]
|
* tags: [Token Operations]
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
|
|||||||
@@ -184,6 +184,36 @@ class CashuService {
|
|||||||
return this.wallets.get(mintUrl);
|
return this.wallets.get(mintUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get melt quote for a Cashu token and Lightning invoice
|
||||||
|
* @param {string} token - The encoded Cashu token
|
||||||
|
* @param {string} bolt11 - The Lightning invoice
|
||||||
|
* @returns {Object} Melt quote
|
||||||
|
*/
|
||||||
|
async getMeltQuote(token, bolt11) {
|
||||||
|
try {
|
||||||
|
const parsed = await this.parseToken(token);
|
||||||
|
const wallet = await this.getWallet(parsed.mint);
|
||||||
|
|
||||||
|
// Create melt quote to get fee estimate
|
||||||
|
const meltQuote = await wallet.createMeltQuote(bolt11);
|
||||||
|
|
||||||
|
console.log('Melt quote created:', {
|
||||||
|
amount: meltQuote.amount,
|
||||||
|
fee_reserve: meltQuote.fee_reserve,
|
||||||
|
quote: meltQuote.quote
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: meltQuote.amount,
|
||||||
|
fee_reserve: meltQuote.fee_reserve,
|
||||||
|
quote: meltQuote.quote
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to get melt quote: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Melt a Cashu token to pay a Lightning invoice
|
* Melt a Cashu token to pay a Lightning invoice
|
||||||
* @param {string} token - The encoded Cashu token
|
* @param {string} token - The encoded Cashu token
|
||||||
@@ -199,20 +229,36 @@ class CashuService {
|
|||||||
const decoded = await this.decodeTokenStructure(token);
|
const decoded = await this.decodeTokenStructure(token);
|
||||||
const proofs = decoded.proofs;
|
const proofs = decoded.proofs;
|
||||||
|
|
||||||
// Create melt quote to get fee estimate
|
// Step 1: Create melt quote to get fee estimate
|
||||||
const meltQuote = await wallet.createMeltQuote(bolt11);
|
const meltQuote = await wallet.createMeltQuote(bolt11);
|
||||||
|
console.log('Melt quote created:', {
|
||||||
|
amount: meltQuote.amount,
|
||||||
|
fee_reserve: meltQuote.fee_reserve,
|
||||||
|
quote: meltQuote.quote
|
||||||
|
});
|
||||||
|
console.log('Paying invoice:', bolt11.substring(0, 50) + '...');
|
||||||
|
console.log('Full invoice being paid:', bolt11);
|
||||||
|
|
||||||
// Calculate expected fee
|
// Step 2: Calculate total required (amount + fee_reserve)
|
||||||
const expectedFee = this.calculateFee(parsed.totalAmount);
|
const total = meltQuote.amount + meltQuote.fee_reserve;
|
||||||
|
console.log('Total required:', total, 'sats (amount:', meltQuote.amount, '+ fee:', meltQuote.fee_reserve, ')');
|
||||||
|
console.log('Available in token:', parsed.totalAmount, 'sats');
|
||||||
|
|
||||||
// Check if we have sufficient funds including fees
|
// Check if we have sufficient funds
|
||||||
const totalRequired = meltQuote.amount + meltQuote.fee_reserve;
|
if (total > parsed.totalAmount) {
|
||||||
if (totalRequired > parsed.totalAmount) {
|
throw new Error(`Insufficient funds. Required: ${total} sats (including ${meltQuote.fee_reserve} sats fee), Available: ${parsed.totalAmount} sats`);
|
||||||
throw new Error(`Insufficient funds. Required: ${totalRequired} sats (including ${meltQuote.fee_reserve} sats fee), Available: ${parsed.totalAmount} sats`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the melt operation using the quote and proofs
|
// Step 3: Send tokens with includeFees: true to get the right proofs
|
||||||
const meltResponse = await wallet.meltTokens(meltQuote, proofs);
|
console.log('Selecting proofs with includeFees: true for', total, 'sats');
|
||||||
|
const { send: proofsToSend } = await wallet.send(total, proofs, {
|
||||||
|
includeFees: true,
|
||||||
|
});
|
||||||
|
console.log('Selected', proofsToSend.length, 'proofs for melting');
|
||||||
|
|
||||||
|
// Step 4: Perform the melt operation using the quote and selected proofs
|
||||||
|
console.log('Performing melt operation...');
|
||||||
|
const meltResponse = await wallet.meltTokens(meltQuote, proofsToSend);
|
||||||
|
|
||||||
// Debug: Log the melt response structure
|
// Debug: Log the melt response structure
|
||||||
console.log('Melt response:', JSON.stringify(meltResponse, null, 2));
|
console.log('Melt response:', JSON.stringify(meltResponse, null, 2));
|
||||||
@@ -245,7 +291,6 @@ class CashuService {
|
|||||||
change: meltResponse.change || [],
|
change: meltResponse.change || [],
|
||||||
amount: meltQuote.amount,
|
amount: meltQuote.amount,
|
||||||
fee: actualFeeCharged, // Use actual fee from melt response
|
fee: actualFeeCharged, // Use actual fee from melt response
|
||||||
actualFee: expectedFee, // Keep the calculated expected fee for comparison
|
|
||||||
netAmount: actualNetAmount, // Use net amount based on actual fee
|
netAmount: actualNetAmount, // Use net amount based on actual fee
|
||||||
quote: meltQuote.quote,
|
quote: meltQuote.quote,
|
||||||
rawMeltResponse: meltResponse // Include raw response for debugging
|
rawMeltResponse: meltResponse // Include raw response for debugging
|
||||||
@@ -330,43 +375,44 @@ class CashuService {
|
|||||||
totalAmount: parsed.totalAmount
|
totalAmount: parsed.totalAmount
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Check if it's a known 422 error (already spent token) - log less verbosely
|
// Enhanced error logging for debugging
|
||||||
const isExpected422Error = (error.status === 422 || error.response?.status === 422) &&
|
console.error('Spendability check error details:', {
|
||||||
error.constructor.name === 'HttpResponseError';
|
errorType: error.constructor.name,
|
||||||
|
errorMessage: error.message,
|
||||||
if (isExpected422Error) {
|
errorCode: error.code,
|
||||||
console.log('Token spendability check: 422 status detected - token already spent');
|
errorStatus: error.status,
|
||||||
} else {
|
errorResponse: error.response,
|
||||||
// Enhanced error logging for unexpected errors
|
errorData: error.data,
|
||||||
console.error('Spendability check error details:', {
|
errorStack: error.stack,
|
||||||
errorType: error.constructor.name,
|
errorString: String(error)
|
||||||
errorMessage: error.message,
|
});
|
||||||
errorCode: error.code,
|
|
||||||
errorStatus: error.status,
|
|
||||||
errorResponse: error.response,
|
|
||||||
errorData: error.data,
|
|
||||||
errorStack: error.stack,
|
|
||||||
errorString: String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle different types of errors
|
// Handle different types of errors
|
||||||
let errorMessage = 'Unknown error occurred';
|
let errorMessage = 'Unknown error occurred';
|
||||||
|
|
||||||
// Handle cashu-ts HttpResponseError specifically
|
// Handle cashu-ts HttpResponseError specifically
|
||||||
if (error.constructor.name === 'HttpResponseError') {
|
if (error.constructor.name === 'HttpResponseError') {
|
||||||
if (!isExpected422Error) {
|
|
||||||
console.log('HttpResponseError detected, extracting details...');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract status code first
|
// Extract status code first
|
||||||
const status = error.status || error.response?.status || error.statusCode;
|
const status = error.status || error.response?.status || error.statusCode;
|
||||||
|
|
||||||
// For 422 errors, we know it's about already spent tokens
|
// For 422 errors, we need to be more specific about the reason
|
||||||
if (status === 422) {
|
if (status === 422) {
|
||||||
errorMessage = 'Token proofs are not spendable - they have already been used or are invalid';
|
// Try to get more details about the 422 error
|
||||||
if (!isExpected422Error) {
|
let responseBody = null;
|
||||||
console.log('Detected 422 status - token already spent');
|
try {
|
||||||
|
responseBody = error.response?.data || error.data || error.body;
|
||||||
|
console.log('HTTP 422 response body:', responseBody);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not extract response body');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 422 can mean different things, let's be more specific
|
||||||
|
if (responseBody && typeof responseBody === 'object' && responseBody.detail) {
|
||||||
|
errorMessage = `Token validation failed: ${responseBody.detail}`;
|
||||||
|
console.log('422 error with detail:', responseBody.detail);
|
||||||
|
} else {
|
||||||
|
errorMessage = 'Token proofs are not spendable - they may have already been used or are invalid';
|
||||||
|
console.log('Detected 422 status - token validation failed');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Try to extract useful information from the HTTP response error
|
// Try to extract useful information from the HTTP response error
|
||||||
@@ -432,9 +478,7 @@ class CashuService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log the final extracted error message for debugging
|
// Log the final extracted error message for debugging
|
||||||
if (!isExpected422Error) {
|
console.log('Final extracted error message:', errorMessage);
|
||||||
console.log('Final extracted error message:', errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a known error pattern indicating unsupported operation
|
// Check if it's a known error pattern indicating unsupported operation
|
||||||
if (errorMessage.includes('not supported') ||
|
if (errorMessage.includes('not supported') ||
|
||||||
@@ -448,6 +492,26 @@ class CashuService {
|
|||||||
throw new Error('This mint does not support spendability checking. Token may still be valid.');
|
throw new Error('This mint does not support spendability checking. Token may still be valid.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the error indicates the token is spent (HTTP 422 or specific messages)
|
||||||
|
const status = error.status || error.response?.status || error.statusCode;
|
||||||
|
if (status === 422) {
|
||||||
|
// For 422 errors, we need to be more careful about determining if it's "spent" vs "invalid"
|
||||||
|
// Only mark as spent if we have clear indicators
|
||||||
|
if (errorMessage.includes('already been used') ||
|
||||||
|
errorMessage.includes('already spent') ||
|
||||||
|
errorMessage.includes('not spendable')) {
|
||||||
|
throw new Error('TOKEN_SPENT: Token proofs are not spendable - they have already been used');
|
||||||
|
} else {
|
||||||
|
// For other 422 errors, it might be invalid but not necessarily spent
|
||||||
|
console.log('HTTP 422 but not clearly indicating spent token - treating as validation error');
|
||||||
|
throw new Error(`Token validation failed at mint: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
} else if (errorMessage.includes('Token proofs are not spendable') ||
|
||||||
|
errorMessage.includes('already been used') ||
|
||||||
|
errorMessage.includes('invalid proofs')) {
|
||||||
|
throw new Error('TOKEN_SPENT: Token proofs are not spendable - they have already been used');
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`Failed to check token spendability: ${errorMessage}`);
|
throw new Error(`Failed to check token spendability: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const bolt11 = require('bolt11');
|
||||||
|
|
||||||
class LightningService {
|
class LightningService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -220,11 +221,19 @@ class LightningService {
|
|||||||
*/
|
*/
|
||||||
async resolveInvoice(lightningAddress, amount, comment = 'Cashu token redemption') {
|
async resolveInvoice(lightningAddress, amount, comment = 'Cashu token redemption') {
|
||||||
try {
|
try {
|
||||||
|
console.log(`Resolving Lightning address: ${lightningAddress} for ${amount} sats`);
|
||||||
|
|
||||||
// Get LNURLp endpoint
|
// Get LNURLp endpoint
|
||||||
const lnurlpUrl = this.getLNURLpEndpoint(lightningAddress);
|
const lnurlpUrl = this.getLNURLpEndpoint(lightningAddress);
|
||||||
|
console.log(`LNURLp endpoint: ${lnurlpUrl}`);
|
||||||
|
|
||||||
// Fetch LNURLp response
|
// Fetch LNURLp response
|
||||||
const lnurlpResponse = await this.fetchLNURLpResponse(lnurlpUrl);
|
const lnurlpResponse = await this.fetchLNURLpResponse(lnurlpUrl);
|
||||||
|
console.log('LNURLp response:', {
|
||||||
|
callback: lnurlpResponse.callback,
|
||||||
|
minSendable: lnurlpResponse.minSendable,
|
||||||
|
maxSendable: lnurlpResponse.maxSendable
|
||||||
|
});
|
||||||
|
|
||||||
// Validate amount
|
// Validate amount
|
||||||
if (!this.validateAmount(amount, lnurlpResponse)) {
|
if (!this.validateAmount(amount, lnurlpResponse)) {
|
||||||
@@ -235,7 +244,17 @@ class LightningService {
|
|||||||
|
|
||||||
// Get invoice
|
// Get invoice
|
||||||
const amountMsats = this.satsToMillisats(amount);
|
const amountMsats = this.satsToMillisats(amount);
|
||||||
|
console.log(`Requesting invoice for ${amountMsats} millisats (${amount} sats)`);
|
||||||
|
console.log(`Using callback URL: ${lnurlpResponse.callback}`);
|
||||||
const invoiceResponse = await this.getInvoice(lnurlpResponse.callback, amountMsats, comment);
|
const invoiceResponse = await this.getInvoice(lnurlpResponse.callback, amountMsats, comment);
|
||||||
|
|
||||||
|
console.log('Invoice created successfully:', {
|
||||||
|
bolt11: invoiceResponse.bolt11.substring(0, 50) + '...',
|
||||||
|
lightningAddress,
|
||||||
|
amount,
|
||||||
|
amountMsats,
|
||||||
|
callback: lnurlpResponse.callback
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bolt11: invoiceResponse.bolt11,
|
bolt11: invoiceResponse.bolt11,
|
||||||
@@ -247,6 +266,7 @@ class LightningService {
|
|||||||
lnurlpResponse
|
lnurlpResponse
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Lightning address resolution failed:', error.message);
|
||||||
throw new Error(`Lightning address resolution failed: ${error.message}`);
|
throw new Error(`Lightning address resolution failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,6 +291,62 @@ class LightningService {
|
|||||||
throw new Error(`Invoice parsing failed: ${error.message}`);
|
throw new Error(`Invoice parsing failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a Lightning invoice is valid and for the expected amount
|
||||||
|
* @param {string} bolt11Invoice - The Lightning invoice to verify
|
||||||
|
* @param {string} expectedLightningAddress - The expected Lightning address (for logging)
|
||||||
|
* @param {number} expectedAmount - Expected amount in satoshis (optional)
|
||||||
|
* @returns {boolean} Whether the invoice is valid
|
||||||
|
*/
|
||||||
|
verifyInvoiceDestination(bolt11Invoice, expectedLightningAddress, expectedAmount = null) {
|
||||||
|
try {
|
||||||
|
console.log(`Verifying invoice destination for: ${expectedLightningAddress}`);
|
||||||
|
console.log(`Invoice: ${bolt11Invoice.substring(0, 50)}...`);
|
||||||
|
|
||||||
|
// Decode the invoice using the bolt11 library
|
||||||
|
const decoded = bolt11.decode(bolt11Invoice);
|
||||||
|
|
||||||
|
// Basic validation checks
|
||||||
|
if (!decoded.complete) {
|
||||||
|
console.error('Invoice verification failed: Invoice is incomplete');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decoded.paymentRequest) {
|
||||||
|
console.error('Invoice verification failed: No payment request found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the invoice has expired
|
||||||
|
if (decoded.timeExpireDate && decoded.timeExpireDate < Date.now() / 1000) {
|
||||||
|
console.error('Invoice verification failed: Invoice has expired');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify amount if provided
|
||||||
|
if (expectedAmount !== null) {
|
||||||
|
const invoiceAmount = decoded.satoshis || (decoded.millisatoshis ? Math.floor(decoded.millisatoshis / 1000) : 0);
|
||||||
|
if (invoiceAmount !== expectedAmount) {
|
||||||
|
console.error(`Invoice verification failed: Amount mismatch. Expected: ${expectedAmount} sats, Got: ${invoiceAmount} sats`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Invoice verification: All checks passed');
|
||||||
|
console.log('Invoice details:', {
|
||||||
|
amount: decoded.satoshis || (decoded.millisatoshis ? Math.floor(decoded.millisatoshis / 1000) : 0),
|
||||||
|
timestamp: decoded.timestamp,
|
||||||
|
expiry: decoded.expiry,
|
||||||
|
description: decoded.tags?.find(tag => tag.tagName === 'description')?.data || 'No description'
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Invoice verification failed:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new LightningService();
|
module.exports = new LightningService();
|
||||||
@@ -167,23 +167,10 @@ class RedemptionService {
|
|||||||
this.updateRedemption(redeemId, { status: 'parsing_token' });
|
this.updateRedemption(redeemId, { status: 'parsing_token' });
|
||||||
const tokenData = await cashuService.parseToken(token);
|
const tokenData = await cashuService.parseToken(token);
|
||||||
|
|
||||||
// Calculate expected fee according to NUT-05
|
|
||||||
const expectedFee = cashuService.calculateFee(tokenData.totalAmount);
|
|
||||||
|
|
||||||
// Calculate net amount after subtracting fees
|
|
||||||
const netAmountAfterFee = tokenData.totalAmount - expectedFee;
|
|
||||||
|
|
||||||
// Ensure we have enough for the minimum payment after fees
|
|
||||||
if (netAmountAfterFee <= 0) {
|
|
||||||
throw new Error(`Token amount (${tokenData.totalAmount} sats) is insufficient to cover the minimum fee (${expectedFee} sats)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateRedemption(redeemId, {
|
this.updateRedemption(redeemId, {
|
||||||
amount: tokenData.totalAmount,
|
amount: tokenData.totalAmount,
|
||||||
mint: tokenData.mint,
|
mint: tokenData.mint,
|
||||||
numProofs: tokenData.numProofs,
|
numProofs: tokenData.numProofs,
|
||||||
expectedFee: expectedFee,
|
|
||||||
netAmountAfterFee: netAmountAfterFee,
|
|
||||||
format: tokenData.format
|
format: tokenData.format
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,26 +190,58 @@ class RedemptionService {
|
|||||||
// This is likely an already-spent token - fail the redemption with clear message
|
// This is likely an already-spent token - fail the redemption with clear message
|
||||||
throw new Error('This token has already been spent and cannot be redeemed again');
|
throw new Error('This token has already been spent and cannot be redeemed again');
|
||||||
}
|
}
|
||||||
// Log but don't fail for other errors - some mints might not support this check
|
// For other errors, log but continue - some mints might not support this check
|
||||||
console.warn('Spendability check failed:', spendError.message);
|
console.warn('Spendability check failed:', spendError.message);
|
||||||
|
console.log('Continuing with redemption despite spendability check failure...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Resolve Lightning address to invoice
|
// Step 2: Get melt quote first to determine exact fees
|
||||||
// IMPORTANT: Create invoice for net amount (after subtracting expected fees)
|
this.updateRedemption(redeemId, { status: 'getting_melt_quote' });
|
||||||
|
|
||||||
|
// Create a temporary invoice to get the melt quote (we'll create the real one after)
|
||||||
|
console.log(`Getting melt quote for ${tokenData.totalAmount} sats to determine exact fees`);
|
||||||
|
const tempInvoiceData = await lightningService.resolveInvoice(
|
||||||
|
lightningAddressToUse,
|
||||||
|
tokenData.totalAmount, // Use full amount initially
|
||||||
|
'Cashu redemption'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get melt quote to determine exact fee
|
||||||
|
const meltQuote = await cashuService.getMeltQuote(token, tempInvoiceData.bolt11);
|
||||||
|
const exactFee = meltQuote.fee_reserve;
|
||||||
|
const finalInvoiceAmount = tokenData.totalAmount - exactFee;
|
||||||
|
|
||||||
|
console.log(`Melt quote: amount=${meltQuote.amount}, fee=${exactFee}, net to user=${finalInvoiceAmount}`);
|
||||||
|
|
||||||
|
// Step 3: Create final invoice for the correct amount (total - exact fee)
|
||||||
this.updateRedemption(redeemId, { status: 'resolving_invoice' });
|
this.updateRedemption(redeemId, { status: 'resolving_invoice' });
|
||||||
|
|
||||||
|
if (finalInvoiceAmount <= 0) {
|
||||||
|
throw new Error(`Token amount (${tokenData.totalAmount} sats) is insufficient to cover the fee (${exactFee} sats)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Creating final invoice for ${finalInvoiceAmount} sats (${tokenData.totalAmount} - ${exactFee} fee)`);
|
||||||
|
|
||||||
const invoiceData = await lightningService.resolveInvoice(
|
const invoiceData = await lightningService.resolveInvoice(
|
||||||
lightningAddressToUse,
|
lightningAddressToUse,
|
||||||
netAmountAfterFee, // Use net amount instead of full token amount
|
finalInvoiceAmount, // Use amount minus exact fee
|
||||||
'Cashu redemption'
|
'Cashu redemption'
|
||||||
);
|
);
|
||||||
|
|
||||||
this.updateRedemption(redeemId, {
|
this.updateRedemption(redeemId, {
|
||||||
bolt11: invoiceData.bolt11.substring(0, 50) + '...',
|
bolt11: invoiceData.bolt11.substring(0, 50) + '...',
|
||||||
domain: invoiceData.domain,
|
domain: invoiceData.domain,
|
||||||
invoiceAmount: netAmountAfterFee
|
invoiceAmount: finalInvoiceAmount,
|
||||||
|
exactFee: exactFee
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 3: Melt the token to pay the invoice
|
// Verify the invoice is valid and for the correct amount
|
||||||
|
const invoiceVerified = lightningService.verifyInvoiceDestination(invoiceData.bolt11, lightningAddressToUse, finalInvoiceAmount);
|
||||||
|
if (!invoiceVerified) {
|
||||||
|
throw new Error('Invoice verification failed - invalid invoice or amount mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Melt the token to pay the invoice
|
||||||
this.updateRedemption(redeemId, { status: 'melting_token' });
|
this.updateRedemption(redeemId, { status: 'melting_token' });
|
||||||
const meltResult = await cashuService.meltToken(token, invoiceData.bolt11);
|
const meltResult = await cashuService.meltToken(token, invoiceData.bolt11);
|
||||||
|
|
||||||
@@ -256,12 +275,12 @@ class RedemptionService {
|
|||||||
redeemId,
|
redeemId,
|
||||||
paid: paymentSuccessful,
|
paid: paymentSuccessful,
|
||||||
amount: tokenData.totalAmount,
|
amount: tokenData.totalAmount,
|
||||||
invoiceAmount: netAmountAfterFee, // Amount actually sent in the invoice
|
invoiceAmount: finalInvoiceAmount, // Amount actually sent in the invoice
|
||||||
to: lightningAddressToUse,
|
to: lightningAddressToUse,
|
||||||
usingDefaultAddress: isUsingDefault,
|
usingDefaultAddress: isUsingDefault,
|
||||||
fee: meltResult.fee,
|
fee: exactFee, // Use the exact fee from the melt quote
|
||||||
actualFee: meltResult.actualFee,
|
actualFee: meltResult.actualFee,
|
||||||
netAmount: meltResult.netAmount,
|
netAmount: finalInvoiceAmount, // This is the net amount the user receives
|
||||||
preimage: meltResult.preimage,
|
preimage: meltResult.preimage,
|
||||||
change: meltResult.change,
|
change: meltResult.change,
|
||||||
mint: tokenData.mint,
|
mint: tokenData.mint,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const options = {
|
|||||||
token: {
|
token: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Cashu token to decode (supports v1 and v3 formats)',
|
description: 'Cashu token to decode (supports v1 and v3 formats)',
|
||||||
example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0'
|
example: 'cashuB...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -95,6 +95,11 @@ const options = {
|
|||||||
enum: ['cashuA', 'cashuB'],
|
enum: ['cashuA', 'cashuB'],
|
||||||
description: 'Token format version',
|
description: 'Token format version',
|
||||||
example: 'cashuA'
|
example: 'cashuA'
|
||||||
|
},
|
||||||
|
spent: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether the token has already been spent (true = spent, false = still valid)',
|
||||||
|
example: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -115,13 +120,13 @@ const options = {
|
|||||||
token: {
|
token: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Cashu token to redeem',
|
description: 'Cashu token to redeem',
|
||||||
example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0'
|
example: 'cashuB...'
|
||||||
},
|
},
|
||||||
lightningAddress: {
|
lightningAddress: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'email',
|
format: 'email',
|
||||||
description: 'Lightning address to send payment to (optional - uses default if not provided)',
|
description: 'Lightning address to send payment to (optional - uses default if not provided)',
|
||||||
example: 'user@ln.tips'
|
example: 'user@blink.sv'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -324,6 +329,10 @@ const options = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
tags: [
|
tags: [
|
||||||
|
{
|
||||||
|
name: 'General',
|
||||||
|
description: 'General API information and utilities'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Token Operations',
|
name: 'Token Operations',
|
||||||
description: 'Operations for decoding and redeeming Cashu tokens'
|
description: 'Operations for decoding and redeeming Cashu tokens'
|
||||||
|
|||||||
Reference in New Issue
Block a user