@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.
- package/README.md +3 -3
- package/dist/api.d.ts +25 -96
- package/dist/api.js +23 -175
- package/dist/io/BunFileInterface.d.ts +32 -0
- package/dist/io/atomicWrite.d.ts +2 -0
- package/dist/io/atomicWrite.js +10 -0
- package/dist/io/extension.d.ts +18 -0
- package/dist/io/extension.js +11 -0
- package/dist/io/indexedDB.d.ts +45 -0
- package/dist/io/indexedDB.js +221 -0
- package/dist/io/readDir.d.ts +1 -0
- package/dist/io/readDir.js +7 -0
- package/dist/io/sleep.d.ts +1 -0
- package/dist/io/sleep.js +1 -0
- package/dist/keypairs-seeds/keypairs.d.ts +29 -0
- package/dist/keypairs-seeds/keypairs.js +207 -0
- package/dist/keypairs-seeds/writeKeypairs.d.ts +11 -0
- package/dist/keypairs-seeds/writeKeypairs.js +75 -0
- package/dist/node-interaction/binaryEndpoints.d.ts +59 -14
- package/dist/node-interaction/binaryEndpoints.js +110 -54
- package/dist/node-interaction/jsonEndpoints.d.ts +249 -187
- package/dist/node-interaction/jsonEndpoints.js +287 -0
- package/dist/node-interaction/nodeUrl.d.ts +129 -0
- package/dist/node-interaction/nodeUrl.js +113 -0
- package/dist/scanning-syncing/backgroundWorker.d.ts +6 -0
- package/dist/scanning-syncing/backgroundWorker.js +56 -0
- package/dist/scanning-syncing/connectionStatus.d.ts +15 -0
- package/dist/scanning-syncing/connectionStatus.js +35 -0
- package/dist/scanning-syncing/openWallet.d.ts +28 -0
- package/dist/scanning-syncing/openWallet.js +57 -0
- package/dist/scanning-syncing/scanSettings.d.ts +96 -0
- package/dist/scanning-syncing/scanSettings.js +243 -0
- package/dist/scanning-syncing/scanresult/computeKeyImage.d.ts +3 -0
- package/dist/scanning-syncing/scanresult/computeKeyImage.js +21 -0
- package/dist/scanning-syncing/scanresult/getBlocksbinBuffer.d.ts +28 -0
- package/dist/scanning-syncing/scanresult/getBlocksbinBuffer.js +52 -0
- package/dist/scanning-syncing/scanresult/reorg.d.ts +14 -0
- package/dist/scanning-syncing/scanresult/reorg.js +78 -0
- package/dist/scanning-syncing/scanresult/scanCache.d.ts +84 -0
- package/dist/scanning-syncing/scanresult/scanCache.js +134 -0
- package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +149 -0
- package/dist/scanning-syncing/scanresult/scanCacheOpened.js +648 -0
- package/dist/scanning-syncing/scanresult/scanResult.d.ts +64 -0
- package/dist/scanning-syncing/scanresult/scanResult.js +213 -0
- package/dist/scanning-syncing/scanresult/scanStats.d.ts +60 -0
- package/dist/scanning-syncing/scanresult/scanStats.js +273 -0
- package/dist/scanning-syncing/worker-entrypoints/worker.d.ts +1 -0
- package/dist/scanning-syncing/worker-entrypoints/worker.js +8 -0
- package/dist/scanning-syncing/worker-mains/worker.d.ts +1 -0
- package/dist/scanning-syncing/worker-mains/worker.js +7 -0
- package/dist/send-functionality/conversion.d.ts +4 -0
- package/dist/send-functionality/conversion.js +75 -0
- package/dist/send-functionality/inputSelection.d.ts +13 -0
- package/dist/send-functionality/inputSelection.js +8 -0
- package/dist/send-functionality/transactionBuilding.d.ts +51 -0
- package/dist/send-functionality/transactionBuilding.js +111 -0
- package/dist/tools/monero-tools.d.ts +46 -0
- package/dist/tools/monero-tools.js +165 -0
- package/dist/viewpair/ViewPair.d.ts +157 -0
- package/dist/viewpair/ViewPair.js +346 -0
- package/dist/wasm-processing/wasi.js +1 -2
- package/dist/wasm-processing/wasmFile.d.ts +1 -1
- package/dist/wasm-processing/wasmFile.js +2 -2
- package/dist/wasm-processing/wasmProcessor.d.ts +16 -4
- package/dist/wasm-processing/wasmProcessor.js +23 -7
- package/package.json +29 -6
- package/dist/testscrap.js +0 -36
- /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 @@
|
|
|
1
|
+
export declare const sleep: (ms: number) => Promise<unknown>;
|
package/dist/io/sleep.js
ADDED
|
@@ -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;
|