dcr-ts 0.2.0 → 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 CHANGED
@@ -4,6 +4,224 @@ 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
+
187
+ ## 0.2.1 — 2026-08-25
188
+
189
+ **No changes to the library.** `src/` is byte-identical to 0.2.0 and the built
190
+ bundle is unchanged; the package version is not embedded in it. Everything here
191
+ is release infrastructure, plus the one thing that could only be tested by
192
+ performing a release.
193
+
194
+ ### Changed — releasing
195
+
196
+ - **Publishing moved to CI, authorised by a signed tag.** The workflow verifies
197
+ the tag against an allowed-signers list before it does anything else, pins the
198
+ checkout to the commit the tag points at, refuses a tag that disagrees with
199
+ `package.json`, and re-runs the full gates — typecheck, tests, both-format
200
+ entry-point resolution, a production-dependency audit, and a dcrd vector
201
+ regeneration — before it can publish.
202
+ - **Authentication is OIDC, not a stored token.** Trusted publishing means no
203
+ long-lived credential exists in the repository at all, and npm generates the
204
+ provenance attestation automatically. 0.2.0 was published by hand before the
205
+ package existed on the registry, so it carries no attestation; this is the
206
+ first release that does.
207
+ - **The signer policy is read from the default branch, not from the tag.**
208
+ Reading it from the tagged tree would have let a tag supply the policy
209
+ authorising it — a tag carrying its own allowed-signers list, signed with the
210
+ key it names, would have verified itself.
211
+ - **Canonical `repository.url`** (`git+https://…​.git`), matching what
212
+ `hosted-git-info` normalises to. This was previously described as a provenance
213
+ fix; it is not. `libnpmpublish` never reads the field when generating an
214
+ attestation.
215
+ - **CI and npm badges in the README**, which is also the package page on npm.
216
+
217
+ ### Why this version exists
218
+
219
+ The publish step had never executed. The republish guard correctly stops any
220
+ re-run against a version already on the registry, so no amount of re-running
221
+ 0.2.0 could exercise the OIDC exchange — only a version that does not yet exist
222
+ can. Releasing 0.2.1 is how the path gets proven, and the cost is a version
223
+ number rather than an untested release pipeline discovered at the worst moment.
224
+
7
225
  ## 0.2.0 — 2026-08-25
8
226
 
9
227
  ### Fixed — consensus
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # dcr-ts
2
2
 
3
+ [![CI](https://github.com/jzbz/dcr-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/jzbz/dcr-ts/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/dcr-ts)](https://www.npmjs.com/package/dcr-ts)
5
+
3
6
  Decred (DCR) primitives for TypeScript: **BLAKE-256**, addresses, WIF, **BIP32
4
7
  HD keys** with Decred serialization, BIP39 mnemonics, the **transaction wire
5
8
  format**, the **Decred signature hash**, and **low-S ECDSA** P2PKH signing.
@@ -142,9 +145,11 @@ signP2PKHInputs(tx, [
142
145
  ]);
143
146
  ```
144
147
 
145
- Byte-identical to calling `signP2PKHInput` per input. On the hashing alone this
146
- is 12–26x for 50–1000 inputs; end to end the win is smaller (1.7x at 250 inputs)
147
- because ECDSA dominates. `calcSignatureHash` also takes an optional
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
148
153
  `cachedPrefix` from `sigHashPrefixAll(tx)` if you are building signature scripts
149
154
  yourself.
150
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
- if (!network || typeof network.name !== "string") {
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;