Puzzle records
One file, one literal
import { bitcoinPuzzle, bits, funding, increase, p2pkh } from "@agntn/puzzles";
export const b1000Puzzle71 = bitcoinPuzzle({
id: "b1000/71",
address: p2pkh("1PWo3JeB9jrGwfHDNpdGK54CRas7fsVzXU", "f6f5431d25bbf7b12e8add9af5e3475c44a0a5b8"),
sourceUrl: "https://bitcointalk.org/index.php?topic=5218972",
startedAt: "2015-01-15 18:07:14",
prize: 7.100226,
key: bits(71),
transactions: [
funding(
"08389f34c98c606322740c0be6a7125d9860bb8d5cb182c02f98461e5fa6cd15",
"2015-01-15 18:07:14",
0.071,
),
increase(
"5d45587cfd1d5b0fb826805541da7d94c61fe432259e68ee26f4a04544384164",
"2017-07-11 05:00:53",
0.639,
),
],
});
That's the whole record. No status, because unsolved is the default. No solver, no assets, no null anywhere. The factory picks the chain. p2pkh picks the address kind. bits(71) says the key is somewhere in [2^70, 2^71 - 1] and nothing more. Typo in a field name? The type checker rejects it before any test runs.
Six factories
bitcoinPuzzle, ethereumPuzzle, litecoinPuzzle, decredPuzzle, arweavePuzzle and moneroPuzzle take the same PuzzleSpec and answer chain() for it. Monero has a factory and no puzzles yet. Behind each one sits an abstract base, BitcoinPuzzle and so on. A puzzle that needs behavior of its own can extend a base directly. None of the 332 records do.
| Field | Required | Built with |
|---|---|---|
id, sourceUrl, startedAt | yes | strings, YYYY-MM-DD HH:MM:SS in UTC |
address | yes | p2pkh, p2sh, p2wpkh or standard, each with the optional HASH160 |
status | no, unsolved | Status.Solved, Status.Claimed, Status.Swept, Status.Expired |
prize, currency | no | a number in the chain's native token, or currency: "DAI" when it isn't |
pubkey | no | compressed("02…") or uncompressed("04…") |
key | no | the Key builder below |
transactions | no | funding, increase, decrease, pubkeyReveal, claim, sweep, each (txid, date, amount) |
solvedAt, solveTime, solver | no | when, seconds, and party(name, { addresses, profiles }) |
assets | no | assets({ puzzle, solver, hints, sourceUrl }), file names under assets/<collection>/ |
preGenesis | no | the address predates the puzzle thread, so the funding date is the thread's |
Keys are a chain of calls
Key material comes in more shapes than any other field. So it's a builder, not a literal:
import { bits, derivation, encryptedWif, hex, seed, share } from "@agntn/puzzles";
hex("00…01", 1).wif("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn"); // b1000/1
hex("af9a…4a0d").wif("5K9d…U7g").passphrase("5784623964023 578462396402"); // a brainwallet
encryptedWif("6PnW…4cH", { passphrase: "335Y-K745-C8WT-4D2W-80WP" }); // a BIP38 Ballet wallet
derivation("m/84'/0'/0'/0/0")
.xpub("zpub6qd…JCqL")
.shares(3, 5, [share(1, "session cigar …")]); // Bitaps
derivation("m/84'/0'/0'/0/0").entropy(
"1808…32ed",
source("https://twitter.com/aantonop/status/603701870482300928"),
); // Bitimage
seed("abandon abandon … about", "m/44'/0'/0'/0/0"); // a full BIP39 record, none in the dataset yet
bits(71); // a search width and nothing else
key() hands back the builder. keyData() gives its serialized record. secretOf(keyData) picks the one representation verification will use, in this order: a hex key, a decrypted WIF, an encrypted WIF, a seed phrase, a mini key. A record with only a derivation path or an xpub has no secret. hasPrivateKey() says false for it, and verification agrees.
Status is explicit
A claim transaction plus a published key still means solved. A sweep by a stranger after the public key leaked means swept. A prize the author took back means expired. Those are judgments, and the record writes them down. The library refuses to guess them from the transaction list. It would guess wrong on exactly the puzzles people care about.
No nulls
toJSON() omits what a puzzle never claimed to have. No solver, no assets, no prize? Then no keys for them either. The data gate fails the build if the string null shows up anywhere in the dataset. Consumers check === undefined once and move on.
{
"id": "b1000/71",
"chain": "bitcoin",
"address": { "value": "1PWo3JeB9jrGwfHDNpdGK54CRas7fsVzXU", "kind": "p2pkh", "hash160": "f6f5…a5b8" },
"status": "unsolved",
"key": { "bits": 71 },
"prize": 7.100226,
"start_date": "2015-01-15 18:07:14",
"source_url": "https://bitcointalk.org/index.php?topic=5218972",
"transactions": [ ... ]
}
The serialized names are snake case, start_date and source_url. That's what the JSON dataset looked like before this was a TypeScript package. Nothing downstream should have to care about the rewrite.
The data gate
pnpm test re-checks every record. Identifiers are unique and owned by their collection. Every address fits its chain's format. A published private key derives the stored address. A decrypted WIF matches its hex. A BIP38 payload decrypts to the declared key when the passphrase is known. A claimed or swept puzzle carries a public key. Every asset path exists. Nothing serializes as null. A wrong hex digit doesn't reach main.
Getting Started
Install the package. Load one puzzle. Read its address and its key range. Every puzzle is a record and every answer comes from a method on it.
Registry
A manifest of ten keys and ten lazy imports. Keys and aliases answer at once. Records load on the first lookup for their collection. How to register yours.