--- /dev/null
+# seed-phrase-transposer
+
+This is a simple [Deno](https://deno.land/) CLI to print possible permutations
+of a seed phrase to help test if a transposition error occurred while the seed
+phrase was recorded.
+
+## Background
+
+12-word and 24-word
+[BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) seed
+phrases are ubiquitous in crypto. However, due to the length of the seed phrase,
+they're often presented to users in a grid. Sometimes bad wallet UIs do not
+number the words,
+[making the order of the words ambiguous](https://knowyourmeme.com/memes/dont-dead-open-inside)
+and thus introducing the risk of a user recording the words in the wrong order
+(a transposition error).
+
+This tool is intended to show possible permutations of a seed phrase when read
+from ambiguous grid layouts to assist with testing and potential recovery.
+
+## Example
+
+Consider this 3x4 grid layout:
+
+```
+1 2 3
+4 5 6
+7 8 9
+10 11 12
+```
+
+The grid is intended to be read left-to-right, top-to-bottom. But a user may
+incorrectly record these numbers top-to-bottom, left-to-right. Like this:
+
+```
+1 4 7 10 2 5 8 11 3 6 9 12
+```
+
+This CLI will output possible permutations of the input, enabling a user to
+quickly discover the correct one:
+
+```
+> deno run main.ts
+Seed phrase: 1 4 7 10 2 5 8 11 3 6 9 12
+
+1 8 4 11 7 3 10 6 2 9 5 12
+1 7 2 8 3 9 4 10 5 11 6 12
+1 2 3 4 5 6 7 8 9 10 11 12 <-- Correct permutation found!
+1 10 8 6 4 2 11 9 7 5 3 12
+```
--- /dev/null
+export function parseInput(input: string | null) {
+ if (input === null) {
+ throw new Error("No seed phrase entered.");
+ }
+ const words = input.split(/\s/g).map((w) => w.trim());
+ if (words.length % 2 === 1) {
+ throw new Error("Seed phrase length must be even.");
+ }
+ return words;
+}
+
+export function getPermutations(words: Array<string>, columns: number) {
+ if (words.length % columns !== 0) {
+ throw new Error(
+ "Seed phrase length must be divisible by number of columns.",
+ );
+ }
+
+ const rows = words.length / columns;
+
+ // Assuming only permutations are horizontal-tb and vertical-lr
+ // Note that a 3x4 grid read sideways is the same as a 4x3 grid, so only need to check lower factors
+ const verticalLR = [];
+ const horizontalTB = [];
+ for (const [index, word] of words.entries()) {
+ const verticalIndex = (index % rows) * columns + Math.floor(index / rows);
+ verticalLR[verticalIndex] = word;
+
+ const horizontalIndex = (index % columns) * rows +
+ Math.floor(index / columns);
+ horizontalTB[horizontalIndex] = word;
+ }
+
+ return [verticalLR.join(" "), horizontalTB.join(" ")];
+}
+
+// Gets all factors that are >= 2 and not a multiple of another factor
+export function getLowerFactors(num: number) {
+ const lowerFactors = [];
+ for (let i = 2; i < num / (lowerFactors.at(-1) ?? 1); i++) {
+ if (num % i === 0) {
+ lowerFactors.push(i);
+ }
+ }
+ return lowerFactors;
+}
+
+if (import.meta.main) {
+ const input = prompt("Seed phrase:");
+ const words = parseInput(input);
+ const factors = getLowerFactors(words.length);
+ const permutations = factors.map((columns) => getPermutations(words, columns))
+ .flat();
+ console.log("\n");
+ permutations.forEach((permutation) => console.log(permutation));
+}
--- /dev/null
+import { getLowerFactors, getPermutations, parseInput } from "./main.ts";
+import {
+ assertArrayIncludes,
+ assertEquals,
+ assertFalse,
+ assertThrows,
+} from "https://deno.land/std@0.201.0/assert/mod.ts";
+
+Deno.test("parseInput throws on null", () => {
+ assertThrows(() => parseInput(null));
+});
+
+Deno.test("parseInput throws on invalid length", () => {
+ assertThrows(() => parseInput("test test test"));
+});
+
+Deno.test("parseInput parses 12 word seed phrase", () => {
+ const words = parseInput(
+ "test test test test test test test test test test test junk",
+ );
+ assertEquals(words.length, 12);
+});
+
+Deno.test("parseInput parses 24 word seed phrase", () => {
+ const words = parseInput(
+ "test test test test test test test test test test test junk test test test test test test test test test test test junk",
+ );
+ assertEquals(words.length, 24);
+});
+
+Deno.test("getPermutations throws on invalid number of columns", () => {
+ assertThrows(() =>
+ getPermutations(
+ "test test test test test test test test test test test junk".split(" "),
+ 5,
+ )
+ );
+});
+
+/*
+Example: "3-column vertical-lr incorrectly recorded as horizontal-tb"
+
+Grid:
+1 2 3
+4 5 6
+7 8 9
+10 11 12
+
+User enters:
+1 4 7 10 2 5 8 11 3 6 9 12
+
+getPermutations should return:
+[ "1 2 3 4 5 6 7 8 9 10 11 12", "1 10 8 6 4 2 11 9 7 5 3 12" ]
+*/
+const test12WordGrids = [
+ {
+ testName: "2-column vertical-lr incorrectly recorded as horizontal-tb",
+ words: "1 3 5 7 9 11 2 4 6 8 10 12".split(" "),
+ columns: 2,
+ },
+ {
+ testName: "2-column horizontal-tb incorrectly recorded as vertical-lr",
+ words: "1 7 2 8 3 9 4 10 5 11 6 12".split(" "),
+ columns: 2,
+ },
+ {
+ testName: "3-column vertical-lr incorrectly recorded as horizontal-tb",
+ words: "1 4 7 10 2 5 8 11 3 6 9 12".split(" "),
+ columns: 3,
+ },
+ {
+ testName: "3-column horizontal-tb incorrectly recorded as vertical-lr",
+ words: "1 5 9 2 6 10 3 7 11 4 8 12".split(" "),
+ columns: 3,
+ },
+];
+const test24WordGrids = [
+ {
+ testName: "6-column vertical-lr incorrectly recorded as horizontal-tb",
+ words: "1 7 13 19 2 8 14 20 3 9 15 21 4 10 16 22 5 11 17 23 6 12 18 24"
+ .split(" "),
+ columns: 6,
+ },
+ {
+ testName: "6-column horizontal-tb incorrectly recorded as vertical-lr",
+ words: "1 5 9 13 17 21 2 6 10 14 18 22 3 7 11 15 19 23 4 8 12 16 20 24"
+ .split(" "),
+ columns: 6,
+ },
+ {
+ testName: "8-column vertical-lr incorrectly recorded as horizontal-tb",
+ words: "1 9 17 2 10 18 3 11 19 4 12 20 5 13 21 6 14 22 7 15 23 8 16 24"
+ .split(" "),
+ columns: 8,
+ },
+ {
+ testName: "8-column horizontal-tb incorrectly recorded as vertical-lr",
+ words: "1 4 7 10 13 16 19 22 2 5 8 11 14 17 20 23 3 6 9 12 15 18 21 24"
+ .split(" "),
+ columns: 8,
+ },
+ {
+ testName: "12-column vertical-lr incorrectly recorded as horizontal-tb",
+ words: "1 13 2 14 3 15 4 16 5 17 6 18 7 19 8 20 9 21 10 22 11 23 12 24"
+ .split(" "),
+ columns: 12,
+ },
+ {
+ testName: "12-column horizontal-tb incorrectly recorded as vertical-lr",
+ words: "1 3 5 7 9 11 13 15 17 19 21 23 2 4 6 8 10 12 14 16 18 20 22 24"
+ .split(" "),
+ columns: 12,
+ },
+];
+Deno.test("getPermutations returns correct permutations of 12-word test grids", () => {
+ test12WordGrids.forEach((grid) => {
+ const permutations = getPermutations(grid.words, grid.columns);
+ assertArrayIncludes<string>(permutations, ["1 2 3 4 5 6 7 8 9 10 11 12"]);
+ });
+});
+Deno.test("getPermutations returns correct permutations of 24-word test grids", () => {
+ test24WordGrids.forEach((grid) => {
+ const permutations = getPermutations(grid.words, grid.columns);
+ assertArrayIncludes<string>(permutations, [
+ "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24",
+ ]);
+ });
+});
+
+Deno.test("getLowerFactors returns correct factors of 12", () => {
+ const factors = getLowerFactors(12);
+ assertArrayIncludes<number>(factors, [2, 3]);
+ assertFalse(factors.includes(4));
+ assertFalse(factors.includes(6));
+});
+
+Deno.test("getLowerFactors returns correct factors of 24", () => {
+ const factors = getLowerFactors(24);
+ assertArrayIncludes<number>(factors, [2, 3, 4]);
+ assertFalse(factors.includes(6));
+ assertFalse(factors.includes(8));
+ assertFalse(factors.includes(12));
+});