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:**
|
||||
```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,
|
||||
"numProofs": 3,
|
||||
"denominations": [1000, 10000, 10000],
|
||||
"format": "cashuA"
|
||||
"format": "cashuA",
|
||||
"spent": false
|
||||
},
|
||||
"mint_url": "https://mint.azzamo.net"
|
||||
}
|
||||
@@ -59,7 +60,7 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"token": "cashuAeyJhbGciOi...",
|
||||
"token": "cashuB...",
|
||||
"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):**
|
||||
```json
|
||||
{
|
||||
"token": "cashuAeyJhbGciOi..."
|
||||
"token": "cashuB..."
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
466
package-lock.json
generated
466
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@cashu/cashu-ts": "^1.1.0",
|
||||
"axios": "^1.7.7",
|
||||
"bolt11": "^1.4.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
@@ -431,6 +432,14 @@
|
||||
"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": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
@@ -444,6 +453,14 @@
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"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": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -545,6 +562,20 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"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": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
@@ -562,6 +593,11 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"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": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -582,6 +618,11 @@
|
||||
],
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -595,6 +636,40 @@
|
||||
"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": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
@@ -619,6 +694,21 @@
|
||||
"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": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@@ -642,6 +732,28 @@
|
||||
"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": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
@@ -675,6 +787,23 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -775,6 +904,18 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -871,6 +1012,18 @@
|
||||
"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": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -902,6 +1055,22 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -974,6 +1143,20 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
@@ -1573,6 +1770,17 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -1600,6 +1808,28 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -1612,6 +1842,16 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@@ -1743,6 +1983,17 @@
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -1776,6 +2027,25 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -1856,6 +2126,11 @@
|
||||
"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": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
@@ -1892,6 +2167,16 @@
|
||||
"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": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -1952,6 +2237,16 @@
|
||||
"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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -1986,6 +2281,21 @@
|
||||
"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": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
||||
@@ -2242,6 +2552,14 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -2327,6 +2645,19 @@
|
||||
"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": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -2350,6 +2681,15 @@
|
||||
"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": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -2376,6 +2716,20 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"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": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
@@ -2443,12 +2797,47 @@
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -2566,6 +2955,14 @@
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -2706,6 +3116,24 @@
|
||||
"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": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
@@ -2713,6 +3141,11 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -2732,6 +3165,11 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@@ -2763,6 +3201,14 @@
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@@ -2788,6 +3234,26 @@
|
||||
"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": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"dependencies": {
|
||||
"@cashu/cashu-ts": "^1.1.0",
|
||||
"axios": "^1.7.7",
|
||||
"bolt11": "^1.4.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
@@ -34,8 +35,8 @@
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.4",
|
||||
"eslint": "^9.9.1"
|
||||
"eslint": "^9.9.1",
|
||||
"nodemon": "^3.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
|
||||
120
server.js
120
server.js
@@ -79,6 +79,78 @@ function asyncHandler(fn) {
|
||||
|
||||
// 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
|
||||
* /api/decode:
|
||||
@@ -128,6 +200,44 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
|
||||
const decoded = await cashuService.parseToken(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({
|
||||
success: true,
|
||||
decoded: {
|
||||
@@ -135,7 +245,8 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
|
||||
totalAmount: decoded.totalAmount,
|
||||
numProofs: decoded.numProofs,
|
||||
denominations: decoded.denominations,
|
||||
format: decoded.format
|
||||
format: decoded.format,
|
||||
spent: spent
|
||||
},
|
||||
mint_url: mintUrl
|
||||
});
|
||||
@@ -157,13 +268,14 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
|
||||
*
|
||||
* The redemption process includes:
|
||||
* 1. Token validation and parsing
|
||||
* 2. Fee calculation (NUT-05: 2% of amount, minimum 1 satoshi)
|
||||
* 3. Invoice creation for net amount (token amount - fees)
|
||||
* 2. Getting exact melt quote from mint to determine precise fees
|
||||
* 3. Invoice creation for net amount (token amount - exact fees)
|
||||
* 4. Spendability checking at the mint
|
||||
* 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.
|
||||
* No sats are lost to fee estimation errors.
|
||||
* tags: [Token Operations]
|
||||
* requestBody:
|
||||
* required: true
|
||||
|
||||
@@ -184,6 +184,36 @@ class CashuService {
|
||||
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
|
||||
* @param {string} token - The encoded Cashu token
|
||||
@@ -199,20 +229,36 @@ class CashuService {
|
||||
const decoded = await this.decodeTokenStructure(token);
|
||||
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);
|
||||
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
|
||||
const expectedFee = this.calculateFee(parsed.totalAmount);
|
||||
// Step 2: Calculate total required (amount + fee_reserve)
|
||||
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
|
||||
const totalRequired = meltQuote.amount + meltQuote.fee_reserve;
|
||||
if (totalRequired > parsed.totalAmount) {
|
||||
throw new Error(`Insufficient funds. Required: ${totalRequired} sats (including ${meltQuote.fee_reserve} sats fee), Available: ${parsed.totalAmount} sats`);
|
||||
// Check if we have sufficient funds
|
||||
if (total > parsed.totalAmount) {
|
||||
throw new Error(`Insufficient funds. Required: ${total} sats (including ${meltQuote.fee_reserve} sats fee), Available: ${parsed.totalAmount} sats`);
|
||||
}
|
||||
|
||||
// Perform the melt operation using the quote and proofs
|
||||
const meltResponse = await wallet.meltTokens(meltQuote, proofs);
|
||||
// Step 3: Send tokens with includeFees: true to get the right 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
|
||||
console.log('Melt response:', JSON.stringify(meltResponse, null, 2));
|
||||
@@ -245,7 +291,6 @@ class CashuService {
|
||||
change: meltResponse.change || [],
|
||||
amount: meltQuote.amount,
|
||||
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
|
||||
quote: meltQuote.quote,
|
||||
rawMeltResponse: meltResponse // Include raw response for debugging
|
||||
@@ -330,43 +375,44 @@ class CashuService {
|
||||
totalAmount: parsed.totalAmount
|
||||
};
|
||||
} catch (error) {
|
||||
// Check if it's a known 422 error (already spent token) - log less verbosely
|
||||
const isExpected422Error = (error.status === 422 || error.response?.status === 422) &&
|
||||
error.constructor.name === 'HttpResponseError';
|
||||
|
||||
if (isExpected422Error) {
|
||||
console.log('Token spendability check: 422 status detected - token already spent');
|
||||
} else {
|
||||
// Enhanced error logging for unexpected errors
|
||||
console.error('Spendability check error details:', {
|
||||
errorType: error.constructor.name,
|
||||
errorMessage: error.message,
|
||||
errorCode: error.code,
|
||||
errorStatus: error.status,
|
||||
errorResponse: error.response,
|
||||
errorData: error.data,
|
||||
errorStack: error.stack,
|
||||
errorString: String(error)
|
||||
});
|
||||
}
|
||||
// Enhanced error logging for debugging
|
||||
console.error('Spendability check error details:', {
|
||||
errorType: error.constructor.name,
|
||||
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
|
||||
let errorMessage = 'Unknown error occurred';
|
||||
|
||||
// Handle cashu-ts HttpResponseError specifically
|
||||
if (error.constructor.name === 'HttpResponseError') {
|
||||
if (!isExpected422Error) {
|
||||
console.log('HttpResponseError detected, extracting details...');
|
||||
}
|
||||
|
||||
// Extract status code first
|
||||
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) {
|
||||
errorMessage = 'Token proofs are not spendable - they have already been used or are invalid';
|
||||
if (!isExpected422Error) {
|
||||
console.log('Detected 422 status - token already spent');
|
||||
// Try to get more details about the 422 error
|
||||
let responseBody = null;
|
||||
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 {
|
||||
// Try to extract useful information from the HTTP response error
|
||||
@@ -432,9 +478,7 @@ class CashuService {
|
||||
}
|
||||
|
||||
// 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
|
||||
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.');
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const axios = require('axios');
|
||||
const bolt11 = require('bolt11');
|
||||
|
||||
class LightningService {
|
||||
constructor() {
|
||||
@@ -220,11 +221,19 @@ class LightningService {
|
||||
*/
|
||||
async resolveInvoice(lightningAddress, amount, comment = 'Cashu token redemption') {
|
||||
try {
|
||||
console.log(`Resolving Lightning address: ${lightningAddress} for ${amount} sats`);
|
||||
|
||||
// Get LNURLp endpoint
|
||||
const lnurlpUrl = this.getLNURLpEndpoint(lightningAddress);
|
||||
console.log(`LNURLp endpoint: ${lnurlpUrl}`);
|
||||
|
||||
// Fetch LNURLp response
|
||||
const lnurlpResponse = await this.fetchLNURLpResponse(lnurlpUrl);
|
||||
console.log('LNURLp response:', {
|
||||
callback: lnurlpResponse.callback,
|
||||
minSendable: lnurlpResponse.minSendable,
|
||||
maxSendable: lnurlpResponse.maxSendable
|
||||
});
|
||||
|
||||
// Validate amount
|
||||
if (!this.validateAmount(amount, lnurlpResponse)) {
|
||||
@@ -235,8 +244,18 @@ class LightningService {
|
||||
|
||||
// Get invoice
|
||||
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);
|
||||
|
||||
console.log('Invoice created successfully:', {
|
||||
bolt11: invoiceResponse.bolt11.substring(0, 50) + '...',
|
||||
lightningAddress,
|
||||
amount,
|
||||
amountMsats,
|
||||
callback: lnurlpResponse.callback
|
||||
});
|
||||
|
||||
return {
|
||||
bolt11: invoiceResponse.bolt11,
|
||||
amount,
|
||||
@@ -247,6 +266,7 @@ class LightningService {
|
||||
lnurlpResponse
|
||||
};
|
||||
} catch (error) {
|
||||
console.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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@@ -167,23 +167,10 @@ class RedemptionService {
|
||||
this.updateRedemption(redeemId, { status: 'parsing_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, {
|
||||
amount: tokenData.totalAmount,
|
||||
mint: tokenData.mint,
|
||||
numProofs: tokenData.numProofs,
|
||||
expectedFee: expectedFee,
|
||||
netAmountAfterFee: netAmountAfterFee,
|
||||
format: tokenData.format
|
||||
});
|
||||
|
||||
@@ -203,26 +190,58 @@ class RedemptionService {
|
||||
// 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');
|
||||
}
|
||||
// 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.log('Continuing with redemption despite spendability check failure...');
|
||||
}
|
||||
|
||||
// Step 2: Resolve Lightning address to invoice
|
||||
// IMPORTANT: Create invoice for net amount (after subtracting expected fees)
|
||||
// Step 2: Get melt quote first to determine exact 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' });
|
||||
|
||||
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(
|
||||
lightningAddressToUse,
|
||||
netAmountAfterFee, // Use net amount instead of full token amount
|
||||
finalInvoiceAmount, // Use amount minus exact fee
|
||||
'Cashu redemption'
|
||||
);
|
||||
|
||||
this.updateRedemption(redeemId, {
|
||||
bolt11: invoiceData.bolt11.substring(0, 50) + '...',
|
||||
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' });
|
||||
const meltResult = await cashuService.meltToken(token, invoiceData.bolt11);
|
||||
|
||||
@@ -256,12 +275,12 @@ class RedemptionService {
|
||||
redeemId,
|
||||
paid: paymentSuccessful,
|
||||
amount: tokenData.totalAmount,
|
||||
invoiceAmount: netAmountAfterFee, // Amount actually sent in the invoice
|
||||
invoiceAmount: finalInvoiceAmount, // Amount actually sent in the invoice
|
||||
to: lightningAddressToUse,
|
||||
usingDefaultAddress: isUsingDefault,
|
||||
fee: meltResult.fee,
|
||||
fee: exactFee, // Use the exact fee from the melt quote
|
||||
actualFee: meltResult.actualFee,
|
||||
netAmount: meltResult.netAmount,
|
||||
netAmount: finalInvoiceAmount, // This is the net amount the user receives
|
||||
preimage: meltResult.preimage,
|
||||
change: meltResult.change,
|
||||
mint: tokenData.mint,
|
||||
|
||||
@@ -52,7 +52,7 @@ const options = {
|
||||
token: {
|
||||
type: 'string',
|
||||
description: 'Cashu token to decode (supports v1 and v3 formats)',
|
||||
example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0'
|
||||
example: 'cashuB...'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -95,6 +95,11 @@ const options = {
|
||||
enum: ['cashuA', 'cashuB'],
|
||||
description: 'Token format version',
|
||||
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: {
|
||||
type: 'string',
|
||||
description: 'Cashu token to redeem',
|
||||
example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0'
|
||||
example: 'cashuB...'
|
||||
},
|
||||
lightningAddress: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
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: [
|
||||
{
|
||||
name: 'General',
|
||||
description: 'General API information and utilities'
|
||||
},
|
||||
{
|
||||
name: 'Token Operations',
|
||||
description: 'Operations for decoding and redeeming Cashu tokens'
|
||||
|
||||
Reference in New Issue
Block a user