@spirobel/monero-wallet-api 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.
Files changed (68) hide show
  1. package/README.md +3 -3
  2. package/dist/api.d.ts +25 -96
  3. package/dist/api.js +23 -175
  4. package/dist/io/BunFileInterface.d.ts +32 -0
  5. package/dist/io/atomicWrite.d.ts +2 -0
  6. package/dist/io/atomicWrite.js +10 -0
  7. package/dist/io/extension.d.ts +18 -0
  8. package/dist/io/extension.js +11 -0
  9. package/dist/io/indexedDB.d.ts +45 -0
  10. package/dist/io/indexedDB.js +221 -0
  11. package/dist/io/readDir.d.ts +1 -0
  12. package/dist/io/readDir.js +7 -0
  13. package/dist/io/sleep.d.ts +1 -0
  14. package/dist/io/sleep.js +1 -0
  15. package/dist/keypairs-seeds/keypairs.d.ts +29 -0
  16. package/dist/keypairs-seeds/keypairs.js +207 -0
  17. package/dist/keypairs-seeds/writeKeypairs.d.ts +11 -0
  18. package/dist/keypairs-seeds/writeKeypairs.js +75 -0
  19. package/dist/node-interaction/binaryEndpoints.d.ts +59 -14
  20. package/dist/node-interaction/binaryEndpoints.js +110 -54
  21. package/dist/node-interaction/jsonEndpoints.d.ts +249 -187
  22. package/dist/node-interaction/jsonEndpoints.js +287 -0
  23. package/dist/node-interaction/nodeUrl.d.ts +129 -0
  24. package/dist/node-interaction/nodeUrl.js +113 -0
  25. package/dist/scanning-syncing/backgroundWorker.d.ts +6 -0
  26. package/dist/scanning-syncing/backgroundWorker.js +56 -0
  27. package/dist/scanning-syncing/connectionStatus.d.ts +15 -0
  28. package/dist/scanning-syncing/connectionStatus.js +35 -0
  29. package/dist/scanning-syncing/openWallet.d.ts +28 -0
  30. package/dist/scanning-syncing/openWallet.js +57 -0
  31. package/dist/scanning-syncing/scanSettings.d.ts +96 -0
  32. package/dist/scanning-syncing/scanSettings.js +243 -0
  33. package/dist/scanning-syncing/scanresult/computeKeyImage.d.ts +3 -0
  34. package/dist/scanning-syncing/scanresult/computeKeyImage.js +21 -0
  35. package/dist/scanning-syncing/scanresult/getBlocksbinBuffer.d.ts +28 -0
  36. package/dist/scanning-syncing/scanresult/getBlocksbinBuffer.js +52 -0
  37. package/dist/scanning-syncing/scanresult/reorg.d.ts +14 -0
  38. package/dist/scanning-syncing/scanresult/reorg.js +78 -0
  39. package/dist/scanning-syncing/scanresult/scanCache.d.ts +84 -0
  40. package/dist/scanning-syncing/scanresult/scanCache.js +134 -0
  41. package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +149 -0
  42. package/dist/scanning-syncing/scanresult/scanCacheOpened.js +648 -0
  43. package/dist/scanning-syncing/scanresult/scanResult.d.ts +64 -0
  44. package/dist/scanning-syncing/scanresult/scanResult.js +213 -0
  45. package/dist/scanning-syncing/scanresult/scanStats.d.ts +60 -0
  46. package/dist/scanning-syncing/scanresult/scanStats.js +273 -0
  47. package/dist/scanning-syncing/worker-entrypoints/worker.d.ts +1 -0
  48. package/dist/scanning-syncing/worker-entrypoints/worker.js +8 -0
  49. package/dist/scanning-syncing/worker-mains/worker.d.ts +1 -0
  50. package/dist/scanning-syncing/worker-mains/worker.js +7 -0
  51. package/dist/send-functionality/conversion.d.ts +4 -0
  52. package/dist/send-functionality/conversion.js +75 -0
  53. package/dist/send-functionality/inputSelection.d.ts +13 -0
  54. package/dist/send-functionality/inputSelection.js +8 -0
  55. package/dist/send-functionality/transactionBuilding.d.ts +51 -0
  56. package/dist/send-functionality/transactionBuilding.js +111 -0
  57. package/dist/tools/monero-tools.d.ts +46 -0
  58. package/dist/tools/monero-tools.js +165 -0
  59. package/dist/viewpair/ViewPair.d.ts +157 -0
  60. package/dist/viewpair/ViewPair.js +346 -0
  61. package/dist/wasm-processing/wasi.js +1 -2
  62. package/dist/wasm-processing/wasmFile.d.ts +1 -1
  63. package/dist/wasm-processing/wasmFile.js +2 -2
  64. package/dist/wasm-processing/wasmProcessor.d.ts +16 -4
  65. package/dist/wasm-processing/wasmProcessor.js +23 -7
  66. package/package.json +29 -6
  67. package/dist/testscrap.js +0 -36
  68. /package/dist/{testscrap.d.ts → io/BunFileInterface.js} +0 -0
