@orangecheck/sdk 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/attestation.d.mts +1 -1
- package/dist/attestation.d.ts +1 -1
- package/dist/canonical.d.mts +1 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/challenge.d.mts +1 -1
- package/dist/challenge.d.ts +1 -1
- package/dist/identity.d.mts +1 -1
- package/dist/identity.d.ts +1 -1
- package/dist/index.d.mts +105 -3
- package/dist/index.d.ts +105 -3
- package/dist/index.js +194 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +183 -6
- package/dist/index.mjs.map +1 -1
- package/dist/nostr.d.mts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/{types-D9Tb1roj.d.mts → types-BBRMCgMo.d.mts} +1 -0
- package/dist/{types-D9Tb1roj.d.ts → types-BBRMCgMo.d.ts} +1 -0
- package/dist/verify.d.mts +1 -1
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +20 -2
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +20 -2
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { bech32 } from '@scure/base';
|
|
2
2
|
import { Buffer } from 'buffer';
|
|
3
|
-
import { verifySignature as verifySignature$1 } from '@orangecheck/bip322';
|
|
4
|
-
import { schnorr } from '@noble/curves/secp256k1.js';
|
|
3
|
+
import { verifySignature as verifySignature$1, decodeAddress } from '@orangecheck/bip322';
|
|
5
4
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
5
|
+
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
6
|
+
import { schnorr } from '@noble/curves/secp256k1.js';
|
|
6
7
|
|
|
7
8
|
// src/canonical.ts
|
|
8
9
|
function toLowerHex(bytes) {
|
|
@@ -1024,7 +1025,7 @@ async function getAddressUtxos(address, network, opts) {
|
|
|
1024
1025
|
log4.error({ endpoints, error: lastError?.message, network }, "All Esplora endpoints failed");
|
|
1025
1026
|
throw new Error(`esplora_all_failed: ${lastError?.message || "unknown"}`);
|
|
1026
1027
|
}
|
|
1027
|
-
function computeMetrics(utxos, bondAmount) {
|
|
1028
|
+
function computeMetrics(utxos, bondAmount, coverSats) {
|
|
1028
1029
|
const confirmedUtxos = utxos.filter((u) => u.status.confirmed);
|
|
1029
1030
|
if (confirmedUtxos.length === 0) {
|
|
1030
1031
|
return { sats_bonded: 0, days_unspent: 0, confirmed_balance: 0 };
|
|
@@ -1057,6 +1058,23 @@ function computeMetrics(utxos, bondAmount) {
|
|
|
1057
1058
|
};
|
|
1058
1059
|
}
|
|
1059
1060
|
const sats_bonded = confirmed_balance;
|
|
1061
|
+
if (coverSats !== void 0 && coverSats > 0) {
|
|
1062
|
+
const sorted = [...confirmedUtxos].sort(
|
|
1063
|
+
(a, b) => (a.status.block_height ?? Infinity) - (b.status.block_height ?? Infinity)
|
|
1064
|
+
);
|
|
1065
|
+
let sum = 0;
|
|
1066
|
+
let youngestTime = 0;
|
|
1067
|
+
for (const utxo of sorted) {
|
|
1068
|
+
sum += utxo.value;
|
|
1069
|
+
youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1e3);
|
|
1070
|
+
if (sum >= coverSats) break;
|
|
1071
|
+
}
|
|
1072
|
+
const days_unspent2 = Math.max(
|
|
1073
|
+
0,
|
|
1074
|
+
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
1075
|
+
);
|
|
1076
|
+
return { sats_bonded, days_unspent: days_unspent2, confirmed_balance };
|
|
1077
|
+
}
|
|
1060
1078
|
const oldestTime = Math.min(
|
|
1061
1079
|
...confirmedUtxos.map((u) => u.status.block_time || Date.now() / 1e3)
|
|
1062
1080
|
);
|
|
@@ -1120,7 +1138,8 @@ async function verify(input, opts = {}) {
|
|
|
1120
1138
|
});
|
|
1121
1139
|
const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(
|
|
1122
1140
|
utxos,
|
|
1123
|
-
parsed.bond
|
|
1141
|
+
parsed.bond,
|
|
1142
|
+
opts.coverSats
|
|
1124
1143
|
);
|
|
1125
1144
|
if (parsed.bond !== void 0 && parsed.bond > 0) {
|
|
1126
1145
|
if (confirmed_balance < parsed.bond) {
|
|
@@ -1225,7 +1244,7 @@ async function check(params) {
|
|
|
1225
1244
|
sig: env.signature,
|
|
1226
1245
|
scheme: env.scheme
|
|
1227
1246
|
},
|
|
1228
|
-
verifyOptions
|
|
1247
|
+
{ ...verifyOptions, coverSats: minSats > 0 ? minSats : void 0 }
|
|
1229
1248
|
);
|
|
1230
1249
|
const sats = outcome.metrics?.sats_bonded ?? 0;
|
|
1231
1250
|
const days = outcome.metrics?.days_unspent ?? 0;
|
|
@@ -1429,6 +1448,164 @@ async function verifyChallenge(opts) {
|
|
|
1429
1448
|
...purpose !== void 0 ? { purpose } : {}
|
|
1430
1449
|
};
|
|
1431
1450
|
}
|
|
1451
|
+
var HEADER = "orangecheck-bond";
|
|
1452
|
+
var ACK = "I pledge this coin to back this subject. It stays in my wallet.";
|
|
1453
|
+
var FIELDS = ["address", "outpoint", "subject", "nonce", "issued_at", "expires_at"];
|
|
1454
|
+
var BOND_SATS_FLOORS = [1e3, 1e4, 1e5, 1e6, 1e7];
|
|
1455
|
+
var BOND_DAYS_FLOORS = [7, 30, 90, 365];
|
|
1456
|
+
var BOND_MIN_CONFIRMATIONS = 6;
|
|
1457
|
+
var OUTPOINT_RE = /^[0-9a-f]{64}:(0|[1-9][0-9]{0,9})$/;
|
|
1458
|
+
var NONCE_RE = /^[0-9a-f]{32}$/;
|
|
1459
|
+
var TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
1460
|
+
var SUBJECT_RE = /^[\x21-\x7e]{1,256}$/;
|
|
1461
|
+
function iso2(d) {
|
|
1462
|
+
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1463
|
+
}
|
|
1464
|
+
function randomNonce2() {
|
|
1465
|
+
const b = new Uint8Array(16);
|
|
1466
|
+
globalThis.crypto.getRandomValues(b);
|
|
1467
|
+
return bytesToHex(b);
|
|
1468
|
+
}
|
|
1469
|
+
function wellFormed(f) {
|
|
1470
|
+
return decodeAddress(f.address) !== null && OUTPOINT_RE.test(f.outpoint) && SUBJECT_RE.test(f.subject) && NONCE_RE.test(f.nonce) && TIME_RE.test(f.issued_at) && TIME_RE.test(f.expires_at) && Date.parse(f.expires_at) > Date.parse(f.issued_at);
|
|
1471
|
+
}
|
|
1472
|
+
function buildBondMessage(fields) {
|
|
1473
|
+
if (!wellFormed(fields)) throw new Error("bond fields are malformed");
|
|
1474
|
+
return [HEADER, ...FIELDS.map((k) => `${k}: ${fields[k]}`), `ack: ${ACK}`].join("\n") + "\n";
|
|
1475
|
+
}
|
|
1476
|
+
function issueBondMessage(opts) {
|
|
1477
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
1478
|
+
const expires = new Date(now.getTime() + (opts.ttlSeconds ?? 600) * 1e3);
|
|
1479
|
+
const nonce = randomNonce2();
|
|
1480
|
+
const message = buildBondMessage({
|
|
1481
|
+
address: opts.address,
|
|
1482
|
+
outpoint: opts.outpoint,
|
|
1483
|
+
subject: opts.subject,
|
|
1484
|
+
nonce,
|
|
1485
|
+
issued_at: iso2(now),
|
|
1486
|
+
expires_at: iso2(expires)
|
|
1487
|
+
});
|
|
1488
|
+
return { message, nonce, expiresAt: expires.getTime() };
|
|
1489
|
+
}
|
|
1490
|
+
function parseBondMessage(message) {
|
|
1491
|
+
if (!message.endsWith("\n") || message.includes("\r")) return null;
|
|
1492
|
+
const lines = message.slice(0, -1).split("\n");
|
|
1493
|
+
if (lines.length !== FIELDS.length + 2 || lines[0] !== HEADER) return null;
|
|
1494
|
+
if (lines[lines.length - 1] !== `ack: ${ACK}`) return null;
|
|
1495
|
+
const out = {};
|
|
1496
|
+
for (const [i, key] of FIELDS.entries()) {
|
|
1497
|
+
const line = lines[i + 1];
|
|
1498
|
+
const prefix = `${key}: `;
|
|
1499
|
+
if (!line.startsWith(prefix)) return null;
|
|
1500
|
+
out[key] = line.slice(prefix.length);
|
|
1501
|
+
}
|
|
1502
|
+
const fields = out;
|
|
1503
|
+
return wellFormed(fields) && buildBondMessage(fields) === message ? fields : null;
|
|
1504
|
+
}
|
|
1505
|
+
function bondId(message) {
|
|
1506
|
+
return bytesToHex(sha256(utf8ToBytes(message)));
|
|
1507
|
+
}
|
|
1508
|
+
function bondNullifier(audience, outpoint) {
|
|
1509
|
+
return bytesToHex(sha256(utf8ToBytes(`orangecheck-bond-nullifier
|
|
1510
|
+
${audience}
|
|
1511
|
+
${outpoint}`)));
|
|
1512
|
+
}
|
|
1513
|
+
function bondFloors(sats, ageBlocks) {
|
|
1514
|
+
const days = Math.floor(ageBlocks / 144);
|
|
1515
|
+
const floor = (ladder, v) => ladder.filter((f) => v >= f).reduce((a, b) => Math.max(a, b), 0);
|
|
1516
|
+
return { sats_floor: floor(BOND_SATS_FLOORS, sats), days_floor: floor(BOND_DAYS_FLOORS, days) };
|
|
1517
|
+
}
|
|
1518
|
+
function verifyBondSignature(opts) {
|
|
1519
|
+
const fields = parseBondMessage(opts.message);
|
|
1520
|
+
if (!fields) return { ok: false, reason: "bond_malformed" };
|
|
1521
|
+
if (opts.subject !== void 0 && fields.subject !== opts.subject) {
|
|
1522
|
+
return { ok: false, reason: "bond_subject_mismatch" };
|
|
1523
|
+
}
|
|
1524
|
+
const now = (opts.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
1525
|
+
const skew = (opts.skewSeconds ?? 60) * 1e3;
|
|
1526
|
+
if (Date.parse(fields.issued_at) - skew > now) return { ok: false, reason: "bond_not_yet_valid" };
|
|
1527
|
+
if (Date.parse(fields.expires_at) + skew < now) return { ok: false, reason: "bond_expired" };
|
|
1528
|
+
if (!verifySignature$1(fields.address, opts.message, opts.signature)) {
|
|
1529
|
+
return { ok: false, reason: "sig_invalid" };
|
|
1530
|
+
}
|
|
1531
|
+
return { ok: true, fields, bond_id: bondId(opts.message) };
|
|
1532
|
+
}
|
|
1533
|
+
async function checkBondCoin(opts) {
|
|
1534
|
+
const decoded = decodeAddress(opts.address);
|
|
1535
|
+
if (!decoded || !OUTPOINT_RE.test(opts.outpoint)) return { ok: false, reason: "bond_malformed" };
|
|
1536
|
+
let coin;
|
|
1537
|
+
let tip;
|
|
1538
|
+
try {
|
|
1539
|
+
[coin, tip] = await Promise.all([opts.source.coin(opts.outpoint), opts.source.tip()]);
|
|
1540
|
+
} catch {
|
|
1541
|
+
return { ok: false, reason: "chain_unavailable" };
|
|
1542
|
+
}
|
|
1543
|
+
if (!coin) return { ok: false, reason: "coin_not_found" };
|
|
1544
|
+
if (coin.scriptPubKey !== bytesToHex(decoded.scriptPubKey)) {
|
|
1545
|
+
return { ok: false, reason: "coin_address_mismatch" };
|
|
1546
|
+
}
|
|
1547
|
+
if (coin.confirmedHeight === null) return { ok: false, reason: "coin_unconfirmed" };
|
|
1548
|
+
if (coin.spent) return { ok: false, reason: "coin_spent" };
|
|
1549
|
+
const confirmations = tip.height - coin.confirmedHeight + 1;
|
|
1550
|
+
if (confirmations < (opts.minConfirmations ?? BOND_MIN_CONFIRMATIONS)) {
|
|
1551
|
+
return { ok: false, reason: "coin_too_recent" };
|
|
1552
|
+
}
|
|
1553
|
+
return { ok: true, sats: coin.sats, confirmed_height: coin.confirmedHeight, tip };
|
|
1554
|
+
}
|
|
1555
|
+
async function verifyBond(opts) {
|
|
1556
|
+
const sig = verifyBondSignature(opts);
|
|
1557
|
+
if (!sig.ok) return sig;
|
|
1558
|
+
const coin = await checkBondCoin({
|
|
1559
|
+
address: sig.fields.address,
|
|
1560
|
+
outpoint: sig.fields.outpoint,
|
|
1561
|
+
source: opts.source,
|
|
1562
|
+
minConfirmations: opts.minConfirmations
|
|
1563
|
+
});
|
|
1564
|
+
if (!coin.ok) return coin;
|
|
1565
|
+
return {
|
|
1566
|
+
ok: true,
|
|
1567
|
+
bond: {
|
|
1568
|
+
bond_id: sig.bond_id,
|
|
1569
|
+
subject: sig.fields.subject,
|
|
1570
|
+
address: sig.fields.address,
|
|
1571
|
+
outpoint: sig.fields.outpoint,
|
|
1572
|
+
sats: coin.sats,
|
|
1573
|
+
confirmed_height: coin.confirmed_height,
|
|
1574
|
+
tip: coin.tip
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
function esploraSource(base = "https://mempool.space/api", opts = {}) {
|
|
1579
|
+
const get = async (path) => {
|
|
1580
|
+
const r = await (opts.fetch ?? fetch)(`${base}${path}`, {
|
|
1581
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5e3)
|
|
1582
|
+
});
|
|
1583
|
+
if (!r.ok && r.status !== 404) throw new Error(`esplora ${path}: HTTP ${r.status}`);
|
|
1584
|
+
return r;
|
|
1585
|
+
};
|
|
1586
|
+
return {
|
|
1587
|
+
async tip() {
|
|
1588
|
+
const [h, hash] = await Promise.all([get("/blocks/tip/height"), get("/blocks/tip/hash")]);
|
|
1589
|
+
return { height: Number(await h.text()), hash: (await hash.text()).trim() };
|
|
1590
|
+
},
|
|
1591
|
+
async coin(outpoint) {
|
|
1592
|
+
const [txid, voutStr] = outpoint.split(":");
|
|
1593
|
+
const vout = Number(voutStr);
|
|
1594
|
+
const txr = await get(`/tx/${txid}`);
|
|
1595
|
+
if (txr.status === 404) return null;
|
|
1596
|
+
const tx = await txr.json();
|
|
1597
|
+
const out = tx.vout[vout];
|
|
1598
|
+
if (!out) return null;
|
|
1599
|
+
const spend = await (await get(`/tx/${txid}/outspend/${vout}`)).json();
|
|
1600
|
+
return {
|
|
1601
|
+
sats: out.value,
|
|
1602
|
+
scriptPubKey: out.scriptpubkey.toLowerCase(),
|
|
1603
|
+
confirmedHeight: tx.status.confirmed ? tx.status.block_height ?? null : null,
|
|
1604
|
+
spent: spend.spent
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1432
1609
|
|
|
1433
1610
|
// src/utils/rate-limiter.ts
|
|
1434
1611
|
var RateLimitError = class extends Error {
|
|
@@ -2218,6 +2395,6 @@ var STATUS_META = {
|
|
|
2218
2395
|
}
|
|
2219
2396
|
};
|
|
2220
2397
|
|
|
2221
|
-
export { DEFAULT_RELAYS, FANOUT_DEADLINE_MS, STATUS_META, buildCanonicalMessage, check, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueChallenge, nostrIdentifierForms, nostrPubkeyToHex, parseAttestationFromEvent, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify, verifyAllIdentities, verifyAttestationById, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
|
|
2398
|
+
export { BOND_DAYS_FLOORS, BOND_MIN_CONFIRMATIONS, BOND_SATS_FLOORS, DEFAULT_RELAYS, FANOUT_DEADLINE_MS, STATUS_META, bondFloors, bondId, bondNullifier, buildBondMessage, buildCanonicalMessage, check, checkBondCoin, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, esploraSource, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueBondMessage, issueChallenge, nostrIdentifierForms, nostrPubkeyToHex, parseAttestationFromEvent, parseBondMessage, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify, verifyAllIdentities, verifyAttestationById, verifyBond, verifyBondSignature, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
|
|
2222
2399
|
//# sourceMappingURL=index.mjs.map
|
|
2223
2400
|
//# sourceMappingURL=index.mjs.map
|