@solard/sdk 0.2.6 → 0.2.8

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.
@@ -0,0 +1,228 @@
1
+ import {
2
+ Keypair,
3
+ Transaction,
4
+ VersionedTransaction,
5
+ type PublicKey,
6
+ } from "@solana/web3.js";
7
+
8
+ import type { BrowserStorageLike, BrowserWalletSigner } from "./types.ts";
9
+ import { defaultBrowserStorage } from "./storage.ts";
10
+
11
+ type StoredKeypair = {
12
+ version: 1;
13
+ name: string;
14
+ publicKey: string;
15
+ salt: string;
16
+ iv: string;
17
+ ciphertext: string;
18
+ iterations: number;
19
+ createdAtMs: number;
20
+ };
21
+
22
+ const encoder = new TextEncoder();
23
+
24
+ function bytesToBase64(bytes: Uint8Array): string {
25
+ let binary = "";
26
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) {
27
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
28
+ }
29
+ return btoa(binary);
30
+ }
31
+
32
+ function base64ToBytes(value: string): Uint8Array {
33
+ const binary = atob(value);
34
+ const bytes = new Uint8Array(binary.length);
35
+ for (let index = 0; index < binary.length; index += 1) {
36
+ bytes[index] = binary.charCodeAt(index);
37
+ }
38
+ return bytes;
39
+ }
40
+
41
+ function requireCrypto(): SubtleCrypto {
42
+ const subtle = globalThis.crypto?.subtle;
43
+ if (!subtle) {
44
+ throw new Error("WebCrypto SubtleCrypto is unavailable in this browser.");
45
+ }
46
+ return subtle;
47
+ }
48
+
49
+ async function deriveKey(
50
+ password: string,
51
+ salt: Uint8Array,
52
+ iterations: number,
53
+ ): Promise<CryptoKey> {
54
+ if (!password) throw new Error("A non-empty vault password is required.");
55
+ const subtle = requireCrypto();
56
+ const material = await subtle.importKey(
57
+ "raw",
58
+ encoder.encode(password),
59
+ "PBKDF2",
60
+ false,
61
+ ["deriveKey"],
62
+ );
63
+ return await subtle.deriveKey(
64
+ {
65
+ name: "PBKDF2",
66
+ hash: "SHA-256",
67
+ salt,
68
+ iterations,
69
+ },
70
+ material,
71
+ { name: "AES-GCM", length: 256 },
72
+ false,
73
+ ["encrypt", "decrypt"],
74
+ );
75
+ }
76
+
77
+ export class KeypairBrowserSigner implements BrowserWalletSigner {
78
+ readonly publicKey: PublicKey;
79
+
80
+ constructor(private readonly keypair: Keypair) {
81
+ this.publicKey = keypair.publicKey;
82
+ }
83
+
84
+ async signTransaction<T extends Transaction | VersionedTransaction>(
85
+ transaction: T,
86
+ ): Promise<T> {
87
+ if (transaction instanceof VersionedTransaction) {
88
+ transaction.sign([this.keypair]);
89
+ } else {
90
+ transaction.partialSign(this.keypair);
91
+ }
92
+ return transaction;
93
+ }
94
+
95
+ async signAllTransactions<T extends Transaction | VersionedTransaction>(
96
+ transactions: T[],
97
+ ): Promise<T[]> {
98
+ for (const transaction of transactions) {
99
+ await this.signTransaction(transaction);
100
+ }
101
+ return transactions;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Optional local-key mode for browser-only apps.
107
+ *
108
+ * Secret bytes are AES-GCM encrypted before being written to localStorage.
109
+ * For user-facing trading apps, an injected wallet extension remains the
110
+ * preferred signer because XSS can access any origin's localStorage.
111
+ */
112
+ export class BrowserKeyVault {
113
+ constructor(
114
+ private readonly storage: BrowserStorageLike = defaultBrowserStorage(),
115
+ private readonly namespace = "solard:browser:v1:vault",
116
+ ) {}
117
+
118
+ private key(name: string): string {
119
+ return `${this.namespace}:${name.trim()}`;
120
+ }
121
+
122
+ list(): Array<{ name: string; publicKey: string; createdAtMs: number }> {
123
+ const prefix = `${this.namespace}:`;
124
+ const rows: Array<{
125
+ name: string;
126
+ publicKey: string;
127
+ createdAtMs: number;
128
+ }> = [];
129
+
130
+ for (let index = 0; index < this.storage.length; index += 1) {
131
+ const key = this.storage.key(index);
132
+ if (!key?.startsWith(prefix)) continue;
133
+ const raw = this.storage.getItem(key);
134
+ if (!raw) continue;
135
+ const row = JSON.parse(raw) as StoredKeypair;
136
+ rows.push({
137
+ name: row.name,
138
+ publicKey: row.publicKey,
139
+ createdAtMs: row.createdAtMs,
140
+ });
141
+ }
142
+
143
+ return rows.sort((a, b) => a.name.localeCompare(b.name));
144
+ }
145
+
146
+ async save(
147
+ nameInput: string,
148
+ keypair: Keypair,
149
+ password: string,
150
+ options: { overwrite?: boolean; iterations?: number } = {},
151
+ ): Promise<void> {
152
+ const name = nameInput.trim();
153
+ if (!name) throw new Error("Vault key name is required.");
154
+ const storageKey = this.key(name);
155
+ if (this.storage.getItem(storageKey) && !options.overwrite) {
156
+ throw new Error(`Browser vault key "${name}" already exists.`);
157
+ }
158
+
159
+ const iterations = Math.max(100_000, options.iterations ?? 310_000);
160
+ const salt = crypto.getRandomValues(new Uint8Array(16));
161
+ const iv = crypto.getRandomValues(new Uint8Array(12));
162
+ const key = await deriveKey(password, salt, iterations);
163
+ const encrypted = await requireCrypto().encrypt(
164
+ { name: "AES-GCM", iv },
165
+ key,
166
+ keypair.secretKey,
167
+ );
168
+
169
+ const row: StoredKeypair = {
170
+ version: 1,
171
+ name,
172
+ publicKey: keypair.publicKey.toBase58(),
173
+ salt: bytesToBase64(salt),
174
+ iv: bytesToBase64(iv),
175
+ ciphertext: bytesToBase64(new Uint8Array(encrypted)),
176
+ iterations,
177
+ createdAtMs: Date.now(),
178
+ };
179
+ this.storage.setItem(storageKey, JSON.stringify(row));
180
+ }
181
+
182
+ async generate(
183
+ name: string,
184
+ password: string,
185
+ options: { overwrite?: boolean; iterations?: number } = {},
186
+ ): Promise<KeypairBrowserSigner> {
187
+ const keypair = Keypair.generate();
188
+ await this.save(name, keypair, password, options);
189
+ return new KeypairBrowserSigner(keypair);
190
+ }
191
+
192
+ async unlock(
193
+ nameInput: string,
194
+ password: string,
195
+ ): Promise<KeypairBrowserSigner> {
196
+ const name = nameInput.trim();
197
+ const raw = this.storage.getItem(this.key(name));
198
+ if (!raw) throw new Error(`Unknown browser vault key: ${name}`);
199
+
200
+ const row = JSON.parse(raw) as StoredKeypair;
201
+ if (row.version !== 1) {
202
+ throw new Error(`Unsupported browser vault version: ${row.version}`);
203
+ }
204
+
205
+ try {
206
+ const salt = base64ToBytes(row.salt);
207
+ const iv = base64ToBytes(row.iv);
208
+ const ciphertext = base64ToBytes(row.ciphertext);
209
+ const key = await deriveKey(password, salt, row.iterations);
210
+ const plain = await requireCrypto().decrypt(
211
+ { name: "AES-GCM", iv },
212
+ key,
213
+ ciphertext,
214
+ );
215
+ const keypair = Keypair.fromSecretKey(new Uint8Array(plain));
216
+ if (keypair.publicKey.toBase58() !== row.publicKey) {
217
+ throw new Error("Decrypted key does not match stored public key.");
218
+ }
219
+ return new KeypairBrowserSigner(keypair);
220
+ } catch (error) {
221
+ throw new Error("Unable to decrypt browser wallet.", { cause: error });
222
+ }
223
+ }
224
+
225
+ remove(name: string): void {
226
+ this.storage.removeItem(this.key(name.trim()));
227
+ }
228
+ }
package/src/index.ts CHANGED
@@ -5,3 +5,22 @@
5
5
  * the same wallet store, transaction engine, venues, senders and database.
6
6
  */
7
7
  export * from "@solard/core";
8
+
9
+ // Explicit source-level re-export for the generic Jupiter swap helpers.
10
+ // This avoids depending on the @solard/core package barrel resolution when the
11
+ // monorepo CLI is executed directly from source with Bun.
12
+ export {
13
+ executeJupiterSwap,
14
+ quoteJupiterSwap,
15
+ executeJupiterTokenToSol,
16
+ quoteJupiterTokenToSol,
17
+ } from "../../core/src/chain/jupiter-swap.ts";
18
+ export type {
19
+ JupiterSwapExecuteResult,
20
+ JupiterSwapOrder,
21
+ JupiterSwapQuote,
22
+ } from "../../core/src/chain/jupiter-swap.ts";
23
+
24
+ // Direct source re-exports used by the source-executed CLI.
25
+ export { readMint } from "../../core/src/chain/state.ts";
26
+ export { transferTokenIxs } from "../../core/src/tx/spl.ts";