@@ -0,0 +1,221 @@
1
+ import { writeEnvLineToDotEnvRefresh } from "../keypairs-seeds/writeKeypairs";
2
+ class IndexedDBBun {
3
+ stdin = new IndexedDBFile();
4
+ stdout = new IndexedDBFile();
5
+ stderr = new IndexedDBFile();
6
+ file(path, options) {
7
+ return new IndexedDBFile(getFileFromIndexedDB(path.toString()), path.toString());
8
+ }
9
+ async write(destination, input) {
10
+ return await putFileIntoIndexedDB(destination.toString(), input);
11
+ }
12
+ env = {};
13
+ }
14
+ class IndexedDBFile {
15
+ content;
16
+ path;
17
+ size = 0;
18
+ type = "";
19
+ constructor(content, path) {
20
+ this.content = content;
21
+ this.path = path;
22
+ }
23
+ async text() {
24
+ const result = (await this.content);
25
+ if (!result)
26
+ throw new Error(`no such file or directory, open '${this.path}'`);
27
+ return result;
28
+ }
29
+ stream() {
30
+ throw new Error("not implemented");
31
+ return new ReadableStream();
32
+ }
33
+ async arrayBuffer() {
34
+ const result = (await this.content);
35
+ if (!result)
36
+ throw new Error(`no such file or directory, open '${this.path}'`);
37
+ return result;
38
+ }
39
+ json() {
40
+ throw new Error("not implemented");
41
+ return Promise.resolve({});
42
+ }
43
+ writer(params) {
44
+ throw new Error("not implemented");
45
+ return new BunFileSink();
46
+ }
47
+ exists() {
48
+ throw new Error("not implemented");
49
+ return Promise.resolve(false);
50
+ }
51
+ delete() {
52
+ return deleteFileFromIndexedDB(this.path);
53
+ }
54
+ }
55
+ class BunFileSink {
56
+ write(chunk) {
57
+ throw new Error("not implemented");
58
+ return 0;
59
+ }
60
+ flush() {
61
+ throw new Error("not implemented");
62
+ return 0;
63
+ }
64
+ end(error) {
65
+ throw new Error("not implemented");
66
+ return 0;
67
+ }
68
+ start(options) {
69
+ throw new Error("not implemented");
70
+ }
71
+ ref() {
72
+ throw new Error("not implemented");
73
+ }
74
+ unref() {
75
+ throw new Error("not implemented");
76
+ }
77
+ }
78
+ export async function getItemLength(input) {
79
+ if (typeof input === "string") {
80
+ return [input, new TextEncoder().encode(input).length];
81
+ }
82
+ if (input instanceof Blob) {
83
+ return [input, input.size];
84
+ }
85
+ if (ArrayBuffer.isView(input)) {
86
+ return [input.buffer, input.byteLength];
87
+ }
88
+ if ("arrayBuffer" in input) {
89
+ const bytes = await input.arrayBuffer();
90
+ return [bytes, bytes.byteLength];
91
+ }
92
+ // SharedArrayBuffer/ArrayBuffer fallback
93
+ if ("byteLength" in input) {
94
+ return [input, input.byteLength];
95
+ }
96
+ throw new Error(`ENOSPC: unsupported input type`);
97
+ }
98
+ export async function putFileIntoIndexedDB(path, content) {
99
+ if (!browserGlobal.filesDb) {
100
+ throw new Error("IndexedDB not initialized");
101
+ }
102
+ const [dbContent, byteLength] = await getItemLength(content);
103
+ const tx = browserGlobal.filesDb.transaction(fileStoreName, "readwrite");
104
+ const store = tx.objectStore(fileStoreName);
105
+ const request = store.put(dbContent, path);
106
+ return await new Promise((resolve, reject) => {
107
+ request.onsuccess = () => resolve(byteLength);
108
+ request.onerror = () => reject(request.error);
109
+ });
110
+ }
111
+ export function getFileFromIndexedDB(path) {
112
+ if (!browserGlobal.filesDb) {
113
+ throw new Error("IndexedDB not initialized");
114
+ }
115
+ else {
116
+ const tx = browserGlobal.filesDb.transaction(fileStoreName, "readonly");
117
+ const store = tx.objectStore(fileStoreName);
118
+ const request = store.get(path);
119
+ return new Promise((resolve, reject) => {
120
+ request.onsuccess = () => resolve(request.result);
121
+ request.onerror = () => reject(request.error);
122
+ });
123
+ }
124
+ }
125
+ export async function deleteFileFromIndexedDB(path) {
126
+ if (!browserGlobal.filesDb) {
127
+ throw new Error("IndexedDB not initialized");
128
+ }
129
+ const tx = browserGlobal.filesDb.transaction(fileStoreName, "readwrite");
130
+ const store = tx.objectStore(fileStoreName);
131
+ const request = store.delete(path.trim());
132
+ return new Promise((resolve, reject) => {
133
+ request.onsuccess = () => resolve();
134
+ request.onerror = () => reject(request.error);
135
+ });
136
+ }
137
+ export const fileStoreName = "files";
138
+ async function initFilesDB() {
139
+ return new Promise((resolve, reject) => {
140
+ const request = indexedDB.open(fileStoreName);
141
+ request.onerror = () => reject(request.error);
142
+ request.onsuccess = () => resolve(request.result);
143
+ request.onupgradeneeded = () => request.result.createObjectStore(fileStoreName);
144
+ });
145
+ }
146
+ // In browsers: window in main thread, self in workers
147
+ const hasWindow = typeof window !== "undefined";
148
+ const hasSelf = typeof self !== "undefined";
149
+ //@ts-ignore
150
+ const browserGlobal = hasWindow ? window : hasSelf ? self : {}; // non-browser -> no shimming
151
+ if (typeof globalThis.Bun === "undefined") {
152
+ browserGlobal.filesDb = await initFilesDB();
153
+ browserGlobal.Bun = new IndexedDBBun();
154
+ browserGlobal.Bun.env = await readEnvIndexedDB();
155
+ browserGlobal.areWeInTheBrowser = true;
156
+ }
157
+ else {
158
+ browserGlobal.areWeInTheBrowser = false;
159
+ }
160
+ export async function refreshEnvIndexedDB() {
161
+ browserGlobal.Bun.env = await readEnvIndexedDB();
162
+ }
163
+ // we need this to change the env at runtime from inside the Browser extension,
164
+ // or react native app. Or to persist view keys in bun web backend.
165
+ // this one is specifically for indexedDB (convention of treating .env as Bun.env)
166
+ export async function writeEnvIndexedDB(key, value) {
167
+ // this file should be treated as ephemeral
168
+ // private spendkeys + viewkeys are deterministically derived from seedphrase and password
169
+ // we have to go through indexedDB just so the background worker has access to this.
170
+ // (after waking up from an alarm or onmessage event)
171
+ await writeEnvLineToDotEnvRefresh(key, value, ".env");
172
+ }
173
+ export async function readEnvIndexedDB() {
174
+ const file = Bun.file(".env");
175
+ const content = await file
176
+ .text()
177
+ .catch(() => { })
178
+ .then((c) => c || "");
179
+ const lines = content.split("\n");
180
+ const result = {};
181
+ for (const line of lines) {
182
+ const keyValue = line.split("=");
183
+ const key = keyValue[0];
184
+ if (!key)
185
+ continue;
186
+ const value = keyValue[1];
187
+ result[key.trim()] = value?.trim();
188
+ }
189
+ return result;
190
+ }
191
+ /**
192
+ * useless function
193
+ * you can just do process.env[key] instead. Look at the code above.
194
+ * @param key
195
+ * @returns value
196
+ */
197
+ export async function readEnvIndexedDBLine(key) {
198
+ const file = Bun.file(".env");
199
+ const content = await file.text();
200
+ const lines = content.split("\n");
201
+ const idx = lines.findIndex((line) => line.startsWith(key.trim()));
202
+ return lines[idx].split("=")[1].trim();
203
+ }
204
+ export async function readdir(dirpath) {
205
+ if (!browserGlobal.filesDb) {
206
+ throw new Error("IndexedDB not initialized");
207
+ }
208
+ let prefix = dirpath.trim();
209
+ if (prefix && !prefix.endsWith("/")) {
210
+ prefix += "/";
211
+ }
212
+ const tx = browserGlobal.filesDb.transaction(fileStoreName, "readonly");
213
+ const store = tx.objectStore(fileStoreName);
214
+ const range = IDBKeyRange.bound(prefix, prefix + "\uffff", false, true);
215
+ const keys = await new Promise((resolve, reject) => {
216
+ const request = store.getAllKeys(range);
217
+ request.onsuccess = () => resolve(request.result);
218
+ request.onerror = () => reject(request.error);
219
+ });
220
+ return keys.map((key) => key.slice(prefix.length));
221
+ }
@@ -0,0 +1 @@
1
+ export declare function readDir(path: string): Promise<string[]>;
@@ -0,0 +1,7 @@
1
+ import { readdir as indexedDBreaddir } from "./indexedDB";
2
+ export async function readDir(path) {
3
+ if (areWeInTheBrowser)
4
+ return await indexedDBreaddir(path);
5
+ const { readdir } = await import("node:fs/promises");
6
+ return await readdir(path);
7
+ }
@@ -0,0 +1 @@
1
+ export declare const sleep: (ms: number) => Promise<unknown>;
@@ -0,0 +1 @@
1
+ export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -0,0 +1,29 @@
1
+ /**
2
+ * "So when we first decided to create a mnemonic system the spec we
3
+ * came up with was: take the seed from the mnemonic, hash it for the
4
+ * spend key, hash it twice for the view key. Somewhere during the
5
+ * simplewallet implementation we forgot about that, and just used the
6
+ * mnemonic seed as the spendkey directly.
7
+ *
8
+ * This proved to be a blessing in disguise, though, as we'd not realised
9
+ * that people might want to retrieve their seed. Using our original
10
+ * design this wouldn't have been possible, as we didn't store the seed
11
+ * in the wallet file.
12
+ *
13
+ * Much later on when we were creating MyMonero (a different group of
14
+ * developers, I'm the only common link between the two) we decided that
15
+ * a 13 word seed would be much easier for people to remember, but
16
+ * because we wanted it to match simplewallet's implementation we made
17
+ * sure that we followed the spec... as it was originally... before we
18
+ * duffed the implementation."
19
+ */
20
+ export type SpendKey = string;
21
+ export declare function makeSpendKey(): Promise<SpendKey>;
22
+ export declare function makeSpendKeyFromSeed(wallet_secret: string): Promise<SpendKey>;
23
+ export type ViewPairJson = {
24
+ view_key: string;
25
+ mainnet_primary: string;
26
+ stagenet_primary: string;
27
+ testnet_primary: string;
28
+ };
29
+ export declare function makeViewKey(spend_private_key: string): Promise<ViewPairJson>;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * "So when we first decided to create a mnemonic system the spec we
3
+ * came up with was: take the seed from the mnemonic, hash it for the
4
+ * spend key, hash it twice for the view key. Somewhere during the
5
+ * simplewallet implementation we forgot about that, and just used the
6
+ * mnemonic seed as the spendkey directly.
7
+ *
8
+ * This proved to be a blessing in disguise, though, as we'd not realised
9
+ * that people might want to retrieve their seed. Using our original
10
+ * design this wouldn't have been possible, as we didn't store the seed
11
+ * in the wallet file.
12
+ *
13
+ * Much later on when we were creating MyMonero (a different group of
14
+ * developers, I'm the only common link between the two) we decided that
15
+ * a 13 word seed would be much easier for people to remember, but
16
+ * because we wanted it to match simplewallet's implementation we made
17
+ * sure that we followed the spec... as it was originally... before we
18
+ * duffed the implementation."
19
+ */
20
+ // Source: https://old.reddit.com/r/Monero/comments/3s80l2/why_mymonero_key_derivation_is_different_than_for/cwv5lzs/
21
+ // rpc create new wallet command handling code calls generate() method on wallet2 object instance
22
+ // 3653: wal->generate(wallet_file, req.password, dummy_key, false, false);
23
+ //https://github.com/monero-project/monero/blob/48ad374b0d6d6e045128729534dc2508e6999afe/src/wallet/wallet_rpc_server.cpp#L3653
24
+ // wallet2.cpp generate method definition last few lines, notice it returns retval which is the result of m_account.generate():
25
+ /**
26
+ * crypto::secret_key retval = m_account.generate(recovery_param, recover, two_random);
27
+
28
+ [ ... ommitted irrelevant wallet key file saving lines... ]
29
+ return retval;
30
+ }
31
+ */
32
+ // https://github.com/monero-project/monero/blob/48ad374b0d6d6e045128729534dc2508e6999afe/src/wallet/wallet2.cpp#L5683
33
+ // the account.generate() method is defined in account.cpp.
34
+ // Relevant lines show it calls the crypto.cpp generate_keys() method twice.
35
+ // 1. the first call uses a random number generator to make the spend private key
36
+ // (and calls sc_reduce32 on it to make sure it is a valid ed25519 scalar)
37
+ // 2. then it hashes that spend private key with keccak to make the view private key
38
+ // 3. then it calls generate_keys() again on this hashed value to make the view private key
39
+ // (to make sure via sc_reduce32 that the view private key is a valid ed25519 scalar)
40
+ //
41
+ // another side effect of this generate_keys function is that it creates the respective public keys from the private keys
42
+ /**
43
+ * crypto::secret_key first = generate_keys(m_keys.m_account_address.m_spend_public_key, m_keys.m_spend_secret_key, recovery_key, recover);
44
+
45
+ // rng for generating second set of keys is hash of first rng. means only one set of electrum-style words needed for recovery
46
+ crypto::secret_key second;
47
+ keccak((uint8_t *)&m_keys.m_spend_secret_key, sizeof(crypto::secret_key), (uint8_t *)&second, sizeof(crypto::secret_key));
48
+
49
+ generate_keys(m_keys.m_account_address.m_view_public_key, m_keys.m_view_secret_key, second, two_random ? false : true);
50
+ [ ... ommitted irrelevant timestamp lines... ]
51
+ return first; [... returns first which is the spend private key ... ]
52
+ */
53
+ // https://github.com/monero-project/monero/blob/48ad374b0d6d6e045128729534dc2508e6999afe/src/cryptonote_basic/account.cpp#L166-L195
54
+ // generate_keys() method definition shows it calls sc_reduce32 on the input key material to make sure it is a valid ed25519 scalar
55
+ // https://github.com/monero-project/monero/blob/master/src/crypto/crypto.cpp#L153
56
+ // side note: simplewallet cpp codepath is similar to the walletrpc server codepath shown above
57
+ // cryptonote_basic/account.cpp generate() method returns first (also known as the spend private key) which becomes recovery_val:
58
+ // 4832 recovery_val = m_wallet->generate(m_wallet_file, std::move(rc.second).password(), recovery_key, recover, two_random, create_address_file);
59
+ // https://github.com/monero-project/monero/blob/48ad374b0d6d6e045128729534dc2508e6999afe/src/simplewallet/simplewallet.cpp#L4832
60
+ // simple wallet turns spend private key (recovery_val) into mnemonic words with this call:
61
+ // 4849 crypto::ElectrumWords::bytes_to_words(recovery_val, electrum_words, mnemonic_language);
62
+ // https://github.com/monero-project/monero/blob/48ad374b0d6d6e045128729534dc2508e6999afe/src/simplewallet/simplewallet.cpp#L4849
63
+ import { WasmProcessor } from "../wasm-processing/wasmProcessor";
64
+ export async function makeSpendKey() {
65
+ const wasmProcessor = await WasmProcessor.init();
66
+ let result = undefined;
67
+ wasmProcessor.readFromWasmMemory = (ptr, len) => {
68
+ result = String(wasmProcessor.readString(ptr, len));
69
+ };
70
+ //@ts-ignore
71
+ wasmProcessor.tinywasi.instance.exports.make_spendkey();
72
+ if (!result) {
73
+ throw new Error("Failed to make spend key");
74
+ }
75
+ return result;
76
+ }
77
+ export async function makeSpendKeyFromSeed(wallet_secret) {
78
+ if (wallet_secret.length !== 128) {
79
+ throw new Error(`Invalid wallet secret length: ${wallet_secret.length}.
80
+ Expected 128 hex characters (64 bytes).`);
81
+ }
82
+ const wasmProcessor = await WasmProcessor.init();
83
+ wasmProcessor.writeToWasmMemory = (ptr, len) => {
84
+ wasmProcessor.writeString(ptr, len, wallet_secret);
85
+ };
86
+ let result = undefined;
87
+ wasmProcessor.readFromWasmMemory = (ptr, len) => {
88
+ result = String(wasmProcessor.readString(ptr, len));
89
+ };
90
+ //@ts-ignore
91
+ wasmProcessor.tinywasi.instance.exports.make_spendkey_from_seed(wallet_secret.length);
92
+ if (!result) {
93
+ throw new Error("Failed to obtain spend_key from seed.");
94
+ }
95
+ return result;
96
+ }
97
+ export async function makeViewKey(spend_private_key) {
98
+ if (spend_private_key.length !== 64) {
99
+ throw new Error(`Invalid spendkey length: ${spend_private_key.length}.
100
+ Expected 64 hex characters (32 bytes).`);
101
+ }
102
+ const wasmProcessor = await WasmProcessor.init();
103
+ wasmProcessor.writeToWasmMemory = (ptr, len) => {
104
+ wasmProcessor.writeString(ptr, len, spend_private_key);
105
+ };
106
+ let result = undefined;
107
+ wasmProcessor.readFromWasmMemory = (ptr, len) => {
108
+ result = JSON.parse(wasmProcessor.readString(ptr, len));
109
+ };
110
+ //@ts-ignore
111
+ wasmProcessor.tinywasi.instance.exports.make_viewkey(spend_private_key.length);
112
+ if (!result) {
113
+ throw new Error("Failed to obtain view key from spend key.");
114
+ }
115
+ return result;
116
+ }
117
+ /**
118
+ *
119
+ * unlike crypto-ops.c sc_reduce32, this is not unrolled
120
+ * we run this once at wallet creation, no timing side channel expected,
121
+ * goal is to be as clear as possible.
122
+ *
123
+ * crypto-ops.c source: https://github.com/monero-project/monero/blob/master/src/crypto/crypto-ops.c#L2432-L2544
124
+ * @param input - random 32 bytes used as seed, private key
125
+ * @returns - reduced 32 bytes that are a valid ed25519 scalar
126
+ */
127
+ function sc_reduce32(input) {
128
+ if (input.length !== 32)
129
+ throw new Error("Input must be 32 bytes");
130
+ const x = bytesToBigInt(input);
131
+ const l = 2n ** 252n + 27742317777372353535851937790883648493n;
132
+ const reduced = x % l;
133
+ return bigIntToBytes(reduced);
134
+ }
135
+ // l source: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1
136
+ /**
137
+ * This function turns a list of bytes into one big number.
138
+ * It uses a loop to add up each byte after moving it to the right spot.
139
+ * Each byte is like a digit in a number where positions grow by 256 each time.
140
+ * Shifting left by 8 bits per position multiplies by 256 to place it correctly.
141
+ * This builds the full number step by step without mistakes.
142
+ */
143
+ function bytesToBigInt(bytes) {
144
+ return bytes.reduce((acc, byte, i) => acc + (BigInt(byte) << BigInt(i * 8)), 0n);
145
+ }
146
+ /**
147
+ * The function starts with 0 as the total.
148
+ * It takes the first byte and adds it as is.
149
+ * For the next byte, it moves it left by 8 bits before adding.
150
+ * This move makes room for the previous byte below it.
151
+ * Each further byte shifts more to stack on top.
152
+ */
153
+ // Other direction:
154
+ /**
155
+ * This function breaks a big number into a list of small bytes.
156
+ * It creates a list of fixed size and fills each spot with one byte from the number.
157
+ * Shifting right brings the right part down to grab it.
158
+ * Masking keeps only that one byte and ignores the rest.
159
+ * It does this for each position to get the full list.
160
+ */
161
+ function bigIntToBytes(value, length = 32) {
162
+ return Uint8Array.from({ length }, (_, i) => Number((value >> BigInt(i * 8)) & 0xffn));
163
+ }
164
+ /**
165
+ * The function makes an empty list of the given length.
166
+ * For the first spot, it takes the lowest byte.
167
+ * It shifts the number right by 0 bits at start.
168
+ * Masking with 255 grabs just 8 bits.
169
+ * For the next spot, it shifts right by 8 bits first.
170
+ */
171
+ function testBigIntUint8Conversion(originalBigInt, length = 32) {
172
+ const bytes = bigIntToBytes(originalBigInt, length);
173
+ const reconstructedBigInt = bytesToBigInt(bytes);
174
+ console.log("Original BigInt:", originalBigInt);
175
+ console.log("Bytes:", bytes);
176
+ console.log("Reconstructed BigInt:", reconstructedBigInt);
177
+ return originalBigInt === reconstructedBigInt;
178
+ }
179
+ // usage
180
+ // const testValue = 123456789012345678901234567890n; // A BigInt fitting < 32 bytes
181
+ // console.log("Test result:", testBigIntUint8Conversion(testValue)); // Should log true
182
+ function testScReduce32() {
183
+ const secret_key = new Uint8Array(32);
184
+ crypto.getRandomValues(secret_key);
185
+ const reduced = sc_reduce32(secret_key);
186
+ const reducedBigInt = bytesToBigInt(reduced);
187
+ const l = 2n ** 252n + 27742317777372353535851937790883648493n;
188
+ const isValid = reducedBigInt >= 0n && reducedBigInt < l && reduced.length === 32;
189
+ console.log("Random input:", secret_key);
190
+ console.log("Reduced output:", reduced);
191
+ console.log("Reduced as BigInt:", reducedBigInt);
192
+ console.log("Is valid Ed25519 scalar:", isValid);
193
+ // non random example (do not use not cryptographically secure values in production. use crypto.getRandomValues for real keys)
194
+ const deadbeef = new Uint8Array(32).fill(0xde);
195
+ const reducedDeadbeef = sc_reduce32(deadbeef);
196
+ const reducedDeadbeefBigInt = bytesToBigInt(reducedDeadbeef);
197
+ const isValidDeadbeef = reducedDeadbeefBigInt >= 0n &&
198
+ reducedDeadbeefBigInt < l &&
199
+ reducedDeadbeef.length === 32;
200
+ console.log("Deadbeef input:", deadbeef);
201
+ console.log("Reduced deadbeef output:", reducedDeadbeef);
202
+ console.log("Reduced deadbeef as BigInt:", reducedDeadbeefBigInt);
203
+ console.log("Is valid Ed25519 scalar (deadbeef):", isValidDeadbeef);
204
+ return isValid && isValidDeadbeef;
205
+ }
206
+ // usage
207
+ // console.log("Test result:", testScReduce32()); // Should log true
@@ -0,0 +1,11 @@
1
+ export declare const stagenet_pk_path = ".env";
2
+ export declare const testnet_pk_path = ".env.local";
3
+ export declare const regtest_pk_path = ".env.local";
4
+ export declare function writeStagenetSpendViewKeysToDotEnv(spend_key?: string): Promise<string>;
5
+ export declare function writeRegtestSpendViewKeysToDotEnvTestLocal(spend_key?: string): Promise<string>;
6
+ export declare function writeTestnetSpendViewKeysToDotEnvLocal(spend_key?: string): Promise<string>;
7
+ export declare function writeWalletSecretsToDotEnv(wallet_secret: Uint8Array): Promise<string>;
8
+ export declare function writeEnvLineToDotEnvRefresh(key: string, value: string, path?: string): Promise<void>;
9
+ export declare function writeEnvLineToDotEnv(key: string, value: string, path?: string): Promise<void>;
10
+ export declare const STAGENET_FRESH_WALLET_HEIGHT_DEFAULT = 2014841;
11
+ export declare const REGTEST_FRESH_WALLET_HEIGHT_DEFAULT = 1;
@@ -0,0 +1,75 @@
1
+ // write testnet keys .env.local
2
+ // write stagenet keys .env
3
+ // write mainnet keys to stdout can > redirect to the place of your desires
4
+ // (but you really shouldnt, use a seedphrase instead)
5
+ import { atomicWrite } from "../io/atomicWrite";
6
+ import { makeSpendKey, makeSpendKeyFromSeed, makeViewKey } from "./keypairs";
7
+ export const stagenet_pk_path = ".env";
8
+ export const testnet_pk_path = ".env.local";
9
+ export const regtest_pk_path = ".env.local";
10
+ // writes "vkPRIMARY_KEY=<view_key> \n skPRIMARY_KEY=<spend_key>" to .env for stagenet
11
+ export async function writeStagenetSpendViewKeysToDotEnv(spend_key) {
12
+ spend_key = spend_key || (await makeSpendKey());
13
+ let view_pair = await makeViewKey(spend_key);
14
+ let primary_address = view_pair.stagenet_primary;
15
+ await writeEnvLineToDotEnvRefresh(`vk${primary_address}`, view_pair.view_key, stagenet_pk_path);
16
+ await writeEnvLineToDotEnvRefresh(`sk${primary_address}`, spend_key, stagenet_pk_path);
17
+ return primary_address;
18
+ }
19
+ // writes "vkPRIMARY_KEY=<view_key> \n skPRIMARY_KEY=<spend_key>" to .env.local for regtest
20
+ export async function writeRegtestSpendViewKeysToDotEnvTestLocal(spend_key) {
21
+ spend_key = spend_key || (await makeSpendKey());
22
+ let view_pair = await makeViewKey(spend_key);
23
+ let primary_address = view_pair.mainnet_primary; // regtest uses mainet style addresses
24
+ await writeEnvLineToDotEnvRefresh(`vk${primary_address}`, view_pair.view_key, regtest_pk_path);
25
+ await writeEnvLineToDotEnvRefresh(`sk${primary_address}`, spend_key, regtest_pk_path);
26
+ return primary_address;
27
+ }
28
+ // writes "vkPRIMARY_KEY=<view_key> \n skPRIMARY_KEY=<spend_key>" to .env.local for testnet
29
+ export async function writeTestnetSpendViewKeysToDotEnvLocal(spend_key) {
30
+ spend_key = spend_key || (await makeSpendKey());
31
+ let view_pair = await makeViewKey(spend_key);
32
+ let primary_address = view_pair.testnet_primary;
33
+ await writeEnvLineToDotEnvRefresh(`vk${primary_address}`, view_pair.view_key, testnet_pk_path);
34
+ await writeEnvLineToDotEnvRefresh(`sk${primary_address}`, spend_key, testnet_pk_path);
35
+ return primary_address;
36
+ }
37
+ // writes "vkPRIMARY_KEY=<view_key> \n skPRIMARY_KEY=<spend_key>" to .env
38
+ // use seedphrase package to get wallet secret from seed + passphrase: getWalletSecret function
39
+ export async function writeWalletSecretsToDotEnv(wallet_secret) {
40
+ let spend_key = await makeSpendKeyFromSeed(wallet_secret.toHex());
41
+ let view_pair = await makeViewKey(spend_key);
42
+ let primary_address = view_pair.mainnet_primary;
43
+ await writeEnvLineToDotEnvRefresh(`vk${primary_address}`, view_pair.view_key);
44
+ await writeEnvLineToDotEnvRefresh(`sk${primary_address}`, spend_key);
45
+ return primary_address;
46
+ }
47
+ // this should be used in a web backend that does (non custodial) scanning
48
+ // to add new view / spend keys, received from the users without a restart.
49
+ export async function writeEnvLineToDotEnvRefresh(key, value, path = ".env") {
50
+ await writeEnvLineToDotEnv(key, value, path);
51
+ Bun.env[key.trim()] = value.trim();
52
+ }
53
+ // assuming Bun.file + Bun.write are filled in or available natively,
54
+ // this is also used by io/indexedDB.ts
55
+ export async function writeEnvLineToDotEnv(key, value, path = ".env") {
56
+ // this file should be treated as ephemeral
57
+ // private spendkeys + viewkeys are deterministically derived from seedphrase and password
58
+ // specific to indexedDB, browser extentension use case: Bun.env = .env contents
59
+ //
60
+ // we have to go through indexedDB just so the background worker has access to this.
61
+ // (after waking up from an alarm or onmessage event)
62
+ const file = Bun.file(path);
63
+ const content = await file
64
+ .text()
65
+ .catch(() => { })
66
+ .then((c) => c || "");
67
+ const lines = content.split("\n");
68
+ const idx = lines.findIndex((line) => line.startsWith(key));
69
+ const updatedLines = idx === -1
70
+ ? [...lines, `${key.trim()}=${value.trim()}`]
71
+ : lines.with(idx, `${key.trim()}=${value.trim()}`);
72
+ await atomicWrite(path, updatedLines.join("\n"));
73
+ }
74
+ export const STAGENET_FRESH_WALLET_HEIGHT_DEFAULT = 2014841; // current height of stagenet dec 18 2025,
75
+ export const REGTEST_FRESH_WALLET_HEIGHT_DEFAULT = 1;