mirror of
https://github.com/dergigi/boris.git
synced 2026-01-06 00:14:48 +01:00
- Add project structure with TypeScript, React, and Vite - Implement nostr authentication using browser extension (NIP-07) - Add NIP-51 compliant bookmark fetching and display - Create minimal UI with login and bookmark components - Integrate applesauce-core and applesauce-react libraries - Add responsive styling with dark/light mode support - Include comprehensive README with setup instructions This is a minimal MVP for a nostr bookmark client that allows users to view their bookmarks according to NIP-51 specification.
345 lines
12 KiB
JavaScript
345 lines
12 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.decodePaymentRequest = exports.sumProofs = exports.sanitizeUrl = exports.joinUrls = exports.checkResponse = exports.isObj = exports.sortProofsById = exports.mergeUInt8Arrays = exports.deriveKeysetId = exports.handleTokens = exports.getDecodedToken = exports.getEncodedTokenV4 = exports.getEncodedToken = exports.getEncodedTokenV3 = exports.bigIntStringify = exports.hasNonHexId = exports.hexToNumber = exports.bytesToNumber = exports.hasCorrespondingKey = exports.getKeysetAmounts = exports.getKeepAmounts = exports.splitAmount = void 0;
|
|
var base64_js_1 = require("./base64.js");
|
|
var Constants_js_1 = require("./utils/Constants.js");
|
|
var utils_1 = require("@noble/curves/abstract/utils");
|
|
var sha256_1 = require("@noble/hashes/sha256");
|
|
var cbor_js_1 = require("./cbor.js");
|
|
var PaymentRequest_js_1 = require("./model/PaymentRequest.js");
|
|
/**
|
|
* Splits the amount into denominations of the provided @param keyset
|
|
* @param value amount to split
|
|
* @param keyset keys to look up split amounts
|
|
* @param split? optional custom split amounts
|
|
* @param order? optional order for split amounts (default: "asc")
|
|
* @returns Array of split amounts
|
|
* @throws Error if @param split amount is greater than @param value amount
|
|
*/
|
|
function splitAmount(value, keyset, split, order) {
|
|
if (split) {
|
|
if (split.reduce(function (a, b) { return a + b; }, 0) > value) {
|
|
throw new Error("Split is greater than total amount: ".concat(split.reduce(function (a, b) { return a + b; }, 0), " > ").concat(value));
|
|
}
|
|
split.forEach(function (amt) {
|
|
if (!hasCorrespondingKey(amt, keyset)) {
|
|
throw new Error('Provided amount preferences do not match the amounts of the mint keyset.');
|
|
}
|
|
});
|
|
value =
|
|
value -
|
|
split.reduce(function (curr, acc) {
|
|
return curr + acc;
|
|
}, 0);
|
|
}
|
|
else {
|
|
split = [];
|
|
}
|
|
var sortedKeyAmounts = getKeysetAmounts(keyset);
|
|
sortedKeyAmounts.forEach(function (amt) {
|
|
var q = Math.floor(value / amt);
|
|
for (var i = 0; i < q; ++i)
|
|
split === null || split === void 0 ? void 0 : split.push(amt);
|
|
value %= amt;
|
|
});
|
|
return split.sort(function (a, b) { return (order === 'desc' ? b - a : a - b); });
|
|
}
|
|
exports.splitAmount = splitAmount;
|
|
/**
|
|
* Creates a list of amounts to keep based on the proofs we have and the proofs we want to reach.
|
|
* @param proofsWeHave complete set of proofs stored (from current mint)
|
|
* @param amountToKeep amount to keep
|
|
* @param keys keys of current keyset
|
|
* @param targetCount the target number of proofs to reach
|
|
* @returns an array of amounts to keep
|
|
*/
|
|
function getKeepAmounts(proofsWeHave, amountToKeep, keys, targetCount) {
|
|
// determines amounts we need to reach the targetCount for each amount based on the amounts of the proofs we have
|
|
// it tries to select amounts so that the proofs we have and the proofs we want reach the targetCount
|
|
var amountsWeWant = [];
|
|
var amountsWeHave = proofsWeHave.map(function (p) { return p.amount; });
|
|
var sortedKeyAmounts = getKeysetAmounts(keys, 'asc');
|
|
sortedKeyAmounts.forEach(function (amt) {
|
|
var countWeHave = amountsWeHave.filter(function (a) { return a === amt; }).length;
|
|
var countWeWant = Math.max(targetCount - countWeHave, 0);
|
|
for (var i = 0; i < countWeWant; ++i) {
|
|
if (amountsWeWant.reduce(function (a, b) { return a + b; }, 0) + amt > amountToKeep) {
|
|
break;
|
|
}
|
|
amountsWeWant.push(amt);
|
|
}
|
|
});
|
|
// use splitAmount to fill the rest between the sum of amountsWeHave and amountToKeep
|
|
var amountDiff = amountToKeep - amountsWeWant.reduce(function (a, b) { return a + b; }, 0);
|
|
if (amountDiff) {
|
|
var remainingAmounts = splitAmount(amountDiff, keys);
|
|
remainingAmounts.forEach(function (amt) {
|
|
amountsWeWant.push(amt);
|
|
});
|
|
}
|
|
var sortedAmountsWeWant = amountsWeWant.sort(function (a, b) { return a - b; });
|
|
return sortedAmountsWeWant;
|
|
}
|
|
exports.getKeepAmounts = getKeepAmounts;
|
|
/**
|
|
* returns the amounts in the keyset sorted by the order specified
|
|
* @param keyset to search in
|
|
* @param order order to sort the amounts in
|
|
* @returns the amounts in the keyset sorted by the order specified
|
|
*/
|
|
function getKeysetAmounts(keyset, order) {
|
|
if (order === void 0) { order = 'desc'; }
|
|
if (order == 'desc') {
|
|
return Object.keys(keyset)
|
|
.map(function (k) { return parseInt(k); })
|
|
.sort(function (a, b) { return b - a; });
|
|
}
|
|
return Object.keys(keyset)
|
|
.map(function (k) { return parseInt(k); })
|
|
.sort(function (a, b) { return a - b; });
|
|
}
|
|
exports.getKeysetAmounts = getKeysetAmounts;
|
|
/**
|
|
* Checks if the provided amount is in the keyset.
|
|
* @param amount amount to check
|
|
* @param keyset to search in
|
|
* @returns true if the amount is in the keyset, false otherwise
|
|
*/
|
|
function hasCorrespondingKey(amount, keyset) {
|
|
return amount in keyset;
|
|
}
|
|
exports.hasCorrespondingKey = hasCorrespondingKey;
|
|
/**
|
|
* Converts a bytes array to a number.
|
|
* @param bytes to convert to number
|
|
* @returns number
|
|
*/
|
|
function bytesToNumber(bytes) {
|
|
return hexToNumber((0, utils_1.bytesToHex)(bytes));
|
|
}
|
|
exports.bytesToNumber = bytesToNumber;
|
|
/**
|
|
* Converts a hex string to a number.
|
|
* @param hex to convert to number
|
|
* @returns number
|
|
*/
|
|
function hexToNumber(hex) {
|
|
return BigInt("0x".concat(hex));
|
|
}
|
|
exports.hexToNumber = hexToNumber;
|
|
function isValidHex(str) {
|
|
return /^[a-f0-9]*$/i.test(str);
|
|
}
|
|
/**
|
|
* Checks wether a proof or a list of proofs contains a non-hex id
|
|
* @param p Proof or list of proofs
|
|
* @returns boolean
|
|
*/
|
|
function hasNonHexId(p) {
|
|
if (Array.isArray(p)) {
|
|
return p.some(function (proof) { return !isValidHex(proof.id); });
|
|
}
|
|
return isValidHex(p.id);
|
|
}
|
|
exports.hasNonHexId = hasNonHexId;
|
|
//used for json serialization
|
|
function bigIntStringify(_key, value) {
|
|
return typeof value === 'bigint' ? value.toString() : value;
|
|
}
|
|
exports.bigIntStringify = bigIntStringify;
|
|
/**
|
|
* Helper function to encode a v3 cashu token
|
|
* @param token to encode
|
|
* @returns encoded token
|
|
*/
|
|
function getEncodedTokenV3(token) {
|
|
var v3TokenObj = { token: [{ mint: token.mint, proofs: token.proofs }] };
|
|
if (token.unit) {
|
|
v3TokenObj.unit = token.unit;
|
|
}
|
|
if (token.memo) {
|
|
v3TokenObj.memo = token.memo;
|
|
}
|
|
return Constants_js_1.TOKEN_PREFIX + Constants_js_1.TOKEN_VERSION + (0, base64_js_1.encodeJsonToBase64)(v3TokenObj);
|
|
}
|
|
exports.getEncodedTokenV3 = getEncodedTokenV3;
|
|
/**
|
|
* Helper function to encode a cashu token (defaults to v4 if keyset id allows it)
|
|
* @param token
|
|
* @param [opts]
|
|
*/
|
|
function getEncodedToken(token, opts) {
|
|
var nonHex = hasNonHexId(token.proofs);
|
|
if (nonHex || (opts === null || opts === void 0 ? void 0 : opts.version) === 3) {
|
|
if ((opts === null || opts === void 0 ? void 0 : opts.version) === 4) {
|
|
throw new Error('can not encode to v4 token if proofs contain non-hex keyset id');
|
|
}
|
|
return getEncodedTokenV3(token);
|
|
}
|
|
return getEncodedTokenV4(token);
|
|
}
|
|
exports.getEncodedToken = getEncodedToken;
|
|
function getEncodedTokenV4(token) {
|
|
var nonHex = hasNonHexId(token.proofs);
|
|
if (nonHex) {
|
|
throw new Error('can not encode to v4 token if proofs contain non-hex keyset id');
|
|
}
|
|
var idMap = {};
|
|
var mint = token.mint;
|
|
for (var i = 0; i < token.proofs.length; i++) {
|
|
var proof = token.proofs[i];
|
|
if (idMap[proof.id]) {
|
|
idMap[proof.id].push(proof);
|
|
}
|
|
else {
|
|
idMap[proof.id] = [proof];
|
|
}
|
|
}
|
|
var tokenTemplate = {
|
|
m: mint,
|
|
u: token.unit || 'sat',
|
|
t: Object.keys(idMap).map(function (id) { return ({
|
|
i: (0, utils_1.hexToBytes)(id),
|
|
p: idMap[id].map(function (p) { return ({ a: p.amount, s: p.secret, c: (0, utils_1.hexToBytes)(p.C) }); })
|
|
}); })
|
|
};
|
|
if (token.memo) {
|
|
tokenTemplate.d = token.memo;
|
|
}
|
|
var encodedData = (0, cbor_js_1.encodeCBOR)(tokenTemplate);
|
|
var prefix = 'cashu';
|
|
var version = 'B';
|
|
var base64Data = (0, base64_js_1.encodeUint8toBase64Url)(encodedData);
|
|
return prefix + version + base64Data;
|
|
}
|
|
exports.getEncodedTokenV4 = getEncodedTokenV4;
|
|
/**
|
|
* Helper function to decode cashu tokens into object
|
|
* @param token an encoded cashu token (cashuAey...)
|
|
* @returns cashu token object
|
|
*/
|
|
function getDecodedToken(token) {
|
|
// remove prefixes
|
|
var uriPrefixes = ['web+cashu://', 'cashu://', 'cashu:', 'cashu'];
|
|
uriPrefixes.forEach(function (prefix) {
|
|
if (!token.startsWith(prefix)) {
|
|
return;
|
|
}
|
|
token = token.slice(prefix.length);
|
|
});
|
|
return handleTokens(token);
|
|
}
|
|
exports.getDecodedToken = getDecodedToken;
|
|
/**
|
|
* Helper function to decode different versions of cashu tokens into an object
|
|
* @param token an encoded cashu token (cashuAey...)
|
|
* @returns cashu Token object
|
|
*/
|
|
function handleTokens(token) {
|
|
var version = token.slice(0, 1);
|
|
var encodedToken = token.slice(1);
|
|
if (version === 'A') {
|
|
var parsedV3Token = (0, base64_js_1.encodeBase64ToJson)(encodedToken);
|
|
if (parsedV3Token.token.length > 1) {
|
|
throw new Error('Multi entry token are not supported');
|
|
}
|
|
var entry = parsedV3Token.token[0];
|
|
var tokenObj = {
|
|
mint: entry.mint,
|
|
proofs: entry.proofs,
|
|
unit: parsedV3Token.unit || 'sat'
|
|
};
|
|
if (parsedV3Token.memo) {
|
|
tokenObj.memo = parsedV3Token.memo;
|
|
}
|
|
return tokenObj;
|
|
}
|
|
else if (version === 'B') {
|
|
var uInt8Token = (0, base64_js_1.encodeBase64toUint8)(encodedToken);
|
|
var tokenData = (0, cbor_js_1.decodeCBOR)(uInt8Token);
|
|
var proofs_1 = [];
|
|
tokenData.t.forEach(function (t) {
|
|
return t.p.forEach(function (p) {
|
|
proofs_1.push({
|
|
secret: p.s,
|
|
C: (0, utils_1.bytesToHex)(p.c),
|
|
amount: p.a,
|
|
id: (0, utils_1.bytesToHex)(t.i)
|
|
});
|
|
});
|
|
});
|
|
var decodedToken = { mint: tokenData.m, proofs: proofs_1, unit: tokenData.u || 'sat' };
|
|
if (tokenData.d) {
|
|
decodedToken.memo = tokenData.d;
|
|
}
|
|
return decodedToken;
|
|
}
|
|
throw new Error('Token version is not supported');
|
|
}
|
|
exports.handleTokens = handleTokens;
|
|
/**
|
|
* Returns the keyset id of a set of keys
|
|
* @param keys keys object to derive keyset id from
|
|
* @returns
|
|
*/
|
|
function deriveKeysetId(keys) {
|
|
var pubkeysConcat = Object.entries(keys)
|
|
.sort(function (a, b) { return +a[0] - +b[0]; })
|
|
.map(function (_a) {
|
|
var pubKey = _a[1];
|
|
return (0, utils_1.hexToBytes)(pubKey);
|
|
})
|
|
.reduce(function (prev, curr) { return mergeUInt8Arrays(prev, curr); }, new Uint8Array());
|
|
var hash = (0, sha256_1.sha256)(pubkeysConcat);
|
|
var hashHex = Buffer.from(hash).toString('hex').slice(0, 14);
|
|
return '00' + hashHex;
|
|
}
|
|
exports.deriveKeysetId = deriveKeysetId;
|
|
function mergeUInt8Arrays(a1, a2) {
|
|
// sum of individual array lengths
|
|
var mergedArray = new Uint8Array(a1.length + a2.length);
|
|
mergedArray.set(a1);
|
|
mergedArray.set(a2, a1.length);
|
|
return mergedArray;
|
|
}
|
|
exports.mergeUInt8Arrays = mergeUInt8Arrays;
|
|
function sortProofsById(proofs) {
|
|
return proofs.sort(function (a, b) { return a.id.localeCompare(b.id); });
|
|
}
|
|
exports.sortProofsById = sortProofsById;
|
|
function isObj(v) {
|
|
return typeof v === 'object';
|
|
}
|
|
exports.isObj = isObj;
|
|
function checkResponse(data) {
|
|
if (!isObj(data))
|
|
return;
|
|
if ('error' in data && data.error) {
|
|
throw new Error(data.error);
|
|
}
|
|
if ('detail' in data && data.detail) {
|
|
throw new Error(data.detail);
|
|
}
|
|
}
|
|
exports.checkResponse = checkResponse;
|
|
function joinUrls() {
|
|
var parts = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
parts[_i] = arguments[_i];
|
|
}
|
|
return parts.map(function (part) { return part.replace(/(^\/+|\/+$)/g, ''); }).join('/');
|
|
}
|
|
exports.joinUrls = joinUrls;
|
|
function sanitizeUrl(url) {
|
|
return url.replace(/\/$/, '');
|
|
}
|
|
exports.sanitizeUrl = sanitizeUrl;
|
|
function sumProofs(proofs) {
|
|
return proofs.reduce(function (acc, proof) { return acc + proof.amount; }, 0);
|
|
}
|
|
exports.sumProofs = sumProofs;
|
|
function decodePaymentRequest(paymentRequest) {
|
|
return PaymentRequest_js_1.PaymentRequest.fromEncodedRequest(paymentRequest);
|
|
}
|
|
exports.decodePaymentRequest = decodePaymentRequest;
|