@majikah/majik-key 0.2.11 → 0.2.13
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 +179 -820
- package/dist/core/types.d.ts +7 -0
- package/dist/core/web3/bitcoin/bitcoin.d.ts +71 -0
- package/dist/core/web3/bitcoin/bitcoin.js +126 -0
- package/dist/core/web3/bitcoin/constants.d.ts +2 -0
- package/dist/core/web3/bitcoin/constants.js +7 -0
- package/dist/core/web3/bitcoin/types.d.ts +23 -0
- package/dist/core/web3/bitcoin/types.js +1 -0
- package/dist/core/web3/index.d.ts +5 -0
- package/dist/core/web3/index.js +2 -0
- package/dist/core/web3/solana/constants.d.ts +1 -0
- package/dist/core/web3/solana/constants.js +1 -0
- package/dist/core/web3/solana/solana.d.ts +73 -0
- package/dist/core/web3/solana/solana.js +120 -0
- package/dist/core/web3/solana/types.d.ts +18 -0
- package/dist/core/web3/solana/types.js +1 -0
- package/dist/core/web3/types.d.ts +7 -0
- package/dist/core/web3/types.js +1 -0
- package/dist/core/web3/utils.d.ts +1 -0
- package/dist/core/web3/utils.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/majik-key.d.ts +73 -0
- package/dist/majik-key.js +209 -3
- package/package.json +24 -4
package/dist/core/types.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface MajikKeyJSON {
|
|
|
19
19
|
encryptedEdSecretKey?: string;
|
|
20
20
|
mlDsaPublicKey?: string;
|
|
21
21
|
encryptedMlDsaSecretKey?: string;
|
|
22
|
+
btcPublicKey?: string;
|
|
23
|
+
encryptedBtcSecretKey?: string;
|
|
22
24
|
mnemonicLanguage?: MnemonicLanguage;
|
|
23
25
|
}
|
|
24
26
|
export interface MajikKeyDangerousJSON extends MajikKeyJSON {
|
|
@@ -26,6 +28,7 @@ export interface MajikKeyDangerousJSON extends MajikKeyJSON {
|
|
|
26
28
|
mlKemSecretKeyBase64: string;
|
|
27
29
|
edSecretKeyBase64: string;
|
|
28
30
|
mlDsaSecretKeyBase64: string;
|
|
31
|
+
btcSecretKeyBase64?: string;
|
|
29
32
|
}
|
|
30
33
|
export interface MajikKeyMetadata {
|
|
31
34
|
id: string;
|
|
@@ -35,6 +38,10 @@ export interface MajikKeyMetadata {
|
|
|
35
38
|
isLocked: boolean;
|
|
36
39
|
kdfVersion: number;
|
|
37
40
|
hasMlKem: boolean;
|
|
41
|
+
web3: {
|
|
42
|
+
hasBitcoin?: boolean;
|
|
43
|
+
hasSolana?: boolean;
|
|
44
|
+
};
|
|
38
45
|
mnemonicLanguage?: MnemonicLanguage;
|
|
39
46
|
}
|
|
40
47
|
export interface MnemonicJSON {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bitcoin.ts
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ EXPERIMENTAL — Bitcoin keypair utilities for MajikKey.
|
|
5
|
+
* This module's API may change or be removed without notice in a minor version.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Real BIP-32/BIP-84 HD derivation directly off the raw 64-byte BIP-39
|
|
9
|
+
* seed — NOT a hash-based domain separation like Solana. This matters:
|
|
10
|
+
* BIP-32's tree structure lets us get privacy AND portability from the
|
|
11
|
+
* exact same standard, auditable derivation, just by choosing the path:
|
|
12
|
+
*
|
|
13
|
+
* MAJIK_BITCOIN_DOMAIN_PATH (default) — effectively private to Majik;
|
|
14
|
+
* not the path any generic wallet would derive by default.
|
|
15
|
+
* MAJIK_BITCOIN_STANDARD_PATH (opt-in) — the REAL BIP-84 mainnet path;
|
|
16
|
+
* recoverable in any standard wallet using nothing but the mnemonic.
|
|
17
|
+
*
|
|
18
|
+
* - `privateKey` is the raw 32-byte secp256k1 scalar. `toWIF()` encodes it
|
|
19
|
+
* into Wallet Import Format — the universal paste-in-import string every
|
|
20
|
+
* Bitcoin wallet accepts (base58check, versioned, deterministic).
|
|
21
|
+
* - `@scure/btc-signer` is NOT a hard dependency. It is lazily `import()`-ed
|
|
22
|
+
* only for bech32 address encoding / PSBT construction. If it isn't
|
|
23
|
+
* installed, we throw a clear, actionable MajikKeyError instead of
|
|
24
|
+
* failing module load.
|
|
25
|
+
*/
|
|
26
|
+
export interface BitcoinKeypairMaterial {
|
|
27
|
+
/** 32-byte secp256k1 private key. */
|
|
28
|
+
privateKey: Uint8Array;
|
|
29
|
+
/** 33-byte compressed secp256k1 public key. */
|
|
30
|
+
publicKey: Uint8Array;
|
|
31
|
+
}
|
|
32
|
+
export interface BitcoinDerivationOptions {
|
|
33
|
+
/**
|
|
34
|
+
* If true, derive the REAL BIP-84 mainnet path (SLIP-44 coin type 0) —
|
|
35
|
+
* the address any standard wallet would show for this mnemonic.
|
|
36
|
+
* Defaults to false (Majik's domain-separated path).
|
|
37
|
+
*/
|
|
38
|
+
standard?: boolean;
|
|
39
|
+
/** Explicit derivation path — overrides `standard` if provided. */
|
|
40
|
+
path?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Derive a Bitcoin keypair via standard BIP-32/BIP-84 from the raw 64-byte
|
|
44
|
+
* BIP-39 seed. Call this once at account creation/import time (mirrors
|
|
45
|
+
* ML-KEM/Ed25519/ML-DSA derivation) — the seed itself is never stored, only
|
|
46
|
+
* the resulting key, encrypted at rest like the others.
|
|
47
|
+
*/
|
|
48
|
+
export declare function deriveBitcoinKeypairFromSeed(seed: Uint8Array, options?: BitcoinDerivationOptions): BitcoinKeypairMaterial;
|
|
49
|
+
/**
|
|
50
|
+
* Re-derive the public key from a raw private key. Used when unlocking —
|
|
51
|
+
* we only encrypt/store the private key, so the public key is recomputed
|
|
52
|
+
* on unlock rather than stored redundantly encrypted.
|
|
53
|
+
*/
|
|
54
|
+
export declare function bitcoinPublicKeyFromPrivateKey(privateKey: Uint8Array): Uint8Array;
|
|
55
|
+
/** Sign a 32-byte message hash (already hashed — e.g. a Bitcoin sighash). */
|
|
56
|
+
export declare function signWithBitcoinMaterial(material: BitcoinKeypairMaterial, messageHash: Uint8Array, scheme?: "ecdsa" | "schnorr"): Uint8Array;
|
|
57
|
+
/**
|
|
58
|
+
* Encode a private key as WIF — the universal paste-in-import string every
|
|
59
|
+
* Bitcoin wallet accepts. Deterministic: same private key → same WIF, always.
|
|
60
|
+
*/
|
|
61
|
+
export declare function toWIF(material: BitcoinKeypairMaterial, options?: {
|
|
62
|
+
compressed?: boolean;
|
|
63
|
+
}): string;
|
|
64
|
+
type BtcSignerModule = typeof import("@scure/btc-signer");
|
|
65
|
+
export declare function loadBtcSigner(): Promise<BtcSignerModule>;
|
|
66
|
+
/**
|
|
67
|
+
* Native SegWit (bech32, "bc1...") mainnet address for this material's
|
|
68
|
+
* public key. Requires @scure/btc-signer — see loadBtcSigner().
|
|
69
|
+
*/
|
|
70
|
+
export declare function toBitcoinAddress(material: BitcoinKeypairMaterial): Promise<string>;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bitcoin.ts
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ EXPERIMENTAL — Bitcoin keypair utilities for MajikKey.
|
|
5
|
+
* This module's API may change or be removed without notice in a minor version.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Real BIP-32/BIP-84 HD derivation directly off the raw 64-byte BIP-39
|
|
9
|
+
* seed — NOT a hash-based domain separation like Solana. This matters:
|
|
10
|
+
* BIP-32's tree structure lets us get privacy AND portability from the
|
|
11
|
+
* exact same standard, auditable derivation, just by choosing the path:
|
|
12
|
+
*
|
|
13
|
+
* MAJIK_BITCOIN_DOMAIN_PATH (default) — effectively private to Majik;
|
|
14
|
+
* not the path any generic wallet would derive by default.
|
|
15
|
+
* MAJIK_BITCOIN_STANDARD_PATH (opt-in) — the REAL BIP-84 mainnet path;
|
|
16
|
+
* recoverable in any standard wallet using nothing but the mnemonic.
|
|
17
|
+
*
|
|
18
|
+
* - `privateKey` is the raw 32-byte secp256k1 scalar. `toWIF()` encodes it
|
|
19
|
+
* into Wallet Import Format — the universal paste-in-import string every
|
|
20
|
+
* Bitcoin wallet accepts (base58check, versioned, deterministic).
|
|
21
|
+
* - `@scure/btc-signer` is NOT a hard dependency. It is lazily `import()`-ed
|
|
22
|
+
* only for bech32 address encoding / PSBT construction. If it isn't
|
|
23
|
+
* installed, we throw a clear, actionable MajikKeyError instead of
|
|
24
|
+
* failing module load.
|
|
25
|
+
*/
|
|
26
|
+
import { HDKey } from "@scure/bip32";
|
|
27
|
+
import { schnorr, secp256k1 } from "@noble/curves/secp256k1.js";
|
|
28
|
+
import { MAJIK_BITCOIN_STANDARD_PATH, MAJIK_BITCOIN_DOMAIN_PATH, } from "./constants";
|
|
29
|
+
import { hash } from "@stablelib/sha256";
|
|
30
|
+
import { base58Encode } from "../utils";
|
|
31
|
+
import { MajikKeyError } from "../../error";
|
|
32
|
+
import { randomBytes } from "@noble/hashes/utils.js";
|
|
33
|
+
// ─── Derivation ─────────────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Derive a Bitcoin keypair via standard BIP-32/BIP-84 from the raw 64-byte
|
|
36
|
+
* BIP-39 seed. Call this once at account creation/import time (mirrors
|
|
37
|
+
* ML-KEM/Ed25519/ML-DSA derivation) — the seed itself is never stored, only
|
|
38
|
+
* the resulting key, encrypted at rest like the others.
|
|
39
|
+
*/
|
|
40
|
+
export function deriveBitcoinKeypairFromSeed(seed, options) {
|
|
41
|
+
const path = options?.path ??
|
|
42
|
+
(options?.standard
|
|
43
|
+
? MAJIK_BITCOIN_STANDARD_PATH
|
|
44
|
+
: MAJIK_BITCOIN_DOMAIN_PATH);
|
|
45
|
+
const child = HDKey.fromMasterSeed(seed).derive(path);
|
|
46
|
+
if (!child.privateKey || !child.publicKey) {
|
|
47
|
+
throw new MajikKeyError("Failed to derive Bitcoin keypair from seed");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
privateKey: child.privateKey,
|
|
51
|
+
publicKey: child.publicKey,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Re-derive the public key from a raw private key. Used when unlocking —
|
|
56
|
+
* we only encrypt/store the private key, so the public key is recomputed
|
|
57
|
+
* on unlock rather than stored redundantly encrypted.
|
|
58
|
+
*/
|
|
59
|
+
export function bitcoinPublicKeyFromPrivateKey(privateKey) {
|
|
60
|
+
return secp256k1.getPublicKey(privateKey, true); // compressed
|
|
61
|
+
}
|
|
62
|
+
/** Sign a 32-byte message hash (already hashed — e.g. a Bitcoin sighash). */
|
|
63
|
+
export function signWithBitcoinMaterial(material, messageHash, scheme = "ecdsa") {
|
|
64
|
+
if (scheme === "schnorr") {
|
|
65
|
+
return schnorr.sign(messageHash, material.privateKey, randomBytes(32));
|
|
66
|
+
}
|
|
67
|
+
const signature = secp256k1.sign(messageHash, material.privateKey, {
|
|
68
|
+
prehash: false,
|
|
69
|
+
lowS: true, // Note: lowS is technically default in v2 for secp256k1, but it's good practice to be explicit!
|
|
70
|
+
});
|
|
71
|
+
return signature;
|
|
72
|
+
}
|
|
73
|
+
// ─── WIF export (Wallet Import Format) — no external lib needed ────────────
|
|
74
|
+
const WIF_VERSION_MAINNET = 0x80;
|
|
75
|
+
function doubleSha256(data) {
|
|
76
|
+
return hash(hash(data));
|
|
77
|
+
}
|
|
78
|
+
function base58checkEncode(payload) {
|
|
79
|
+
const checksum = doubleSha256(payload).slice(0, 4);
|
|
80
|
+
const full = new Uint8Array(payload.length + 4);
|
|
81
|
+
full.set(payload, 0);
|
|
82
|
+
full.set(checksum, payload.length);
|
|
83
|
+
return base58Encode(full);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Encode a private key as WIF — the universal paste-in-import string every
|
|
87
|
+
* Bitcoin wallet accepts. Deterministic: same private key → same WIF, always.
|
|
88
|
+
*/
|
|
89
|
+
export function toWIF(material, options) {
|
|
90
|
+
const compressed = options?.compressed ?? true;
|
|
91
|
+
const payload = new Uint8Array(compressed ? 34 : 33);
|
|
92
|
+
payload[0] = WIF_VERSION_MAINNET;
|
|
93
|
+
payload.set(material.privateKey, 1);
|
|
94
|
+
if (compressed)
|
|
95
|
+
payload[33] = 0x01;
|
|
96
|
+
return base58checkEncode(payload);
|
|
97
|
+
}
|
|
98
|
+
let _btcModule = null;
|
|
99
|
+
export async function loadBtcSigner() {
|
|
100
|
+
if (_btcModule)
|
|
101
|
+
return _btcModule;
|
|
102
|
+
try {
|
|
103
|
+
_btcModule = (await import(
|
|
104
|
+
/* webpackIgnore: true */
|
|
105
|
+
/* @vite-ignore */
|
|
106
|
+
"@scure/btc-signer"));
|
|
107
|
+
return _btcModule;
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
throw new MajikKeyError("@scure/btc-signer is required for this operation but is not installed. " +
|
|
111
|
+
"Install it in your project with `npm install @scure/btc-signer` " +
|
|
112
|
+
"(or the yarn/pnpm equivalent) and try again.", err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Native SegWit (bech32, "bc1...") mainnet address for this material's
|
|
117
|
+
* public key. Requires @scure/btc-signer — see loadBtcSigner().
|
|
118
|
+
*/
|
|
119
|
+
export async function toBitcoinAddress(material) {
|
|
120
|
+
const btc = await loadBtcSigner();
|
|
121
|
+
const p2wpkh = btc.p2wpkh(material.publicKey);
|
|
122
|
+
if (!p2wpkh.address) {
|
|
123
|
+
throw new MajikKeyError("Failed to derive Bitcoin address");
|
|
124
|
+
}
|
|
125
|
+
return p2wpkh.address;
|
|
126
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SLIP-44 coin type 0 = Bitcoin mainnet — the path any standard wallet
|
|
2
|
+
// (Electrum, Sparrow, hardware wallets) derives by default from this mnemonic.
|
|
3
|
+
export const MAJIK_BITCOIN_STANDARD_PATH = "m/84'/0'/0'/0/0";
|
|
4
|
+
// Unregistered/private coin-type index — domain-separates Majik's default
|
|
5
|
+
// Bitcoin key from a user's "real" BTC wallet, while remaining 100% standard
|
|
6
|
+
// BIP-32 math (just a different branch of the same tree).
|
|
7
|
+
export const MAJIK_BITCOIN_DOMAIN_PATH = "m/84'/1989'/0'/0/0";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @experimental By default this is Majik's DOMAIN-SEPARATED Bitcoin key
|
|
3
|
+
* (derived via `MAJIK_BITCOIN_DOMAIN_PATH`) — deterministic and fully
|
|
4
|
+
* standard BIP-32, but not the path a generic wallet would derive by
|
|
5
|
+
* default, so it stays effectively private to Majik. Use
|
|
6
|
+
* `MajikKey.getBitcoinKeypairMaterial({ standard: true })` /
|
|
7
|
+
* `getBitcoinWIF({ standard: true })` for the REAL BIP-84 mainnet key —
|
|
8
|
+
* recoverable in any standard wallet from the same mnemonic alone.
|
|
9
|
+
*/
|
|
10
|
+
export interface MajikKeyBitcoinNamespace {
|
|
11
|
+
/** 33-byte compressed secp256k1 public key. */
|
|
12
|
+
readonly publicKey: Uint8Array;
|
|
13
|
+
/** 32-byte secp256k1 private key. Handle with the same care as any private key. */
|
|
14
|
+
readonly privateKey: Uint8Array;
|
|
15
|
+
/** Native SegWit (bech32) address. Lazily loads @scure/btc-signer — throws if not installed. */
|
|
16
|
+
getBitcoinAddress(): Promise<string>;
|
|
17
|
+
/** WIF string — pastes directly into any standard Bitcoin wallet. */
|
|
18
|
+
getWIF(options?: {
|
|
19
|
+
compressed?: boolean;
|
|
20
|
+
}): string;
|
|
21
|
+
/** Sign a 32-byte message hash. ECDSA (default) or Schnorr. */
|
|
22
|
+
sign(messageHash: Uint8Array, scheme?: "ecdsa" | "schnorr"): Uint8Array;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MAJIK_SOLANA_SEED = "MajikKeySolanaSeed";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAJIK_SOLANA_SEED = "MajikKeySolanaSeed";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* solana.ts
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ EXPERIMENTAL — Solana keypair utilities for MajikKey.
|
|
5
|
+
* This module's API may change or be removed without notice in a minor version.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Solana accounts are plain Ed25519 keypairs — no new key material or
|
|
9
|
+
* wallet standard is needed, just bytes in the right shape.
|
|
10
|
+
* - `secretKey` is always the 64-byte nacl/tweetnacl-compatible format
|
|
11
|
+
* (32-byte seed || 32-byte public key) — exactly what
|
|
12
|
+
* `@solana/web3.js`'s `Keypair.fromSecretKey()` expects, and exactly
|
|
13
|
+
* what `@stablelib/ed25519` already produces.
|
|
14
|
+
* - `@solana/web3.js` is NOT a dependency of this package. It is lazily
|
|
15
|
+
* `import()`-ed only when a caller needs an actual `Keypair`/`PublicKey`
|
|
16
|
+
* instance (e.g. to build a transaction). If it isn't installed, we
|
|
17
|
+
* throw a clear, actionable MajikKeyError instead of failing module
|
|
18
|
+
* load. Base58 address derivation does NOT require the library at all.
|
|
19
|
+
*
|
|
20
|
+
* Two ways to obtain a Solana identity from a MajikKey:
|
|
21
|
+
* 1. deriveSolanaKeypairFromEdSecretKey() — RECOMMENDED. Domain-separates
|
|
22
|
+
* a brand-new Ed25519 keypair from the MajikKey's message-signing
|
|
23
|
+
* Ed25519 secret key, so the Solana key is never reused elsewhere.
|
|
24
|
+
* 2. solanaMaterialFromEd25519SecretKey() — reuses the MajikKey's message
|
|
25
|
+
* signing Ed25519 keypair AS-IS. Simpler, but means the same private
|
|
26
|
+
* key secures two different protocols. Opt-in only.
|
|
27
|
+
*/
|
|
28
|
+
export interface SolanaKeypairMaterial {
|
|
29
|
+
/** 32-byte Ed25519 / Solana public key. */
|
|
30
|
+
publicKey: Uint8Array;
|
|
31
|
+
/** 64-byte nacl-format secret key (32-byte seed || 32-byte public key). */
|
|
32
|
+
secretKey: Uint8Array;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Derive a Solana keypair domain-separated from the MajikKey's
|
|
36
|
+
* message-signing Ed25519 key, but fully deterministic from it (and
|
|
37
|
+
* therefore ultimately from the mnemonic).
|
|
38
|
+
*
|
|
39
|
+
* seed' = SHA256(edSecretKey[0..32] || "MajikMessageSolanaSeed")
|
|
40
|
+
*/
|
|
41
|
+
export declare function deriveSolanaKeypairFromEdSecretKey(edSecretKey: Uint8Array): SolanaKeypairMaterial;
|
|
42
|
+
/**
|
|
43
|
+
* Reuse the MajikKey's existing message-signing Ed25519 keypair directly
|
|
44
|
+
* as a Solana keypair (no re-derivation).
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ Not recommended: the same private key would secure both Majik message
|
|
47
|
+
* signing AND any Solana transactions. Prefer
|
|
48
|
+
* `deriveSolanaKeypairFromEdSecretKey()` unless you specifically want the
|
|
49
|
+
* identical key on both.
|
|
50
|
+
*/
|
|
51
|
+
export declare function solanaMaterialFromEd25519SecretKey(edSecretKey: Uint8Array): SolanaKeypairMaterial;
|
|
52
|
+
/**
|
|
53
|
+
* Solana address for a given Solana/Ed25519 public key — just its base58
|
|
54
|
+
* encoding. Does NOT require @solana/web3.js.
|
|
55
|
+
*/
|
|
56
|
+
export declare function solanaAddressFromPublicKey(publicKey: Uint8Array): string;
|
|
57
|
+
type SolanaKitModule = typeof import("@solana/kit");
|
|
58
|
+
export declare function loadSolanaKit(): Promise<SolanaKitModule>;
|
|
59
|
+
/**
|
|
60
|
+
* Real @solana/kit KeyPairSigner backed by a genuine CryptoKeyPair.
|
|
61
|
+
* Requires @solana/kit — see loadSolanaKit().
|
|
62
|
+
*/
|
|
63
|
+
export declare function toSolanaKeyPairSigner(material: SolanaKeypairMaterial): Promise<Awaited<ReturnType<SolanaKitModule["createKeyPairSignerFromBytes"]>>>;
|
|
64
|
+
/**
|
|
65
|
+
* Real @solana/kit Address (branded string) for this material's public key.
|
|
66
|
+
*/
|
|
67
|
+
export declare function toSolanaAddress(material: SolanaKeypairMaterial): Promise<Awaited<ReturnType<SolanaKitModule["getAddressFromPublicKey"]>>>;
|
|
68
|
+
/**
|
|
69
|
+
* Sign an arbitrary message with a Solana keypair's Ed25519 secret key.
|
|
70
|
+
* Uses @stablelib/ed25519 directly — does NOT require @solana/web3.js.
|
|
71
|
+
*/
|
|
72
|
+
export declare function signWithSolanaMaterial(material: SolanaKeypairMaterial, message: Uint8Array): Uint8Array;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* solana.ts
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ EXPERIMENTAL — Solana keypair utilities for MajikKey.
|
|
5
|
+
* This module's API may change or be removed without notice in a minor version.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Solana accounts are plain Ed25519 keypairs — no new key material or
|
|
9
|
+
* wallet standard is needed, just bytes in the right shape.
|
|
10
|
+
* - `secretKey` is always the 64-byte nacl/tweetnacl-compatible format
|
|
11
|
+
* (32-byte seed || 32-byte public key) — exactly what
|
|
12
|
+
* `@solana/web3.js`'s `Keypair.fromSecretKey()` expects, and exactly
|
|
13
|
+
* what `@stablelib/ed25519` already produces.
|
|
14
|
+
* - `@solana/web3.js` is NOT a dependency of this package. It is lazily
|
|
15
|
+
* `import()`-ed only when a caller needs an actual `Keypair`/`PublicKey`
|
|
16
|
+
* instance (e.g. to build a transaction). If it isn't installed, we
|
|
17
|
+
* throw a clear, actionable MajikKeyError instead of failing module
|
|
18
|
+
* load. Base58 address derivation does NOT require the library at all.
|
|
19
|
+
*
|
|
20
|
+
* Two ways to obtain a Solana identity from a MajikKey:
|
|
21
|
+
* 1. deriveSolanaKeypairFromEdSecretKey() — RECOMMENDED. Domain-separates
|
|
22
|
+
* a brand-new Ed25519 keypair from the MajikKey's message-signing
|
|
23
|
+
* Ed25519 secret key, so the Solana key is never reused elsewhere.
|
|
24
|
+
* 2. solanaMaterialFromEd25519SecretKey() — reuses the MajikKey's message
|
|
25
|
+
* signing Ed25519 keypair AS-IS. Simpler, but means the same private
|
|
26
|
+
* key secures two different protocols. Opt-in only.
|
|
27
|
+
*/
|
|
28
|
+
import * as ed25519 from "@stablelib/ed25519";
|
|
29
|
+
import { hash } from "@stablelib/sha256";
|
|
30
|
+
import { MAJIK_SOLANA_SEED } from "./constants";
|
|
31
|
+
import { MajikKeyError } from "../../error";
|
|
32
|
+
import { base58Encode } from "../utils";
|
|
33
|
+
const ED25519_SECRET_KEY_LENGTH = 64;
|
|
34
|
+
const ED25519_SEED_LENGTH = 32;
|
|
35
|
+
// ─── Derivation ─────────────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Derive a Solana keypair domain-separated from the MajikKey's
|
|
38
|
+
* message-signing Ed25519 key, but fully deterministic from it (and
|
|
39
|
+
* therefore ultimately from the mnemonic).
|
|
40
|
+
*
|
|
41
|
+
* seed' = SHA256(edSecretKey[0..32] || "MajikMessageSolanaSeed")
|
|
42
|
+
*/
|
|
43
|
+
export function deriveSolanaKeypairFromEdSecretKey(edSecretKey) {
|
|
44
|
+
if (edSecretKey.length !== ED25519_SECRET_KEY_LENGTH) {
|
|
45
|
+
throw new MajikKeyError(`Expected a 64-byte Ed25519 secret key, got ${edSecretKey.length} bytes`);
|
|
46
|
+
}
|
|
47
|
+
const edSeed = edSecretKey.slice(0, ED25519_SEED_LENGTH);
|
|
48
|
+
const domain = new TextEncoder().encode(MAJIK_SOLANA_SEED);
|
|
49
|
+
const combined = new Uint8Array(edSeed.length + domain.length);
|
|
50
|
+
combined.set(edSeed, 0);
|
|
51
|
+
combined.set(domain, edSeed.length);
|
|
52
|
+
const solanaSeed = hash(combined); // 32 bytes
|
|
53
|
+
const kp = ed25519.generateKeyPairFromSeed(solanaSeed);
|
|
54
|
+
return { publicKey: kp.publicKey, secretKey: kp.secretKey };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reuse the MajikKey's existing message-signing Ed25519 keypair directly
|
|
58
|
+
* as a Solana keypair (no re-derivation).
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ Not recommended: the same private key would secure both Majik message
|
|
61
|
+
* signing AND any Solana transactions. Prefer
|
|
62
|
+
* `deriveSolanaKeypairFromEdSecretKey()` unless you specifically want the
|
|
63
|
+
* identical key on both.
|
|
64
|
+
*/
|
|
65
|
+
export function solanaMaterialFromEd25519SecretKey(edSecretKey) {
|
|
66
|
+
if (edSecretKey.length !== ED25519_SECRET_KEY_LENGTH) {
|
|
67
|
+
throw new MajikKeyError(`Expected a 64-byte Ed25519 secret key, got ${edSecretKey.length} bytes`);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
publicKey: edSecretKey.slice(ED25519_SEED_LENGTH),
|
|
71
|
+
secretKey: edSecretKey.slice(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Solana address for a given Solana/Ed25519 public key — just its base58
|
|
76
|
+
* encoding. Does NOT require @solana/web3.js.
|
|
77
|
+
*/
|
|
78
|
+
export function solanaAddressFromPublicKey(publicKey) {
|
|
79
|
+
return base58Encode(publicKey);
|
|
80
|
+
}
|
|
81
|
+
let _kitModule = null;
|
|
82
|
+
export async function loadSolanaKit() {
|
|
83
|
+
if (_kitModule)
|
|
84
|
+
return _kitModule;
|
|
85
|
+
try {
|
|
86
|
+
_kitModule = (await import(
|
|
87
|
+
/* webpackIgnore: true */
|
|
88
|
+
/* @vite-ignore */
|
|
89
|
+
"@solana/kit"));
|
|
90
|
+
return _kitModule;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
throw new MajikKeyError("@solana/kit is required for this operation but is not installed. " +
|
|
94
|
+
"Install it in your project with `npm install @solana/kit` " +
|
|
95
|
+
"(or the yarn/pnpm equivalent) and try again.", err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Real @solana/kit KeyPairSigner backed by a genuine CryptoKeyPair.
|
|
100
|
+
* Requires @solana/kit — see loadSolanaKit().
|
|
101
|
+
*/
|
|
102
|
+
export async function toSolanaKeyPairSigner(material) {
|
|
103
|
+
const kit = await loadSolanaKit();
|
|
104
|
+
return kit.createKeyPairSignerFromBytes(material.secretKey);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Real @solana/kit Address (branded string) for this material's public key.
|
|
108
|
+
*/
|
|
109
|
+
export async function toSolanaAddress(material) {
|
|
110
|
+
const kit = await loadSolanaKit();
|
|
111
|
+
const signer = await toSolanaKeyPairSigner(material);
|
|
112
|
+
return signer.address;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Sign an arbitrary message with a Solana keypair's Ed25519 secret key.
|
|
116
|
+
* Uses @stablelib/ed25519 directly — does NOT require @solana/web3.js.
|
|
117
|
+
*/
|
|
118
|
+
export function signWithSolanaMaterial(material, message) {
|
|
119
|
+
return ed25519.sign(material.secretKey, message);
|
|
120
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @experimental Web3 / blockchain integrations are experimental. This
|
|
3
|
+
* namespace's shape may change without a major version bump.
|
|
4
|
+
*/
|
|
5
|
+
export interface MajikKeySolanaNamespace {
|
|
6
|
+
/** 32-byte Solana/Ed25519 public key. */
|
|
7
|
+
readonly publicKey: Uint8Array;
|
|
8
|
+
/** 64-byte nacl-format secret key. Handle with the same care as any private key. */
|
|
9
|
+
readonly secretKey: Uint8Array;
|
|
10
|
+
/** Base58 Solana address — does not require @solana/kit. */
|
|
11
|
+
readonly address: string;
|
|
12
|
+
/** Real @solana/kit Keypair. Lazily loads @solana/kit — throws if not installed. */
|
|
13
|
+
getSolanaKeypair(): Promise<any>;
|
|
14
|
+
/** Real @solana/kit PublicKey. Lazily loads @solana/kit — throws if not installed. */
|
|
15
|
+
getSolanaAddress(): Promise<any>;
|
|
16
|
+
/** Sign a message with this Solana keypair's Ed25519 key. No web3.js needed. */
|
|
17
|
+
sign(message: Uint8Array): Uint8Array;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MajikKeyBitcoinNamespace } from "./bitcoin/types";
|
|
2
|
+
import { MajikKeySolanaNamespace } from "./solana/types";
|
|
3
|
+
/** @experimental */
|
|
4
|
+
export interface MajikKeyWeb3Namespace {
|
|
5
|
+
readonly solana: MajikKeySolanaNamespace;
|
|
6
|
+
readonly bitcoin?: MajikKeyBitcoinNamespace;
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function base58Encode(bytes: Uint8Array): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ─── Base58 (Solana address encoding) — no external dependency ─────────────
|
|
2
|
+
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
3
|
+
export function base58Encode(bytes) {
|
|
4
|
+
if (bytes.length === 0)
|
|
5
|
+
return "";
|
|
6
|
+
const digits = [0];
|
|
7
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
8
|
+
let carry = bytes[i];
|
|
9
|
+
for (let j = 0; j < digits.length; j++) {
|
|
10
|
+
carry += digits[j] << 8;
|
|
11
|
+
digits[j] = carry % 58;
|
|
12
|
+
carry = (carry / 58) | 0;
|
|
13
|
+
}
|
|
14
|
+
while (carry > 0) {
|
|
15
|
+
digits.push(carry % 58);
|
|
16
|
+
carry = (carry / 58) | 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let leadingZeros = 0;
|
|
20
|
+
for (let i = 0; i < bytes.length && bytes[i] === 0; i++)
|
|
21
|
+
leadingZeros++;
|
|
22
|
+
let result = "1".repeat(leadingZeros);
|
|
23
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
24
|
+
result += BASE58_ALPHABET[digits[i]];
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED