dcr-ts 0.2.1 → 0.3.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/CHANGELOG.md +180 -0
- package/README.md +5 -3
- package/dist/index.cjs +121 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -10
- package/dist/index.d.ts +56 -10
- package/dist/index.js +121 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,186 @@ Notable changes per release. Dates are release dates.
|
|
|
4
4
|
|
|
5
5
|
This library has **not** been independently audited. See [SECURITY.md](SECURITY.md).
|
|
6
6
|
|
|
7
|
+
## 0.3.0 — 2026-09-08
|
|
8
|
+
|
|
9
|
+
A second automated review pass, and the first whose findings were reproduced by
|
|
10
|
+
running them rather than read from the source. Everything below was confirmed by
|
|
11
|
+
executing it against the published 0.2.1, and every guard but two was checked by
|
|
12
|
+
deleting it and watching a test fail — the two exceptions are defence in depth
|
|
13
|
+
behind `Writer.bytes` and `@scure`'s own catch, so deleting either leaves the
|
|
14
|
+
downstream guard to catch the same case.
|
|
15
|
+
|
|
16
|
+
Read the breaking section before upgrading: several arguments that 0.2.1 accepted
|
|
17
|
+
and answered now throw, and one predicate answers differently.
|
|
18
|
+
|
|
19
|
+
### Fixed — found by review
|
|
20
|
+
|
|
21
|
+
- **`ExtendedKey.fromSeed` accepted a *string* seed and derived a different,
|
|
22
|
+
fully valid master key.** `@noble`'s `toBytes` UTF-8-encodes a string before its
|
|
23
|
+
own byte check runs, so a seed handed over as hex text never failed — it became
|
|
24
|
+
the seed material. A 64-character hex seed, the likeliest wrong value at that
|
|
25
|
+
call site, sits inside the documented 16–64 window. The result signs, serializes
|
|
26
|
+
and produces addresses that pass this library's own `isValidAddress`, for a
|
|
27
|
+
wallet nobody meant to create. The worst shape of failure this package exists to
|
|
28
|
+
prevent, and it was silent.
|
|
29
|
+
- **The four hash-address encoders checked length but not type.** A 20-character
|
|
30
|
+
string, a 20-element `Array<number>`, an array-like or a `Float64Array` all
|
|
31
|
+
satisfy `.length === 20`, and `Uint8Array.prototype.set` coerces element by
|
|
32
|
+
element, so `pubKeyHashAddress`, `scriptHashAddress`, `pubKeyHashEd25519Address`
|
|
33
|
+
and `pubKeyHashSchnorrAddress` returned well-formed, valid-checksum addresses for
|
|
34
|
+
hashes nobody holds the preimage of. `assertHash160` records this reasoning and
|
|
35
|
+
the pay-to-hash script builders were fixed with it in 0.2.0; their address-side
|
|
36
|
+
siblings were missed, in the same file that pass edited. The realistic way in is
|
|
37
|
+
not a bare string but a hash split into hex pairs and never parsed — `"0a"`
|
|
38
|
+
coerces to 0 and `"10"` to decimal 10, so the address shares a prefix with the
|
|
39
|
+
intended one and differs in the middle.
|
|
40
|
+
- **`calcSignatureHash` type-checks its cached prefix.** The length test stood
|
|
41
|
+
alone, and `Writer.bytes` coerces, so a 32-character string committed the
|
|
42
|
+
preimage to 32 zero bytes: the prefix half stopped distinguishing transactions
|
|
43
|
+
entirely and `rawTxInSignature` returned a well-formed signature over a message
|
|
44
|
+
the caller did not hold.
|
|
45
|
+
- **`addOutput` rejects a value that is not a `bigint`.** `Writer.i64`'s bounds
|
|
46
|
+
test is a BigInt relational comparison, which coerces rather than refuses, so
|
|
47
|
+
`""`, `" "` and `[]` all became a zero-atom output on a transaction that built,
|
|
48
|
+
hashed and signed cleanly — sending the whole input value to the miner as fee.
|
|
49
|
+
The plain `number` that looks like the likelier mistake was the one case that
|
|
50
|
+
already threw. `addInput`'s `valueIn` and `signatureScript` reach the same
|
|
51
|
+
writer and are checked too.
|
|
52
|
+
- **A BIP39 passphrase that is not a string meant `String(v)`.** `@scure`'s salt is
|
|
53
|
+
the concatenation `"mnemonic" + passphrase`, which never rejects, so `null`
|
|
54
|
+
silently meant the passphrase `"null"` — and through `mnemonicToMasterKey`, a
|
|
55
|
+
different wallet with nothing raised. `passphrase ?? null` is ordinary
|
|
56
|
+
JavaScript, and a database column that is NULL for "no passphrase" is the
|
|
57
|
+
ordinary way to arrive at one. Only omitting the argument takes the default.
|
|
58
|
+
- **A network is matched by its version prefixes, not by object identity.**
|
|
59
|
+
`decodeAddress` found its entry with `===`, the last place a `Network` turned on
|
|
60
|
+
the object rather than its contents, while every encoder read the prefix fields
|
|
61
|
+
off whatever it was handed. So one process holding two equal-but-not-identical
|
|
62
|
+
`mainnet` objects could encode an address with one and have the other refuse to
|
|
63
|
+
decode it — and this package invites exactly that, since its own `exports` map
|
|
64
|
+
hands `import` the ESM bundle and `require` the CJS one. `structuredClone`, a
|
|
65
|
+
worker boundary and a network read back from a JSON config do the same.
|
|
66
|
+
`DcrError` has been branded with a registry symbol for this reason since 0.2.0;
|
|
67
|
+
`Network` was not covered. The symptom was self-contradictory: a good address
|
|
68
|
+
rejected as `wrong-network`, reported as *"for mainnet, not mainnet"*.
|
|
69
|
+
- **`signP2PKHInputs` no longer drops a signing it was asked to perform.** Two
|
|
70
|
+
entries sharing an `idx` let the last win: the earlier signature was computed and
|
|
71
|
+
discarded, and the input the caller meant to cover with the mistyped entry kept
|
|
72
|
+
its empty signature script. N signings requested, N−1 delivered, and no way to
|
|
73
|
+
notice, since the return value is the transaction rather than a per-input result.
|
|
74
|
+
Both outcomes fail closed — the overwritten input is unspendable — so this
|
|
75
|
+
reports the mistake rather than repairing it. Rejected before the prefix hash is
|
|
76
|
+
taken, so a refused batch leaves the transaction untouched.
|
|
77
|
+
- **`isValidPublicKey` answered `true` for a hex string, and `verifyHash` took
|
|
78
|
+
the same string.** `@noble` accepts hex wherever it accepts a key, so the
|
|
79
|
+
predicate attested to a key the caller was not holding as bytes and the verifier
|
|
80
|
+
agreed with it. Both now require bytes: the predicate answers `false`, and
|
|
81
|
+
`verifyHash` throws `invalid-argument`, for the reason it already threw on a
|
|
82
|
+
wrong-length hash — a wrong-typed key is a caller bug, not a failed
|
|
83
|
+
verification. `isValidPublicKey`'s two siblings in that file already carried the
|
|
84
|
+
check.
|
|
85
|
+
- **Nine more exported entries proved their argument's shape before reading it.**
|
|
86
|
+
Four of them failed *silently* rather than loudly: `base58Encode` encoded a
|
|
87
|
+
digit string into a plausible base58 string, and `Writer.u64`, `Writer.i64` and
|
|
88
|
+
`Writer.bytes` each accepted a wrong-shaped operand and wrote bytes for it.
|
|
89
|
+
`copyOf`, `dcrToAtoms`, `atomsToDcr`, `ExtendedKey.derivePath` and
|
|
90
|
+
`ExtendedKey.fromSerialized` reported bare engine errors instead of a `DcrError`
|
|
91
|
+
a caller can classify with `hasErrorCode`.
|
|
92
|
+
- **`encodeWif` names a rejected signature type without quoting it.** The one
|
|
93
|
+
`${}` interpolation left in the library that a caller-chosen object could reach,
|
|
94
|
+
so its `toString` decided what the error message said; an object returning 5,000
|
|
95
|
+
characters of text with an embedded ANSI escape produced exactly that message.
|
|
96
|
+
`shown` exists for this and is documented at length; this line predated it.
|
|
97
|
+
|
|
98
|
+
### Changed — breaking
|
|
99
|
+
|
|
100
|
+
Every change is fail-closed, and every guard in the section above rejects input
|
|
101
|
+
the *typed* API never accepted — but two of the three below can be reached by code
|
|
102
|
+
that typechecks, because neither index distinctness nor wordlist length is
|
|
103
|
+
expressible in the type system. A JavaScript caller can reach all of them.
|
|
104
|
+
|
|
105
|
+
- **A supplied `network` must be a `Network`.** `decodeAddress(address, null)` —
|
|
106
|
+
and `0`, `""`, `false`, a partial object, anything that is not a network — now
|
|
107
|
+
throws `invalid-argument` instead of falling back to network-agnostic decoding,
|
|
108
|
+
which is what `addressToScript` already refused. `isValidAddress` with such a
|
|
109
|
+
value goes from `true` to **`false`**, silently, which is the one change here
|
|
110
|
+
that does not announce itself: a caller storing the selected network as `null`
|
|
111
|
+
to mean "any" will find every address rejected. Omitting the argument still
|
|
112
|
+
infers the network, as before.
|
|
113
|
+
- **A malformed wordlist throws where `validateMnemonic` used to answer.** The
|
|
114
|
+
phrase is still answered `false` for a non-string, which is what a predicate is
|
|
115
|
+
for and what `mnemonicToMasterKey` documents relying on — but now by decision
|
|
116
|
+
rather than by `@scure` catching its own `TypeError`. The wordlist is the
|
|
117
|
+
caller's configuration rather than the subject being predicated, so a list that
|
|
118
|
+
is not 2048 words throws, as it already did at every other entry in that module.
|
|
119
|
+
- **`signP2PKHInputs` rejects a repeated `idx`.** It used to let the later entry
|
|
120
|
+
overwrite the earlier one's signature script, dropping a signing the caller
|
|
121
|
+
asked for with nothing to notice it by. `readonly P2PKHInputToSign[]` cannot
|
|
122
|
+
express distinctness, so this is reachable from code that typechecks.
|
|
123
|
+
- **Wrong-shaped arguments throw where they were coerced.** Every guard in the
|
|
124
|
+
section above is a behaviour change for a JavaScript caller, and one is worth
|
|
125
|
+
naming on its own: `pubKeyHashAddress(Array.from(hash), network)` returned the
|
|
126
|
+
correct address in 0.2.1 and now throws, because a plain `Array` is not bytes.
|
|
127
|
+
`Array.from` is how a `Uint8Array` degrades through `JSON.stringify`, so a hash
|
|
128
|
+
rebuilt from JSON is the ordinary way to arrive there. Convert with
|
|
129
|
+
`Uint8Array.from` before calling.
|
|
130
|
+
|
|
131
|
+
### Fixed — regressions in the guards above, before release
|
|
132
|
+
|
|
133
|
+
Caught by reproducing the whole diff against the published 0.2.1 rather than by
|
|
134
|
+
the tests written alongside the guards, which is what that comparison is for.
|
|
135
|
+
|
|
136
|
+
- **Two guards broke the contract they exist to serve.** `validateMnemonic(phrase,
|
|
137
|
+
null)` and `decodeAddress(address, partialNetwork)` each proved one field and
|
|
138
|
+
then read others a partial value does not have, throwing a bare `TypeError`
|
|
139
|
+
where 0.2.1 had answered `false` and thrown a typed `wrong-network`. Both are
|
|
140
|
+
`DcrError`s again, so `hasErrorCode` classifies them and a catch that rethrows
|
|
141
|
+
non-`DcrError`s no longer rethrows.
|
|
142
|
+
- **Two option guards disagreed with the defaults beside them.**
|
|
143
|
+
`addInput(outPoint, { valueIn: null })` threw, where `opts.valueIn ??
|
|
144
|
+
NULL_VALUE_IN` had always taken the documented default — so `valueIn` behaved
|
|
145
|
+
differently from `sequence` and `blockHeight` next to it. JSON carries `null`
|
|
146
|
+
and never `undefined`, so an input rebuilt from an RPC response is the ordinary
|
|
147
|
+
way there. `signatureScript` is aligned the same way: `null` and `""` mean "no
|
|
148
|
+
script" as they always did, and only a *truthy* non-`Uint8Array` is rejected.
|
|
149
|
+
|
|
150
|
+
### Fixed — CI
|
|
151
|
+
|
|
152
|
+
- **A registry outage no longer reds the build, or aborts a release.**
|
|
153
|
+
`npm audit` exits 1 for three unrelated reasons — it found an advisory, the
|
|
154
|
+
registry was unreachable, or the registry rejected the request — and reports
|
|
155
|
+
all three as `audit endpoint returned an error`. Only the first should fail a
|
|
156
|
+
build, and on 2026-09-04 a 503 failed CI after npm spent seven minutes on its
|
|
157
|
+
own internal retries. The same step gates `release.yml`, where that would abort
|
|
158
|
+
a publish and leave a signed tag with nothing shipped against it.
|
|
159
|
+
`.github/audit-production-deps.sh` now tells them apart by HTTP status: an
|
|
160
|
+
advisory and a rejected request each fail on the first attempt, since retrying
|
|
161
|
+
fixes neither, while a 5xx, a 429 or a transport failure is retried with
|
|
162
|
+
backoff and npm's own retrying is capped so each attempt fails in seconds
|
|
163
|
+
rather than minutes. An audit that never ran still fails — passing a gate that
|
|
164
|
+
did not execute is how a security check stops being one — but it says so in
|
|
165
|
+
those words. Both workflows run the one script so they cannot drift.
|
|
166
|
+
- **CI audits through the endpoint the registry still supports.** The npm bundled
|
|
167
|
+
with the Node runtime posts to `/-/npm/v1/security/audits/quick`, which the
|
|
168
|
+
registry is retiring and which answered a 400 `Invalid package tree` here; a
|
|
169
|
+
current npm uses `/-/npm/v1/security/advisories/bulk`. The `package` job now
|
|
170
|
+
installs one, as `release.yml` already did for OIDC. The informational full
|
|
171
|
+
audit, which gates nothing, is bounded by a timeout rather than left to hang.
|
|
172
|
+
|
|
173
|
+
### Documented — a dcrd-faithful non-canonical address
|
|
174
|
+
|
|
175
|
+
- **Ed25519 pay-to-pubkey addresses have one alias, and it matches dcrd.** The
|
|
176
|
+
oddness bit means nothing for an Ed25519 key, so it is masked off rather than
|
|
177
|
+
required to be clear: identifier bytes `0x01` and `0x81` name the same address,
|
|
178
|
+
decoding to the same key and the same pkScript, and only the `0x01` form is what
|
|
179
|
+
`pubKeyEd25519Address` emits. `encode(decode(x))` is therefore not always `x`
|
|
180
|
+
for this one kind. dcrd's `stdaddr.DecodeAddressV0` masks identically and its
|
|
181
|
+
`AddressPubKeyEd25519V0.String()` re-emits the `0x01` form too — verified
|
|
182
|
+
directly against dcrd, address for address and script for script. A sweep of
|
|
183
|
+
every identifier byte across all four networks, every WIF suite byte and every
|
|
184
|
+
extended-key version found this to be the only such alias in the decoders. Now
|
|
185
|
+
pinned by a test, so neither half of the parity can drift unnoticed.
|
|
186
|
+
|
|
7
187
|
## 0.2.1 — 2026-08-25
|
|
8
188
|
|
|
9
189
|
**No changes to the library.** `src/` is byte-identical to 0.2.0 and the built
|
package/README.md
CHANGED
|
@@ -145,9 +145,11 @@ signP2PKHInputs(tx, [
|
|
|
145
145
|
]);
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
Byte-identical to calling `signP2PKHInput` per input
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
Byte-identical to calling `signP2PKHInput` per input, for a list of distinct
|
|
149
|
+
indices — a repeated `idx` is rejected with `invalid-argument` rather than
|
|
150
|
+
letting the later entry overwrite the earlier one's signature. On the hashing
|
|
151
|
+
alone this is 12–26x for 50–1000 inputs; end to end the win is smaller (1.7x at
|
|
152
|
+
250 inputs) because ECDSA dominates. `calcSignatureHash` also takes an optional
|
|
151
153
|
`cachedPrefix` from `sigHashPrefixAll(tx)` if you are building signature scripts
|
|
152
154
|
yourself.
|
|
153
155
|
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,9 @@ function err(code, who, message) {
|
|
|
31
31
|
|
|
32
32
|
// src/bytes.ts
|
|
33
33
|
function copyOf(src, off, n) {
|
|
34
|
+
if (!isBytes(src)) {
|
|
35
|
+
throw err("invalid-argument", "copyOf", `source must be a Uint8Array, got ${typeName(src)}`);
|
|
36
|
+
}
|
|
34
37
|
if (!Number.isInteger(off) || !Number.isInteger(n) || off < 0 || n < 0) {
|
|
35
38
|
throw err(
|
|
36
39
|
"not-an-integer",
|
|
@@ -118,6 +121,7 @@ var Writer = class {
|
|
|
118
121
|
// steps. Every input amount and output value in a transaction crosses one of
|
|
119
122
|
// these, and the loop version measured ~33x slower on the primitive.
|
|
120
123
|
u64(v) {
|
|
124
|
+
if (typeof v !== "bigint") throw err("invalid-argument", "Writer.u64", `value must be a bigint, got ${typeName(v)}`);
|
|
121
125
|
if (v < 0n || v > 0xffffffffffffffffn) throw err("out-of-range", "Writer.u64", "value must fit in an unsigned 64-bit integer");
|
|
122
126
|
this.ensure(8);
|
|
123
127
|
this.view.setBigUint64(this.len, v, true);
|
|
@@ -126,6 +130,7 @@ var Writer = class {
|
|
|
126
130
|
}
|
|
127
131
|
/** Signed 64-bit little-endian (two's complement). Used for atom amounts. */
|
|
128
132
|
i64(v) {
|
|
133
|
+
if (typeof v !== "bigint") throw err("invalid-argument", "Writer.i64", `value must be a bigint, got ${typeName(v)}`);
|
|
129
134
|
if (v < -(1n << 63n) || v >= 1n << 63n) throw err("out-of-range", "Writer.i64", "value must fit in a signed 64-bit integer");
|
|
130
135
|
this.ensure(8);
|
|
131
136
|
this.view.setBigInt64(this.len, v, true);
|
|
@@ -133,6 +138,7 @@ var Writer = class {
|
|
|
133
138
|
return this;
|
|
134
139
|
}
|
|
135
140
|
bytes(b) {
|
|
141
|
+
if (!isBytes(b)) throw err("invalid-argument", "Writer.bytes", `value must be a Uint8Array, got ${typeName(b)}`);
|
|
136
142
|
this.ensure(b.length);
|
|
137
143
|
this.buf.set(b, this.len);
|
|
138
144
|
this.len += b.length;
|
|
@@ -775,6 +781,9 @@ for (let i = 0; i < ALPHABET.length; i++) {
|
|
|
775
781
|
INDEX[ALPHABET.charCodeAt(i)] = i;
|
|
776
782
|
}
|
|
777
783
|
function base58Encode(bytes) {
|
|
784
|
+
if (!isBytes(bytes)) {
|
|
785
|
+
throw err("invalid-argument", "base58Encode", `input must be a Uint8Array, got ${typeName(bytes)}`);
|
|
786
|
+
}
|
|
778
787
|
let zeros = 0;
|
|
779
788
|
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
|
|
780
789
|
let num = 0n;
|
|
@@ -910,6 +919,7 @@ function publicKeyFromPrivate(privateKey, compressed = true) {
|
|
|
910
919
|
return secp256k1.secp256k1.getPublicKey(privateKey, compressed);
|
|
911
920
|
}
|
|
912
921
|
function isValidPublicKey(key) {
|
|
922
|
+
if (!isBytes(key)) return false;
|
|
913
923
|
try {
|
|
914
924
|
secp256k1.secp256k1.ProjectivePoint.fromHex(key);
|
|
915
925
|
return true;
|
|
@@ -1204,6 +1214,32 @@ for (const network of Object.values(networks)) {
|
|
|
1204
1214
|
{ network, kind: "pubkey-ecdsa", prefix: network.pubKeyAddrId }
|
|
1205
1215
|
);
|
|
1206
1216
|
}
|
|
1217
|
+
function eq2(a, b) {
|
|
1218
|
+
return a[0] === b[0] && a[1] === b[1];
|
|
1219
|
+
}
|
|
1220
|
+
var ADDRESS_PREFIX_FIELDS = [
|
|
1221
|
+
"pubKeyAddrId",
|
|
1222
|
+
"pubKeyHashAddrId",
|
|
1223
|
+
"pubKeyHashEdwardsAddrId",
|
|
1224
|
+
"pubKeyHashSchnorrAddrId",
|
|
1225
|
+
"scriptHashAddrId"
|
|
1226
|
+
];
|
|
1227
|
+
function assertNetwork(network, who) {
|
|
1228
|
+
const shaped = typeof network === "object" && network !== null && typeof network.name === "string" && ADDRESS_PREFIX_FIELDS.every((f) => {
|
|
1229
|
+
const p = network[f];
|
|
1230
|
+
return Array.isArray(p) && p.length === 2 && typeof p[0] === "number" && typeof p[1] === "number";
|
|
1231
|
+
});
|
|
1232
|
+
if (!shaped) {
|
|
1233
|
+
throw err(
|
|
1234
|
+
"invalid-argument",
|
|
1235
|
+
who,
|
|
1236
|
+
`network must be a Network with its address prefixes, got ${typeName(network)} (pass mainnet, testnet3, simnet or regnet)`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function sameNetwork(a, b) {
|
|
1241
|
+
return a === b || eq2(a.pubKeyAddrId, b.pubKeyAddrId) && eq2(a.pubKeyHashAddrId, b.pubKeyHashAddrId) && eq2(a.pubKeyHashEdwardsAddrId, b.pubKeyHashEdwardsAddrId) && eq2(a.pubKeyHashSchnorrAddrId, b.pubKeyHashSchnorrAddrId) && eq2(a.scriptHashAddrId, b.scriptHashAddrId);
|
|
1242
|
+
}
|
|
1207
1243
|
function encode(prefix, payload) {
|
|
1208
1244
|
const data = new Uint8Array(2 + payload.length);
|
|
1209
1245
|
data[0] = prefix[0];
|
|
@@ -1211,19 +1247,28 @@ function encode(prefix, payload) {
|
|
|
1211
1247
|
data.set(payload, 2);
|
|
1212
1248
|
return checkEncode(data);
|
|
1213
1249
|
}
|
|
1250
|
+
function assertHash20(hash, who) {
|
|
1251
|
+
if (!isBytes(hash)) {
|
|
1252
|
+
throw err("invalid-argument", who, `hash must be a Uint8Array, got ${typeName(hash)}`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1214
1255
|
function pubKeyHashAddress(hash, network) {
|
|
1256
|
+
assertHash20(hash, "pubKeyHashAddress");
|
|
1215
1257
|
if (hash.length !== 20) throw err("bad-length", "pubKeyHashAddress", `hash must be 20 bytes, got ${hash.length}`);
|
|
1216
1258
|
return encode(network.pubKeyHashAddrId, hash);
|
|
1217
1259
|
}
|
|
1218
1260
|
function scriptHashAddress(hash, network) {
|
|
1261
|
+
assertHash20(hash, "scriptHashAddress");
|
|
1219
1262
|
if (hash.length !== 20) throw err("bad-length", "scriptHashAddress", `hash must be 20 bytes, got ${hash.length}`);
|
|
1220
1263
|
return encode(network.scriptHashAddrId, hash);
|
|
1221
1264
|
}
|
|
1222
1265
|
function pubKeyHashEd25519Address(hash, network) {
|
|
1266
|
+
assertHash20(hash, "pubKeyHashEd25519Address");
|
|
1223
1267
|
if (hash.length !== 20) throw err("bad-length", "pubKeyHashEd25519Address", `hash must be 20 bytes, got ${hash.length}`);
|
|
1224
1268
|
return encode(network.pubKeyHashEdwardsAddrId, hash);
|
|
1225
1269
|
}
|
|
1226
1270
|
function pubKeyHashSchnorrAddress(hash, network) {
|
|
1271
|
+
assertHash20(hash, "pubKeyHashSchnorrAddress");
|
|
1227
1272
|
if (hash.length !== 20) throw err("bad-length", "pubKeyHashSchnorrAddress", `hash must be 20 bytes, got ${hash.length}`);
|
|
1228
1273
|
return encode(network.pubKeyHashSchnorrAddrId, hash);
|
|
1229
1274
|
}
|
|
@@ -1293,6 +1338,7 @@ function decodeAddress(address, network) {
|
|
|
1293
1338
|
if (typeof address !== "string") {
|
|
1294
1339
|
throw err("invalid-argument", "decodeAddress", `address must be a string, got ${typeName(address)}`);
|
|
1295
1340
|
}
|
|
1341
|
+
if (network !== void 0) assertNetwork(network, "decodeAddress");
|
|
1296
1342
|
if (address.length > MAX_ADDRESS_LENGTH) {
|
|
1297
1343
|
throw err(
|
|
1298
1344
|
"input-too-long",
|
|
@@ -1304,21 +1350,18 @@ function decodeAddress(address, network) {
|
|
|
1304
1350
|
if (data.length < 3) throw err("bad-length", "decodeAddress", `payload is ${data.length} bytes, too short to hold a prefix and data`);
|
|
1305
1351
|
const prefix = [data[0], data[1]];
|
|
1306
1352
|
const payload = data.subarray(2);
|
|
1307
|
-
const match = PREFIXES.find(
|
|
1308
|
-
(e) => e.prefix[0] === prefix[0] && e.prefix[1] === prefix[1] && (!network || e.network === network)
|
|
1309
|
-
);
|
|
1353
|
+
const match = PREFIXES.find((e) => e.prefix[0] === prefix[0] && e.prefix[1] === prefix[1]);
|
|
1310
1354
|
if (!match) {
|
|
1311
1355
|
const hex = `0x${prefix[0].toString(16).padStart(2, "0")}${prefix[1].toString(16).padStart(2, "0")}`;
|
|
1312
|
-
const onAnotherNetwork = network && PREFIXES.find((e) => e.prefix[0] === prefix[0] && e.prefix[1] === prefix[1]);
|
|
1313
|
-
if (onAnotherNetwork) {
|
|
1314
|
-
throw err(
|
|
1315
|
-
"wrong-network",
|
|
1316
|
-
"decodeAddress",
|
|
1317
|
-
`address is a ${onAnotherNetwork.kind} address for ${onAnotherNetwork.network.name}, not ${network.name}`
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1320
1356
|
throw err("unknown-prefix", "decodeAddress", `unknown address prefix ${hex}`);
|
|
1321
1357
|
}
|
|
1358
|
+
if (network && !sameNetwork(match.network, network)) {
|
|
1359
|
+
throw err(
|
|
1360
|
+
"wrong-network",
|
|
1361
|
+
"decodeAddress",
|
|
1362
|
+
`address is a ${match.kind} address for ${match.network.name}, not ${network.name}`
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1322
1365
|
if (match.kind === "pubkey-ecdsa") {
|
|
1323
1366
|
if (payload.length !== 33) throw err("bad-length", "decodeAddress", `pay-to-pubkey payload must be 33 bytes, got ${payload.length}`);
|
|
1324
1367
|
const { kind, pubKey } = decodePubKeyData(payload);
|
|
@@ -1341,13 +1384,7 @@ function isValidAddress(address, network) {
|
|
|
1341
1384
|
}
|
|
1342
1385
|
}
|
|
1343
1386
|
function addressToScript(address, network) {
|
|
1344
|
-
|
|
1345
|
-
throw err(
|
|
1346
|
-
"invalid-argument",
|
|
1347
|
-
"addressToScript",
|
|
1348
|
-
"a network is required (pass mainnet, testnet3, simnet or regnet)"
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1387
|
+
assertNetwork(network, "addressToScript");
|
|
1351
1388
|
const d = decodeAddress(address, network);
|
|
1352
1389
|
switch (d.kind) {
|
|
1353
1390
|
case "pubkeyhash-ecdsa":
|
|
@@ -1390,7 +1427,7 @@ function encodeWif(privateKey, network, signatureType = 0 /* Ecdsa */) {
|
|
|
1390
1427
|
if (!isBytes(privateKey)) throw err("invalid-argument", "encodeWif", "private key must be a Uint8Array");
|
|
1391
1428
|
if (privateKey.length !== 32) throw err("bad-length", "encodeWif", `private key must be 32 bytes, got ${privateKey.length}`);
|
|
1392
1429
|
if (!Number.isInteger(signatureType) || SignatureType[signatureType] === void 0) {
|
|
1393
|
-
throw err("unsupported-signature-type", "encodeWif", `unknown signature type ${signatureType}`);
|
|
1430
|
+
throw err("unsupported-signature-type", "encodeWif", `unknown signature type ${shown(signatureType)}`);
|
|
1394
1431
|
}
|
|
1395
1432
|
assertWifScalar(privateKey, signatureType, "encodeWif");
|
|
1396
1433
|
const payload = new Uint8Array(3 + 32);
|
|
@@ -1490,6 +1527,9 @@ var ExtendedKey = class _ExtendedKey {
|
|
|
1490
1527
|
cachedPoint = void 0;
|
|
1491
1528
|
/** Derive a master key from a BIP32 seed (16–64 bytes). */
|
|
1492
1529
|
static fromSeed(seed, network) {
|
|
1530
|
+
if (!isBytes(seed)) {
|
|
1531
|
+
throw err("invalid-argument", "ExtendedKey.fromSeed", `seed must be a Uint8Array, got ${typeName(seed)}`);
|
|
1532
|
+
}
|
|
1493
1533
|
if (seed.length < 16 || seed.length > 64) {
|
|
1494
1534
|
throw err("out-of-range", "ExtendedKey.fromSeed", `seed must be 16..64 bytes, got ${seed.length}`);
|
|
1495
1535
|
}
|
|
@@ -1650,6 +1690,9 @@ var ExtendedKey = class _ExtendedKey {
|
|
|
1650
1690
|
return this.derivePathInner(path, true);
|
|
1651
1691
|
}
|
|
1652
1692
|
derivePathInner(path, strictBip32) {
|
|
1693
|
+
if (typeof path !== "string") {
|
|
1694
|
+
throw err("invalid-path", "ExtendedKey.derivePath", `path must be a string, got ${typeName(path)}`);
|
|
1695
|
+
}
|
|
1653
1696
|
const parts = path.trim().split("/");
|
|
1654
1697
|
if (parts[0] === "m" || parts[0] === "M") parts.shift();
|
|
1655
1698
|
let key = this;
|
|
@@ -1738,6 +1781,9 @@ var ExtendedKey = class _ExtendedKey {
|
|
|
1738
1781
|
* key it just parsed. See {@link copyOf}.
|
|
1739
1782
|
*/
|
|
1740
1783
|
static fromSerialized(data) {
|
|
1784
|
+
if (!isBytes(data)) {
|
|
1785
|
+
throw err("invalid-argument", "ExtendedKey.fromSerialized", `serialization must be a Uint8Array, got ${typeName(data)}`);
|
|
1786
|
+
}
|
|
1741
1787
|
if (data.length !== SERIALIZED_LENGTH) throw err("bad-length", "ExtendedKey.fromSerialized", `expected ${SERIALIZED_LENGTH} bytes, got ${data.length}`);
|
|
1742
1788
|
const version = [data[0], data[1], data[2], data[3]];
|
|
1743
1789
|
const depth = data[4];
|
|
@@ -1798,7 +1844,15 @@ function assertMnemonicString(mnemonic, who) {
|
|
|
1798
1844
|
throw err("invalid-argument", who, `mnemonic must be a string, got ${typeof mnemonic}`);
|
|
1799
1845
|
}
|
|
1800
1846
|
}
|
|
1847
|
+
function assertPassphrase(passphrase, who) {
|
|
1848
|
+
if (typeof passphrase !== "string") {
|
|
1849
|
+
throw err("invalid-argument", who, `passphrase must be a string, got ${typeName(passphrase)}`);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1801
1852
|
function assertWordlist(wordlist, who) {
|
|
1853
|
+
if (!Array.isArray(wordlist)) {
|
|
1854
|
+
throw err("invalid-argument", who, `wordlist must be an array of 2048 words, got ${typeName(wordlist)}`);
|
|
1855
|
+
}
|
|
1802
1856
|
if (wordlist.length !== 2048) {
|
|
1803
1857
|
throw err("invalid-argument", who, `wordlist must hold 2048 words, got ${wordlist.length}`);
|
|
1804
1858
|
}
|
|
@@ -1815,6 +1869,8 @@ function generateMnemonic(strength = 128, wordlist = english.wordlist) {
|
|
|
1815
1869
|
return bip39.generateMnemonic(wordlist, strength);
|
|
1816
1870
|
}
|
|
1817
1871
|
function validateMnemonic(mnemonic, wordlist = english.wordlist) {
|
|
1872
|
+
assertWordlist(wordlist, "validateMnemonic");
|
|
1873
|
+
if (typeof mnemonic !== "string") return false;
|
|
1818
1874
|
return bip39.validateMnemonic(mnemonic, wordlist);
|
|
1819
1875
|
}
|
|
1820
1876
|
function mnemonicToEntropy(mnemonic, wordlist = english.wordlist) {
|
|
@@ -1843,6 +1899,7 @@ function entropyToMnemonic(entropy, wordlist = english.wordlist) {
|
|
|
1843
1899
|
}
|
|
1844
1900
|
function mnemonicToSeed(mnemonic, passphrase = "") {
|
|
1845
1901
|
assertMnemonicString(mnemonic, "mnemonicToSeed");
|
|
1902
|
+
assertPassphrase(passphrase, "mnemonicToSeed");
|
|
1846
1903
|
const words = mnemonic.normalize("NFKD").split(" ").length;
|
|
1847
1904
|
if (!MNEMONIC_WORD_COUNTS.includes(words)) {
|
|
1848
1905
|
throw err(
|
|
@@ -1855,6 +1912,7 @@ function mnemonicToSeed(mnemonic, passphrase = "") {
|
|
|
1855
1912
|
}
|
|
1856
1913
|
function mnemonicToMasterKey(mnemonic, network, passphrase = "", wordlist = english.wordlist) {
|
|
1857
1914
|
assertMnemonicString(mnemonic, "mnemonicToMasterKey");
|
|
1915
|
+
assertPassphrase(passphrase, "mnemonicToMasterKey");
|
|
1858
1916
|
assertWordlist(wordlist, "mnemonicToMasterKey");
|
|
1859
1917
|
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
1860
1918
|
throw err(
|
|
@@ -1954,6 +2012,20 @@ var Transaction = class _Transaction {
|
|
|
1954
2012
|
`outpoint tree must be ${0 /* Regular */} (regular) or ${1 /* Stake */} (stake), got ${shown(previousOutPoint.tree)}`
|
|
1955
2013
|
);
|
|
1956
2014
|
}
|
|
2015
|
+
if (opts.valueIn != null && typeof opts.valueIn !== "bigint") {
|
|
2016
|
+
throw err(
|
|
2017
|
+
"invalid-argument",
|
|
2018
|
+
"tx.addInput",
|
|
2019
|
+
`valueIn must be a bigint, got ${typeName(opts.valueIn)}`
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
if (opts.signatureScript && !isBytes(opts.signatureScript)) {
|
|
2023
|
+
throw err(
|
|
2024
|
+
"invalid-argument",
|
|
2025
|
+
"tx.addInput",
|
|
2026
|
+
`signatureScript must be a Uint8Array, got ${typeName(opts.signatureScript)}`
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
1957
2029
|
this.inputs.push({
|
|
1958
2030
|
previousOutPoint: {
|
|
1959
2031
|
hash: copyOf(previousOutPoint.hash, 0, 32),
|
|
@@ -1970,6 +2042,13 @@ var Transaction = class _Transaction {
|
|
|
1970
2042
|
}
|
|
1971
2043
|
/** Add an output. The script is copied; see {@link addInput}. */
|
|
1972
2044
|
addOutput(value, pkScript, version = 0) {
|
|
2045
|
+
if (typeof value !== "bigint") {
|
|
2046
|
+
throw err(
|
|
2047
|
+
"invalid-argument",
|
|
2048
|
+
"tx.addOutput",
|
|
2049
|
+
`value must be a bigint, got ${typeName(value)}`
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
1973
2052
|
if (!isBytes(pkScript)) {
|
|
1974
2053
|
throw err(
|
|
1975
2054
|
"invalid-argument",
|
|
@@ -2180,6 +2259,9 @@ function calcSignatureHash(subScript, hashType, tx, idx, cachedPrefix) {
|
|
|
2180
2259
|
const prefixIsInputIndependent = masked === 1 /* All */ && !anyoneCanPay;
|
|
2181
2260
|
let prefixHash;
|
|
2182
2261
|
if (cachedPrefix !== void 0 && prefixIsInputIndependent) {
|
|
2262
|
+
if (!isBytes(cachedPrefix)) {
|
|
2263
|
+
throw err("invalid-argument", "sighash", `cachedPrefix must be a Uint8Array, got ${typeName(cachedPrefix)}`);
|
|
2264
|
+
}
|
|
2183
2265
|
if (cachedPrefix.length !== 32) {
|
|
2184
2266
|
throw err("bad-length", "sighash", `cachedPrefix must be 32 bytes, got ${cachedPrefix.length}`);
|
|
2185
2267
|
}
|
|
@@ -2241,6 +2323,9 @@ function signHash(hash, privateKey) {
|
|
|
2241
2323
|
}
|
|
2242
2324
|
function verifyHash(hash, derSignature, publicKey) {
|
|
2243
2325
|
assertHash32(hash, "verifyHash");
|
|
2326
|
+
if (!isBytes(publicKey)) {
|
|
2327
|
+
throw err("invalid-argument", "verifyHash", `public key must be a Uint8Array, got ${typeName(publicKey)}`);
|
|
2328
|
+
}
|
|
2244
2329
|
try {
|
|
2245
2330
|
const sig = secp256k1.secp256k1.Signature.fromDER(derSignature);
|
|
2246
2331
|
const canonical = sig.toDERRawBytes();
|
|
@@ -2286,6 +2371,17 @@ function signP2PKHInput(tx, idx, subScript, privateKey, hashType = 1 /* All */,
|
|
|
2286
2371
|
}
|
|
2287
2372
|
function signP2PKHInputs(tx, toSign, hashType = 1 /* All */) {
|
|
2288
2373
|
assertSignableSigHashType(hashType);
|
|
2374
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2375
|
+
for (const { idx } of toSign) {
|
|
2376
|
+
if (seen.has(idx)) {
|
|
2377
|
+
throw err(
|
|
2378
|
+
"invalid-argument",
|
|
2379
|
+
"signP2PKHInputs",
|
|
2380
|
+
`input index ${idx} is listed more than once`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
seen.add(idx);
|
|
2384
|
+
}
|
|
2289
2385
|
const cachedPrefix = sigHashPrefixAll(tx);
|
|
2290
2386
|
for (const { idx, subScript, privateKey, compressed = true } of toSign) {
|
|
2291
2387
|
tx.inputs[idx].signatureScript = signatureScript(
|
|
@@ -2305,6 +2401,9 @@ function signP2PKHInputs(tx, toSign, hashType = 1 /* All */) {
|
|
|
2305
2401
|
var ATOMS_PER_COIN = 100000000n;
|
|
2306
2402
|
var COIN_DECIMALS = 8;
|
|
2307
2403
|
function dcrToAtoms(dcr) {
|
|
2404
|
+
if (typeof dcr !== "string") {
|
|
2405
|
+
throw err("invalid-amount", "dcrToAtoms", `amount must be a string, got ${typeName(dcr)}`);
|
|
2406
|
+
}
|
|
2308
2407
|
const s = dcr.trim();
|
|
2309
2408
|
if (!/^-?\d+(\.\d+)?$/.test(s)) throw err("invalid-amount", "dcrToAtoms", `cannot parse ${shown(dcr)}`);
|
|
2310
2409
|
const negative = s.startsWith("-");
|
|
@@ -2318,6 +2417,9 @@ function dcrToAtoms(dcr) {
|
|
|
2318
2417
|
return negative ? -atoms : atoms;
|
|
2319
2418
|
}
|
|
2320
2419
|
function atomsToDcr(atoms) {
|
|
2420
|
+
if (typeof atoms !== "bigint") {
|
|
2421
|
+
throw err("invalid-amount", "atomsToDcr", `atoms must be a bigint, got ${typeName(atoms)}`);
|
|
2422
|
+
}
|
|
2321
2423
|
const negative = atoms < 0n;
|
|
2322
2424
|
const a = negative ? -atoms : atoms;
|
|
2323
2425
|
const intPart = a / ATOMS_PER_COIN;
|