@orangecheck/sdk 1.11.0 → 1.13.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 +41 -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 +108 -3
- package/dist/index.d.ts +108 -3
- package/dist/index.js +213 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +201 -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/README.md
CHANGED
|
@@ -145,6 +145,40 @@ Now the gate doesn't have to trust the client — the address it sees has been c
|
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|
|
148
|
+
## Bonds: one coin backing one account
|
|
149
|
+
|
|
150
|
+
An attestation publishes an address. A bond keeps it private. The holder signs
|
|
151
|
+
once to back one subject with one unspent coin, and whoever relies on the bond
|
|
152
|
+
sees only floors (at least N sats, at least D days) and a nullifier that is the
|
|
153
|
+
same for that coin at that site and unrelated anywhere else. The registry keys
|
|
154
|
+
the nullifier with its own secret, so nobody can find the coin by hashing
|
|
155
|
+
outpoints. Not to be confused with an attestation's `bond:` line (an amount at
|
|
156
|
+
a public address) or `@orangecheck/pledge-core`'s `verifyBond`. Spec:
|
|
157
|
+
[`BOND.md`](https://github.com/orangecheck/oc-attest-protocol/blob/main/BOND.md).
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { bondFloors, bondNullifier, esploraSource, issueBondMessage, verifyBond } from '@orangecheck/sdk';
|
|
161
|
+
|
|
162
|
+
// Registry: hand the holder a message naming their coin and your subject.
|
|
163
|
+
const { message } = issueBondMessage({ address, outpoint: `${txid}:${vout}`, subject: didOc });
|
|
164
|
+
|
|
165
|
+
// The holder signs it with BIP-322 in their wallet, then you verify the
|
|
166
|
+
// signature and the coin: unspent, confirmed, 6+ deep, at that address.
|
|
167
|
+
const r = await verifyBond({ message, signature, subject: didOc, source: esploraSource() });
|
|
168
|
+
if (r.ok) {
|
|
169
|
+
const age = r.bond.tip.height - r.bond.confirmed_height + 1;
|
|
170
|
+
// NULLIFIER_KEY: your registry's secret, 32+ bytes, kept for good.
|
|
171
|
+
const nullifier = bondNullifier(NULLIFIER_KEY, audience, r.bond.outpoint);
|
|
172
|
+
const claim = { ...bondFloors(r.bond.sats, age), nullifier };
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`checkBondCoin` re-checks a stored bond's coin later; a spent coin ends the
|
|
177
|
+
bond. Ban a nullifier and you've banned the coin: the holder has to move it to
|
|
178
|
+
get past the ban, and moving it restarts its age.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
148
182
|
## Creating an attestation (issuer side)
|
|
149
183
|
|
|
150
184
|
If you're building a wallet, a Nostr client, or a settings page where users should be able to _create_ a proof, use:
|
|
@@ -248,6 +282,13 @@ createAttestation(options: CreateAttestationOptions): Promise<AttestationEnvelop
|
|
|
248
282
|
issueChallenge(options): Challenge
|
|
249
283
|
verifyChallenge(options): Promise<VerifyChallengeResult>
|
|
250
284
|
|
|
285
|
+
// Bonds (BOND.md).
|
|
286
|
+
issueBondMessage(options): { message, nonce, expiresAt }
|
|
287
|
+
verifyBond(options): Promise<BondResult>
|
|
288
|
+
checkBondCoin(options): Promise<...>
|
|
289
|
+
bondNullifier(key, audience, outpoint): string
|
|
290
|
+
bondFloors(sats, ageBlocks): { sats_floor, days_floor }
|
|
291
|
+
|
|
251
292
|
// Building blocks.
|
|
252
293
|
buildCanonicalMessage(...)
|
|
253
294
|
generateAttestationId(msg: string): Promise<string>
|
package/dist/attestation.d.mts
CHANGED
package/dist/attestation.d.ts
CHANGED
package/dist/canonical.d.mts
CHANGED
package/dist/canonical.d.ts
CHANGED
package/dist/challenge.d.mts
CHANGED
package/dist/challenge.d.ts
CHANGED
package/dist/identity.d.mts
CHANGED
package/dist/identity.d.ts
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-
|
|
2
|
-
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-
|
|
1
|
+
import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-BBRMCgMo.mjs';
|
|
2
|
+
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-BBRMCgMo.mjs';
|
|
3
3
|
export { verify } from './verify.mjs';
|
|
4
4
|
export { createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById } from './attestation.mjs';
|
|
5
5
|
export { Challenge, IssueChallengeOptions, VerifyChallengeOptions, VerifyChallengeReason, VerifyChallengeResult, issueChallenge, verifyChallenge } from './challenge.mjs';
|
|
@@ -36,6 +36,111 @@ declare function check(params: CheckParams): Promise<CheckResult>;
|
|
|
36
36
|
declare function nostrPubkeyToHex(value: string): string | null;
|
|
37
37
|
declare function nostrIdentifierForms(identifier: string): string[];
|
|
38
38
|
|
|
39
|
+
declare const BOND_SATS_FLOORS: readonly [1000, 10000, 100000, 1000000, 10000000];
|
|
40
|
+
declare const BOND_DAYS_FLOORS: readonly [7, 30, 90, 365];
|
|
41
|
+
declare const BOND_MIN_CONFIRMATIONS = 6;
|
|
42
|
+
declare const BOND_MAX_WINDOW_SECONDS = 3600;
|
|
43
|
+
interface BondFields {
|
|
44
|
+
address: string;
|
|
45
|
+
outpoint: string;
|
|
46
|
+
subject: string;
|
|
47
|
+
nonce: string;
|
|
48
|
+
issued_at: string;
|
|
49
|
+
expires_at: string;
|
|
50
|
+
}
|
|
51
|
+
type BondReason = 'bond_malformed' | 'bond_subject_mismatch' | 'bond_expired' | 'bond_not_yet_valid' | 'bond_window_too_long' | 'sig_invalid' | 'coin_not_found' | 'coin_address_mismatch' | 'coin_unconfirmed' | 'coin_too_recent' | 'coin_spent' | 'chain_unavailable';
|
|
52
|
+
interface BondCoin {
|
|
53
|
+
sats: number;
|
|
54
|
+
scriptPubKey: string;
|
|
55
|
+
confirmedHeight: number | null;
|
|
56
|
+
spent: boolean;
|
|
57
|
+
}
|
|
58
|
+
interface ChainTip {
|
|
59
|
+
height: number;
|
|
60
|
+
hash: string;
|
|
61
|
+
}
|
|
62
|
+
interface BondChainSource {
|
|
63
|
+
tip(): Promise<ChainTip>;
|
|
64
|
+
coin(outpoint: string): Promise<BondCoin | null>;
|
|
65
|
+
}
|
|
66
|
+
interface VerifiedBond {
|
|
67
|
+
bond_id: string;
|
|
68
|
+
subject: string;
|
|
69
|
+
address: string;
|
|
70
|
+
outpoint: string;
|
|
71
|
+
sats: number;
|
|
72
|
+
confirmed_height: number;
|
|
73
|
+
tip: ChainTip;
|
|
74
|
+
}
|
|
75
|
+
type BondResult = {
|
|
76
|
+
ok: true;
|
|
77
|
+
bond: VerifiedBond;
|
|
78
|
+
} | {
|
|
79
|
+
ok: false;
|
|
80
|
+
reason: BondReason;
|
|
81
|
+
};
|
|
82
|
+
declare function buildBondMessage(fields: BondFields): string;
|
|
83
|
+
declare function issueBondMessage(opts: {
|
|
84
|
+
address: string;
|
|
85
|
+
outpoint: string;
|
|
86
|
+
subject: string;
|
|
87
|
+
ttlSeconds?: number;
|
|
88
|
+
now?: Date;
|
|
89
|
+
}): {
|
|
90
|
+
message: string;
|
|
91
|
+
nonce: string;
|
|
92
|
+
expiresAt: number;
|
|
93
|
+
};
|
|
94
|
+
declare function parseBondMessage(message: string): BondFields | null;
|
|
95
|
+
declare function bondId(message: string): string;
|
|
96
|
+
declare function bondNullifier(key: Uint8Array, audience: string, outpoint: string): string;
|
|
97
|
+
declare function bondFloors(sats: number, ageBlocks: number): {
|
|
98
|
+
sats_floor: number;
|
|
99
|
+
days_floor: number;
|
|
100
|
+
};
|
|
101
|
+
declare function verifyBondSignature(opts: {
|
|
102
|
+
message: string;
|
|
103
|
+
signature: string;
|
|
104
|
+
subject?: string;
|
|
105
|
+
now?: Date;
|
|
106
|
+
skewSeconds?: number;
|
|
107
|
+
maxWindowSeconds?: number;
|
|
108
|
+
}): {
|
|
109
|
+
ok: true;
|
|
110
|
+
fields: BondFields;
|
|
111
|
+
bond_id: string;
|
|
112
|
+
} | {
|
|
113
|
+
ok: false;
|
|
114
|
+
reason: BondReason;
|
|
115
|
+
};
|
|
116
|
+
declare function checkBondCoin(opts: {
|
|
117
|
+
address: string;
|
|
118
|
+
outpoint: string;
|
|
119
|
+
source: BondChainSource;
|
|
120
|
+
minConfirmations?: number;
|
|
121
|
+
}): Promise<{
|
|
122
|
+
ok: true;
|
|
123
|
+
sats: number;
|
|
124
|
+
confirmed_height: number;
|
|
125
|
+
tip: ChainTip;
|
|
126
|
+
} | {
|
|
127
|
+
ok: false;
|
|
128
|
+
reason: BondReason;
|
|
129
|
+
}>;
|
|
130
|
+
declare function verifyBond(opts: {
|
|
131
|
+
message: string;
|
|
132
|
+
signature: string;
|
|
133
|
+
source: BondChainSource;
|
|
134
|
+
subject?: string;
|
|
135
|
+
now?: Date;
|
|
136
|
+
maxWindowSeconds?: number;
|
|
137
|
+
minConfirmations?: number;
|
|
138
|
+
}): Promise<BondResult>;
|
|
139
|
+
declare function esploraSource(base?: string, opts?: {
|
|
140
|
+
timeoutMs?: number;
|
|
141
|
+
fetch?: typeof fetch;
|
|
142
|
+
}): BondChainSource;
|
|
143
|
+
|
|
39
144
|
declare function isNip07Available(): boolean;
|
|
40
145
|
declare function getNostrPublicKey(): Promise<string | null>;
|
|
41
146
|
declare function signNostrEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent | null>;
|
|
@@ -53,4 +158,4 @@ interface StatusMeta {
|
|
|
53
158
|
}
|
|
54
159
|
declare const STATUS_META: Record<StatusCode, StatusMeta>;
|
|
55
160
|
|
|
56
|
-
export { type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, VerifyOptions, check, getNip07Info, getNostrPublicKey, isNip07Available, nostrIdentifierForms, nostrPubkeyToHex, signNostrEvent };
|
|
161
|
+
export { BOND_DAYS_FLOORS, BOND_MAX_WINDOW_SECONDS, BOND_MIN_CONFIRMATIONS, BOND_SATS_FLOORS, type BondChainSource, type BondCoin, type BondFields, type BondReason, type BondResult, type ChainTip, type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, type VerifiedBond, VerifyOptions, bondFloors, bondId, bondNullifier, buildBondMessage, check, checkBondCoin, esploraSource, getNip07Info, getNostrPublicKey, isNip07Available, issueBondMessage, nostrIdentifierForms, nostrPubkeyToHex, parseBondMessage, signNostrEvent, verifyBond, verifyBondSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-
|
|
2
|
-
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-
|
|
1
|
+
import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-BBRMCgMo.js';
|
|
2
|
+
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-BBRMCgMo.js';
|
|
3
3
|
export { verify } from './verify.js';
|
|
4
4
|
export { createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById } from './attestation.js';
|
|
5
5
|
export { Challenge, IssueChallengeOptions, VerifyChallengeOptions, VerifyChallengeReason, VerifyChallengeResult, issueChallenge, verifyChallenge } from './challenge.js';
|
|
@@ -36,6 +36,111 @@ declare function check(params: CheckParams): Promise<CheckResult>;
|
|
|
36
36
|
declare function nostrPubkeyToHex(value: string): string | null;
|
|
37
37
|
declare function nostrIdentifierForms(identifier: string): string[];
|
|
38
38
|
|
|
39
|
+
declare const BOND_SATS_FLOORS: readonly [1000, 10000, 100000, 1000000, 10000000];
|
|
40
|
+
declare const BOND_DAYS_FLOORS: readonly [7, 30, 90, 365];
|
|
41
|
+
declare const BOND_MIN_CONFIRMATIONS = 6;
|
|
42
|
+
declare const BOND_MAX_WINDOW_SECONDS = 3600;
|
|
43
|
+
interface BondFields {
|
|
44
|
+
address: string;
|
|
45
|
+
outpoint: string;
|
|
46
|
+
subject: string;
|
|
47
|
+
nonce: string;
|
|
48
|
+
issued_at: string;
|
|
49
|
+
expires_at: string;
|
|
50
|
+
}
|
|
51
|
+
type BondReason = 'bond_malformed' | 'bond_subject_mismatch' | 'bond_expired' | 'bond_not_yet_valid' | 'bond_window_too_long' | 'sig_invalid' | 'coin_not_found' | 'coin_address_mismatch' | 'coin_unconfirmed' | 'coin_too_recent' | 'coin_spent' | 'chain_unavailable';
|
|
52
|
+
interface BondCoin {
|
|
53
|
+
sats: number;
|
|
54
|
+
scriptPubKey: string;
|
|
55
|
+
confirmedHeight: number | null;
|
|
56
|
+
spent: boolean;
|
|
57
|
+
}
|
|
58
|
+
interface ChainTip {
|
|
59
|
+
height: number;
|
|
60
|
+
hash: string;
|
|
61
|
+
}
|
|
62
|
+
interface BondChainSource {
|
|
63
|
+
tip(): Promise<ChainTip>;
|
|
64
|
+
coin(outpoint: string): Promise<BondCoin | null>;
|
|
65
|
+
}
|
|
66
|
+
interface VerifiedBond {
|
|
67
|
+
bond_id: string;
|
|
68
|
+
subject: string;
|
|
69
|
+
address: string;
|
|
70
|
+
outpoint: string;
|
|
71
|
+
sats: number;
|
|
72
|
+
confirmed_height: number;
|
|
73
|
+
tip: ChainTip;
|
|
74
|
+
}
|
|
75
|
+
type BondResult = {
|
|
76
|
+
ok: true;
|
|
77
|
+
bond: VerifiedBond;
|
|
78
|
+
} | {
|
|
79
|
+
ok: false;
|
|
80
|
+
reason: BondReason;
|
|
81
|
+
};
|
|
82
|
+
declare function buildBondMessage(fields: BondFields): string;
|
|
83
|
+
declare function issueBondMessage(opts: {
|
|
84
|
+
address: string;
|
|
85
|
+
outpoint: string;
|
|
86
|
+
subject: string;
|
|
87
|
+
ttlSeconds?: number;
|
|
88
|
+
now?: Date;
|
|
89
|
+
}): {
|
|
90
|
+
message: string;
|
|
91
|
+
nonce: string;
|
|
92
|
+
expiresAt: number;
|
|
93
|
+
};
|
|
94
|
+
declare function parseBondMessage(message: string): BondFields | null;
|
|
95
|
+
declare function bondId(message: string): string;
|
|
96
|
+
declare function bondNullifier(key: Uint8Array, audience: string, outpoint: string): string;
|
|
97
|
+
declare function bondFloors(sats: number, ageBlocks: number): {
|
|
98
|
+
sats_floor: number;
|
|
99
|
+
days_floor: number;
|
|
100
|
+
};
|
|
101
|
+
declare function verifyBondSignature(opts: {
|
|
102
|
+
message: string;
|
|
103
|
+
signature: string;
|
|
104
|
+
subject?: string;
|
|
105
|
+
now?: Date;
|
|
106
|
+
skewSeconds?: number;
|
|
107
|
+
maxWindowSeconds?: number;
|
|
108
|
+
}): {
|
|
109
|
+
ok: true;
|
|
110
|
+
fields: BondFields;
|
|
111
|
+
bond_id: string;
|
|
112
|
+
} | {
|
|
113
|
+
ok: false;
|
|
114
|
+
reason: BondReason;
|
|
115
|
+
};
|
|
116
|
+
declare function checkBondCoin(opts: {
|
|
117
|
+
address: string;
|
|
118
|
+
outpoint: string;
|
|
119
|
+
source: BondChainSource;
|
|
120
|
+
minConfirmations?: number;
|
|
121
|
+
}): Promise<{
|
|
122
|
+
ok: true;
|
|
123
|
+
sats: number;
|
|
124
|
+
confirmed_height: number;
|
|
125
|
+
tip: ChainTip;
|
|
126
|
+
} | {
|
|
127
|
+
ok: false;
|
|
128
|
+
reason: BondReason;
|
|
129
|
+
}>;
|
|
130
|
+
declare function verifyBond(opts: {
|
|
131
|
+
message: string;
|
|
132
|
+
signature: string;
|
|
133
|
+
source: BondChainSource;
|
|
134
|
+
subject?: string;
|
|
135
|
+
now?: Date;
|
|
136
|
+
maxWindowSeconds?: number;
|
|
137
|
+
minConfirmations?: number;
|
|
138
|
+
}): Promise<BondResult>;
|
|
139
|
+
declare function esploraSource(base?: string, opts?: {
|
|
140
|
+
timeoutMs?: number;
|
|
141
|
+
fetch?: typeof fetch;
|
|
142
|
+
}): BondChainSource;
|
|
143
|
+
|
|
39
144
|
declare function isNip07Available(): boolean;
|
|
40
145
|
declare function getNostrPublicKey(): Promise<string | null>;
|
|
41
146
|
declare function signNostrEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent | null>;
|
|
@@ -53,4 +158,4 @@ interface StatusMeta {
|
|
|
53
158
|
}
|
|
54
159
|
declare const STATUS_META: Record<StatusCode, StatusMeta>;
|
|
55
160
|
|
|
56
|
-
export { type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, VerifyOptions, check, getNip07Info, getNostrPublicKey, isNip07Available, nostrIdentifierForms, nostrPubkeyToHex, signNostrEvent };
|
|
161
|
+
export { BOND_DAYS_FLOORS, BOND_MAX_WINDOW_SECONDS, BOND_MIN_CONFIRMATIONS, BOND_SATS_FLOORS, type BondChainSource, type BondCoin, type BondFields, type BondReason, type BondResult, type ChainTip, type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, type VerifiedBond, VerifyOptions, bondFloors, bondId, bondNullifier, buildBondMessage, check, checkBondCoin, esploraSource, getNip07Info, getNostrPublicKey, isNip07Available, issueBondMessage, nostrIdentifierForms, nostrPubkeyToHex, parseBondMessage, signNostrEvent, verifyBond, verifyBondSignature };
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
var base = require('@scure/base');
|
|
4
4
|
var buffer = require('buffer');
|
|
5
5
|
var bip322 = require('@orangecheck/bip322');
|
|
6
|
-
var
|
|
6
|
+
var hmac_js = require('@noble/hashes/hmac.js');
|
|
7
7
|
var sha2_js = require('@noble/hashes/sha2.js');
|
|
8
|
+
var utils_js = require('@noble/hashes/utils.js');
|
|
9
|
+
var secp256k1_js = require('@noble/curves/secp256k1.js');
|
|
8
10
|
|
|
9
11
|
// src/canonical.ts
|
|
10
12
|
function toLowerHex(bytes) {
|
|
@@ -1026,7 +1028,7 @@ async function getAddressUtxos(address, network, opts) {
|
|
|
1026
1028
|
log4.error({ endpoints, error: lastError?.message, network }, "All Esplora endpoints failed");
|
|
1027
1029
|
throw new Error(`esplora_all_failed: ${lastError?.message || "unknown"}`);
|
|
1028
1030
|
}
|
|
1029
|
-
function computeMetrics(utxos, bondAmount) {
|
|
1031
|
+
function computeMetrics(utxos, bondAmount, coverSats) {
|
|
1030
1032
|
const confirmedUtxos = utxos.filter((u) => u.status.confirmed);
|
|
1031
1033
|
if (confirmedUtxos.length === 0) {
|
|
1032
1034
|
return { sats_bonded: 0, days_unspent: 0, confirmed_balance: 0 };
|
|
@@ -1059,6 +1061,23 @@ function computeMetrics(utxos, bondAmount) {
|
|
|
1059
1061
|
};
|
|
1060
1062
|
}
|
|
1061
1063
|
const sats_bonded = confirmed_balance;
|
|
1064
|
+
if (coverSats !== void 0 && coverSats > 0) {
|
|
1065
|
+
const sorted = [...confirmedUtxos].sort(
|
|
1066
|
+
(a, b) => (a.status.block_height ?? Infinity) - (b.status.block_height ?? Infinity)
|
|
1067
|
+
);
|
|
1068
|
+
let sum = 0;
|
|
1069
|
+
let youngestTime = 0;
|
|
1070
|
+
for (const utxo of sorted) {
|
|
1071
|
+
sum += utxo.value;
|
|
1072
|
+
youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1e3);
|
|
1073
|
+
if (sum >= coverSats) break;
|
|
1074
|
+
}
|
|
1075
|
+
const days_unspent2 = Math.max(
|
|
1076
|
+
0,
|
|
1077
|
+
Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
|
|
1078
|
+
);
|
|
1079
|
+
return { sats_bonded, days_unspent: days_unspent2, confirmed_balance };
|
|
1080
|
+
}
|
|
1062
1081
|
const oldestTime = Math.min(
|
|
1063
1082
|
...confirmedUtxos.map((u) => u.status.block_time || Date.now() / 1e3)
|
|
1064
1083
|
);
|
|
@@ -1122,7 +1141,8 @@ async function verify(input, opts = {}) {
|
|
|
1122
1141
|
});
|
|
1123
1142
|
const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(
|
|
1124
1143
|
utxos,
|
|
1125
|
-
parsed.bond
|
|
1144
|
+
parsed.bond,
|
|
1145
|
+
opts.coverSats
|
|
1126
1146
|
);
|
|
1127
1147
|
if (parsed.bond !== void 0 && parsed.bond > 0) {
|
|
1128
1148
|
if (confirmed_balance < parsed.bond) {
|
|
@@ -1227,7 +1247,7 @@ async function check(params) {
|
|
|
1227
1247
|
sig: env.signature,
|
|
1228
1248
|
scheme: env.scheme
|
|
1229
1249
|
},
|
|
1230
|
-
verifyOptions
|
|
1250
|
+
{ ...verifyOptions, coverSats: minSats > 0 ? minSats : void 0 }
|
|
1231
1251
|
);
|
|
1232
1252
|
const sats = outcome.metrics?.sats_bonded ?? 0;
|
|
1233
1253
|
const days = outcome.metrics?.days_unspent ?? 0;
|
|
@@ -1431,6 +1451,181 @@ async function verifyChallenge(opts) {
|
|
|
1431
1451
|
...purpose !== void 0 ? { purpose } : {}
|
|
1432
1452
|
};
|
|
1433
1453
|
}
|
|
1454
|
+
var HEADER = "orangecheck-bond";
|
|
1455
|
+
var ACK = "I back this subject with this coin. It stays in my wallet.";
|
|
1456
|
+
var FIELDS = ["address", "outpoint", "subject", "nonce", "issued_at", "expires_at"];
|
|
1457
|
+
var BOND_SATS_FLOORS = [1e3, 1e4, 1e5, 1e6, 1e7];
|
|
1458
|
+
var BOND_DAYS_FLOORS = [7, 30, 90, 365];
|
|
1459
|
+
var BOND_MIN_CONFIRMATIONS = 6;
|
|
1460
|
+
var BOND_MAX_WINDOW_SECONDS = 3600;
|
|
1461
|
+
var OUTPOINT_RE = /^[0-9a-f]{64}:(0|[1-9][0-9]{0,9})$/;
|
|
1462
|
+
var NONCE_RE = /^[0-9a-f]{32}$/;
|
|
1463
|
+
var TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
1464
|
+
var SUBJECT_RE = /^[\x21-\x7e]{1,256}$/;
|
|
1465
|
+
function iso2(d) {
|
|
1466
|
+
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1467
|
+
}
|
|
1468
|
+
function instant(s) {
|
|
1469
|
+
const t = Date.parse(s);
|
|
1470
|
+
return TIME_RE.test(s) && Number.isFinite(t) && iso2(new Date(t)) === s;
|
|
1471
|
+
}
|
|
1472
|
+
function randomNonce2() {
|
|
1473
|
+
const b = new Uint8Array(16);
|
|
1474
|
+
globalThis.crypto.getRandomValues(b);
|
|
1475
|
+
return utils_js.bytesToHex(b);
|
|
1476
|
+
}
|
|
1477
|
+
function wellFormed(f) {
|
|
1478
|
+
return bip322.decodeAddress(f.address) !== null && OUTPOINT_RE.test(f.outpoint) && SUBJECT_RE.test(f.subject) && NONCE_RE.test(f.nonce) && instant(f.issued_at) && instant(f.expires_at) && Date.parse(f.expires_at) > Date.parse(f.issued_at);
|
|
1479
|
+
}
|
|
1480
|
+
function buildBondMessage(fields) {
|
|
1481
|
+
if (!wellFormed(fields)) throw new Error("bond fields are malformed");
|
|
1482
|
+
return [HEADER, ...FIELDS.map((k) => `${k}: ${fields[k]}`), `ack: ${ACK}`].join("\n") + "\n";
|
|
1483
|
+
}
|
|
1484
|
+
function issueBondMessage(opts) {
|
|
1485
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
1486
|
+
const expires = new Date(now.getTime() + (opts.ttlSeconds ?? 600) * 1e3);
|
|
1487
|
+
const nonce = randomNonce2();
|
|
1488
|
+
const message = buildBondMessage({
|
|
1489
|
+
address: opts.address,
|
|
1490
|
+
outpoint: opts.outpoint,
|
|
1491
|
+
subject: opts.subject,
|
|
1492
|
+
nonce,
|
|
1493
|
+
issued_at: iso2(now),
|
|
1494
|
+
expires_at: iso2(expires)
|
|
1495
|
+
});
|
|
1496
|
+
return { message, nonce, expiresAt: expires.getTime() };
|
|
1497
|
+
}
|
|
1498
|
+
function parseBondMessage(message) {
|
|
1499
|
+
if (!message.endsWith("\n") || message.includes("\r")) return null;
|
|
1500
|
+
const lines = message.slice(0, -1).split("\n");
|
|
1501
|
+
if (lines.length !== FIELDS.length + 2 || lines[0] !== HEADER) return null;
|
|
1502
|
+
if (lines[lines.length - 1] !== `ack: ${ACK}`) return null;
|
|
1503
|
+
const out = {};
|
|
1504
|
+
for (const [i, key] of FIELDS.entries()) {
|
|
1505
|
+
const line = lines[i + 1];
|
|
1506
|
+
const prefix = `${key}: `;
|
|
1507
|
+
if (!line.startsWith(prefix)) return null;
|
|
1508
|
+
out[key] = line.slice(prefix.length);
|
|
1509
|
+
}
|
|
1510
|
+
const fields = out;
|
|
1511
|
+
return wellFormed(fields) && buildBondMessage(fields) === message ? fields : null;
|
|
1512
|
+
}
|
|
1513
|
+
function bondId(message) {
|
|
1514
|
+
return utils_js.bytesToHex(sha2_js.sha256(utils_js.utf8ToBytes(message)));
|
|
1515
|
+
}
|
|
1516
|
+
function bondNullifier(key, audience, outpoint) {
|
|
1517
|
+
if (key.length < 32) throw new Error("bond nullifier key must be at least 32 bytes");
|
|
1518
|
+
const message = utils_js.utf8ToBytes(`orangecheck-bond-nullifier
|
|
1519
|
+
${audience}
|
|
1520
|
+
${outpoint}`);
|
|
1521
|
+
return utils_js.bytesToHex(hmac_js.hmac(sha2_js.sha256, key, message));
|
|
1522
|
+
}
|
|
1523
|
+
function bondFloors(sats, ageBlocks) {
|
|
1524
|
+
const days = Math.floor(ageBlocks / 144);
|
|
1525
|
+
const floor = (ladder, v) => ladder.filter((f) => v >= f).reduce((a, b) => Math.max(a, b), 0);
|
|
1526
|
+
return { sats_floor: floor(BOND_SATS_FLOORS, sats), days_floor: floor(BOND_DAYS_FLOORS, days) };
|
|
1527
|
+
}
|
|
1528
|
+
function verifyBondSignature(opts) {
|
|
1529
|
+
const fields = parseBondMessage(opts.message);
|
|
1530
|
+
if (!fields) return { ok: false, reason: "bond_malformed" };
|
|
1531
|
+
if (opts.subject !== void 0 && fields.subject !== opts.subject) {
|
|
1532
|
+
return { ok: false, reason: "bond_subject_mismatch" };
|
|
1533
|
+
}
|
|
1534
|
+
const window2 = Date.parse(fields.expires_at) - Date.parse(fields.issued_at);
|
|
1535
|
+
if (window2 > (opts.maxWindowSeconds ?? BOND_MAX_WINDOW_SECONDS) * 1e3) {
|
|
1536
|
+
return { ok: false, reason: "bond_window_too_long" };
|
|
1537
|
+
}
|
|
1538
|
+
const now = (opts.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
1539
|
+
const skew = (opts.skewSeconds ?? 60) * 1e3;
|
|
1540
|
+
if (Date.parse(fields.issued_at) - skew > now) return { ok: false, reason: "bond_not_yet_valid" };
|
|
1541
|
+
if (Date.parse(fields.expires_at) + skew < now) return { ok: false, reason: "bond_expired" };
|
|
1542
|
+
if (!bip322.verifySignature(fields.address, opts.message, opts.signature)) {
|
|
1543
|
+
return { ok: false, reason: "sig_invalid" };
|
|
1544
|
+
}
|
|
1545
|
+
return { ok: true, fields, bond_id: bondId(opts.message) };
|
|
1546
|
+
}
|
|
1547
|
+
async function checkBondCoin(opts) {
|
|
1548
|
+
const decoded = bip322.decodeAddress(opts.address);
|
|
1549
|
+
if (!decoded || !OUTPOINT_RE.test(opts.outpoint)) return { ok: false, reason: "bond_malformed" };
|
|
1550
|
+
let coin;
|
|
1551
|
+
let tip;
|
|
1552
|
+
try {
|
|
1553
|
+
[coin, tip] = await Promise.all([opts.source.coin(opts.outpoint), opts.source.tip()]);
|
|
1554
|
+
} catch {
|
|
1555
|
+
return { ok: false, reason: "chain_unavailable" };
|
|
1556
|
+
}
|
|
1557
|
+
if (!coin) return { ok: false, reason: "coin_not_found" };
|
|
1558
|
+
if (coin.scriptPubKey !== utils_js.bytesToHex(decoded.scriptPubKey)) {
|
|
1559
|
+
return { ok: false, reason: "coin_address_mismatch" };
|
|
1560
|
+
}
|
|
1561
|
+
if (coin.confirmedHeight === null) return { ok: false, reason: "coin_unconfirmed" };
|
|
1562
|
+
if (coin.spent) return { ok: false, reason: "coin_spent" };
|
|
1563
|
+
const confirmations = tip.height - coin.confirmedHeight + 1;
|
|
1564
|
+
if (confirmations < (opts.minConfirmations ?? BOND_MIN_CONFIRMATIONS)) {
|
|
1565
|
+
return { ok: false, reason: "coin_too_recent" };
|
|
1566
|
+
}
|
|
1567
|
+
return { ok: true, sats: coin.sats, confirmed_height: coin.confirmedHeight, tip };
|
|
1568
|
+
}
|
|
1569
|
+
async function verifyBond(opts) {
|
|
1570
|
+
const sig = verifyBondSignature(opts);
|
|
1571
|
+
if (!sig.ok) return sig;
|
|
1572
|
+
const coin = await checkBondCoin({
|
|
1573
|
+
address: sig.fields.address,
|
|
1574
|
+
outpoint: sig.fields.outpoint,
|
|
1575
|
+
source: opts.source,
|
|
1576
|
+
minConfirmations: opts.minConfirmations
|
|
1577
|
+
});
|
|
1578
|
+
if (!coin.ok) return coin;
|
|
1579
|
+
return {
|
|
1580
|
+
ok: true,
|
|
1581
|
+
bond: {
|
|
1582
|
+
bond_id: sig.bond_id,
|
|
1583
|
+
subject: sig.fields.subject,
|
|
1584
|
+
address: sig.fields.address,
|
|
1585
|
+
outpoint: sig.fields.outpoint,
|
|
1586
|
+
sats: coin.sats,
|
|
1587
|
+
confirmed_height: coin.confirmed_height,
|
|
1588
|
+
tip: coin.tip
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
function esploraSource(base = "https://mempool.space/api", opts = {}) {
|
|
1593
|
+
const get = async (path, missingOk = false) => {
|
|
1594
|
+
const r = await (opts.fetch ?? fetch)(`${base}${path}`, {
|
|
1595
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5e3)
|
|
1596
|
+
});
|
|
1597
|
+
if (!r.ok && !(missingOk && r.status === 404)) {
|
|
1598
|
+
throw new Error(`esplora ${path}: HTTP ${r.status}`);
|
|
1599
|
+
}
|
|
1600
|
+
return r;
|
|
1601
|
+
};
|
|
1602
|
+
return {
|
|
1603
|
+
async tip() {
|
|
1604
|
+
const [h, hash] = await Promise.all([get("/blocks/tip/height"), get("/blocks/tip/hash")]);
|
|
1605
|
+
const tip = { height: Number(await h.text()), hash: (await hash.text()).trim() };
|
|
1606
|
+
if (!Number.isSafeInteger(tip.height) || !/^[0-9a-f]{64}$/.test(tip.hash)) {
|
|
1607
|
+
throw new Error("esplora tip: malformed");
|
|
1608
|
+
}
|
|
1609
|
+
return tip;
|
|
1610
|
+
},
|
|
1611
|
+
async coin(outpoint) {
|
|
1612
|
+
const [txid, voutStr] = outpoint.split(":");
|
|
1613
|
+
const vout = Number(voutStr);
|
|
1614
|
+
const txr = await get(`/tx/${txid}`, true);
|
|
1615
|
+
if (txr.status === 404) return null;
|
|
1616
|
+
const tx = await txr.json();
|
|
1617
|
+
const out = tx.vout[vout];
|
|
1618
|
+
if (!out) return null;
|
|
1619
|
+
const spend = await (await get(`/tx/${txid}/outspend/${vout}`)).json();
|
|
1620
|
+
return {
|
|
1621
|
+
sats: out.value,
|
|
1622
|
+
scriptPubKey: out.scriptpubkey.toLowerCase(),
|
|
1623
|
+
confirmedHeight: tx.status.confirmed ? tx.status.block_height ?? null : null,
|
|
1624
|
+
spent: spend.spent
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1434
1629
|
|
|
1435
1630
|
// src/utils/rate-limiter.ts
|
|
1436
1631
|
var RateLimitError = class extends Error {
|
|
@@ -2220,17 +2415,27 @@ var STATUS_META = {
|
|
|
2220
2415
|
}
|
|
2221
2416
|
};
|
|
2222
2417
|
|
|
2418
|
+
exports.BOND_DAYS_FLOORS = BOND_DAYS_FLOORS;
|
|
2419
|
+
exports.BOND_MAX_WINDOW_SECONDS = BOND_MAX_WINDOW_SECONDS;
|
|
2420
|
+
exports.BOND_MIN_CONFIRMATIONS = BOND_MIN_CONFIRMATIONS;
|
|
2421
|
+
exports.BOND_SATS_FLOORS = BOND_SATS_FLOORS;
|
|
2223
2422
|
exports.DEFAULT_RELAYS = DEFAULT_RELAYS;
|
|
2224
2423
|
exports.FANOUT_DEADLINE_MS = FANOUT_DEADLINE_MS;
|
|
2225
2424
|
exports.STATUS_META = STATUS_META;
|
|
2425
|
+
exports.bondFloors = bondFloors;
|
|
2426
|
+
exports.bondId = bondId;
|
|
2427
|
+
exports.bondNullifier = bondNullifier;
|
|
2428
|
+
exports.buildBondMessage = buildBondMessage;
|
|
2226
2429
|
exports.buildCanonicalMessage = buildCanonicalMessage;
|
|
2227
2430
|
exports.check = check;
|
|
2431
|
+
exports.checkBondCoin = checkBondCoin;
|
|
2228
2432
|
exports.computeAllScores = computeAllScores;
|
|
2229
2433
|
exports.computeScore = computeScore;
|
|
2230
2434
|
exports.createAttestation = createAttestation;
|
|
2231
2435
|
exports.createAttestationEnvelope = createAttestationEnvelope;
|
|
2232
2436
|
exports.createAttestationEvent = createAttestationEvent;
|
|
2233
2437
|
exports.discoverAttestations = discoverAttestations;
|
|
2438
|
+
exports.esploraSource = esploraSource;
|
|
2234
2439
|
exports.extractAttestationIdFromUrl = extractAttestationIdFromUrl;
|
|
2235
2440
|
exports.formatIdentities = formatIdentities;
|
|
2236
2441
|
exports.formatIdentitiesForDisplay = formatIdentitiesForDisplay;
|
|
@@ -2260,10 +2465,12 @@ exports.isNip07Available = isNip07Available;
|
|
|
2260
2465
|
exports.isNostrIdentityVerified = isNostrIdentityVerified;
|
|
2261
2466
|
exports.isValidDomain = isValidDomain;
|
|
2262
2467
|
exports.isValidTweetUrl = isValidTweetUrl;
|
|
2468
|
+
exports.issueBondMessage = issueBondMessage;
|
|
2263
2469
|
exports.issueChallenge = issueChallenge;
|
|
2264
2470
|
exports.nostrIdentifierForms = nostrIdentifierForms;
|
|
2265
2471
|
exports.nostrPubkeyToHex = nostrPubkeyToHex;
|
|
2266
2472
|
exports.parseAttestationFromEvent = parseAttestationFromEvent;
|
|
2473
|
+
exports.parseBondMessage = parseBondMessage;
|
|
2267
2474
|
exports.parseIdentities = parseIdentities;
|
|
2268
2475
|
exports.publishAttestation = publishAttestation;
|
|
2269
2476
|
exports.publishToRelays = publishToRelays;
|
|
@@ -2277,6 +2484,8 @@ exports.supportsAutomaticVerification = supportsAutomaticVerification;
|
|
|
2277
2484
|
exports.verify = verify;
|
|
2278
2485
|
exports.verifyAllIdentities = verifyAllIdentities;
|
|
2279
2486
|
exports.verifyAttestationById = verifyAttestationById;
|
|
2487
|
+
exports.verifyBond = verifyBond;
|
|
2488
|
+
exports.verifyBondSignature = verifyBondSignature;
|
|
2280
2489
|
exports.verifyChallenge = verifyChallenge;
|
|
2281
2490
|
exports.verifyDnsIdentity = verifyDnsIdentity;
|
|
2282
2491
|
exports.verifyGitHubIdentity = verifyGitHubIdentity;
|