@hardkas/accounts 0.8.16-alpha → 0.8.19-alpha
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/dist/index.d.ts +3 -1
- package/dist/index.js +805 -760
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -113,9 +113,33 @@ function validateAddressPrefix(address) {
|
|
|
113
113
|
const validPrefixes = ["kaspa:", "kaspatest:", "kaspasim:"];
|
|
114
114
|
const hasValidPrefix = validPrefixes.some((prefix) => address.startsWith(prefix));
|
|
115
115
|
if (!hasValidPrefix) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
const err = new Error(`HARDKAS_INVALID_ADDRESS: Invalid address '${address}'. Must start with one of: ${validPrefixes.join(", ")}`);
|
|
117
|
+
err.code = "HARDKAS_INVALID_ADDRESS";
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function validateAddressNetwork(address, networkId, allowMainnet) {
|
|
122
|
+
if (address.startsWith("kaspa:sim_")) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
validateAddressPrefix(address);
|
|
126
|
+
let expectedPrefix;
|
|
127
|
+
if (networkId === "mainnet") {
|
|
128
|
+
expectedPrefix = "kaspa:";
|
|
129
|
+
} else if (networkId === "testnet-10" || networkId === "testnet-11") {
|
|
130
|
+
expectedPrefix = "kaspatest:";
|
|
131
|
+
} else if (networkId === "simnet" || networkId === "devnet" || networkId === "simulated") {
|
|
132
|
+
expectedPrefix = "kaspasim:";
|
|
133
|
+
} else {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (expectedPrefix !== "kaspa:" && address.startsWith("kaspa:") && allowMainnet) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!address.startsWith(expectedPrefix)) {
|
|
140
|
+
const err = new Error(`NETWORK_ADDRESS_MISMATCH: Address '${address}' does not match the expected prefix '${expectedPrefix}' for network '${networkId}'.`);
|
|
141
|
+
err.code = "NETWORK_ADDRESS_MISMATCH";
|
|
142
|
+
throw err;
|
|
119
143
|
}
|
|
120
144
|
}
|
|
121
145
|
function importRealDevAccount(store, account) {
|
|
@@ -194,7 +218,8 @@ function resolveHardkasAccount(options) {
|
|
|
194
218
|
return {
|
|
195
219
|
name: alias,
|
|
196
220
|
kind: "kaspa-private-key",
|
|
197
|
-
address: keystore.metadata?.address
|
|
221
|
+
address: keystore.metadata?.address,
|
|
222
|
+
keystorePath: devAccountPath
|
|
198
223
|
};
|
|
199
224
|
}
|
|
200
225
|
} catch (e) {
|
|
@@ -259,7 +284,8 @@ function listHardkasAccounts(config) {
|
|
|
259
284
|
accounts.set(name, {
|
|
260
285
|
name,
|
|
261
286
|
kind: "kaspa-private-key",
|
|
262
|
-
address: keystore.payload?.address || keystore.metadata?.address
|
|
287
|
+
address: keystore.payload?.address || keystore.metadata?.address,
|
|
288
|
+
keystorePath: path2.join(devAccountsDir, file)
|
|
263
289
|
});
|
|
264
290
|
}
|
|
265
291
|
} catch (e) {
|
|
@@ -293,8 +319,9 @@ function listHardkasAccounts(config) {
|
|
|
293
319
|
accounts.set(name, {
|
|
294
320
|
name,
|
|
295
321
|
kind: "kaspa-private-key",
|
|
296
|
-
address: keystore.payload?.address || keystore.metadata?.address
|
|
322
|
+
address: keystore.payload?.address || keystore.metadata?.address,
|
|
297
323
|
// Payloads are encrypted, but address might be in metadata
|
|
324
|
+
keystorePath: path2.join(keystoreDir, file)
|
|
298
325
|
});
|
|
299
326
|
}
|
|
300
327
|
} catch (e) {
|
|
@@ -478,319 +505,332 @@ async function getKaspaSigningBackendStatus() {
|
|
|
478
505
|
|
|
479
506
|
// src/kaspa-wasm-signer.ts
|
|
480
507
|
import { calculateContentHash } from "@hardkas/artifacts";
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
508
|
+
|
|
509
|
+
// src/keystore.ts
|
|
510
|
+
import fs3 from "fs";
|
|
511
|
+
import path3 from "path";
|
|
512
|
+
import crypto from "crypto";
|
|
513
|
+
import { argon2id } from "hash-wasm";
|
|
514
|
+
import { writeFileAtomic } from "@hardkas/core";
|
|
515
|
+
var KeystoreManager = class {
|
|
516
|
+
/**
|
|
517
|
+
* Keystore container format version. Separate from ARTIFACT_VERSION.
|
|
518
|
+
* This versions the encrypted keystore envelope, not HardKAS artifacts.
|
|
519
|
+
*/
|
|
520
|
+
static KEYSTORE_FORMAT_VERSION = "2.0.0";
|
|
521
|
+
static KEYSTORE_FORMAT_TYPE = "hardkas.encryptedKeystore.v2";
|
|
522
|
+
/**
|
|
523
|
+
* Creates an encrypted keystore from a payload and password.
|
|
524
|
+
*/
|
|
525
|
+
static async createEncryptedKeystore(payload, password, options) {
|
|
526
|
+
if (!password) throw new Error("Password cannot be empty.");
|
|
527
|
+
if (password.length < 8)
|
|
528
|
+
throw new Error("Password must be at least 8 characters long.");
|
|
529
|
+
const salt = crypto.randomBytes(16);
|
|
530
|
+
const nonce = crypto.randomBytes(12);
|
|
531
|
+
const iterations = options.iterations || 3;
|
|
532
|
+
const memory = options.memory || 65536;
|
|
533
|
+
const parallelism = options.parallelism || 1;
|
|
534
|
+
const derivedKeyHex = await argon2id({
|
|
535
|
+
password,
|
|
536
|
+
salt,
|
|
537
|
+
parallelism,
|
|
538
|
+
iterations,
|
|
539
|
+
memorySize: memory,
|
|
540
|
+
hashLength: 32,
|
|
541
|
+
// 256 bits for AES-256
|
|
542
|
+
outputType: "hex"
|
|
543
|
+
});
|
|
544
|
+
const derivedKey = Buffer.from(derivedKeyHex, "hex");
|
|
545
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", derivedKey, nonce);
|
|
546
|
+
const encryptedPayload = Buffer.concat([
|
|
547
|
+
cipher.update(JSON.stringify(payload), "utf8"),
|
|
548
|
+
cipher.final()
|
|
549
|
+
]);
|
|
550
|
+
const tag = cipher.getAuthTag();
|
|
551
|
+
derivedKey.fill(0);
|
|
552
|
+
return {
|
|
553
|
+
version: this.KEYSTORE_FORMAT_VERSION,
|
|
554
|
+
type: this.KEYSTORE_FORMAT_TYPE,
|
|
555
|
+
kdf: {
|
|
556
|
+
algorithm: "argon2id",
|
|
557
|
+
memory,
|
|
558
|
+
iterations,
|
|
559
|
+
parallelism,
|
|
560
|
+
salt: salt.toString("base64")
|
|
497
561
|
},
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
562
|
+
cipher: {
|
|
563
|
+
algorithm: "aes-256-gcm",
|
|
564
|
+
nonce: nonce.toString("base64"),
|
|
565
|
+
tag: tag.toString("base64")
|
|
566
|
+
},
|
|
567
|
+
encryptedPayload: encryptedPayload.toString("base64"),
|
|
568
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
569
|
+
metadata: {
|
|
570
|
+
label: options.label,
|
|
571
|
+
network: options.network,
|
|
572
|
+
address: payload.address
|
|
507
573
|
}
|
|
508
|
-
}
|
|
509
|
-
lockTime: txInner.lockTime || 0,
|
|
510
|
-
subnetworkId: txInner.subnetworkId || "0000000000000000000000000000000000000000",
|
|
511
|
-
gas: txInner.gas || 0,
|
|
512
|
-
payload: txInner.payload && txInner.payload.length > 0 ? toHex(txInner.payload) : ""
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
var KaspaWasmPrivateKeySigner = class {
|
|
516
|
-
constructor(options) {
|
|
517
|
-
this.options = options;
|
|
574
|
+
};
|
|
518
575
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
allowMainnet: this.options.allowMainnet
|
|
529
|
-
});
|
|
530
|
-
let pkValue = account.privateKeyEnv ? process.env[account.privateKeyEnv] : void 0;
|
|
531
|
-
if (!pkValue && account.privateKey) {
|
|
532
|
-
if (plan.networkId === "mainnet") {
|
|
533
|
-
throw new Error(`Mainnet guard: Unsafe plaintext privateKey fallback is forbidden on mainnet for account '${account.name}'. Use privateKeyEnv instead.`);
|
|
534
|
-
}
|
|
535
|
-
pkValue = account.privateKey;
|
|
536
|
-
}
|
|
537
|
-
if (!pkValue) {
|
|
538
|
-
throw new Error(`Missing required private key for account '${account.name}'.`);
|
|
576
|
+
/**
|
|
577
|
+
* Decrypts an encrypted keystore using a password.
|
|
578
|
+
*/
|
|
579
|
+
static async decryptEncryptedKeystore(keystore, password) {
|
|
580
|
+
if (keystore.version !== this.KEYSTORE_FORMAT_VERSION) {
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: `Unsupported keystore version: ${keystore.version}`
|
|
584
|
+
};
|
|
539
585
|
}
|
|
540
586
|
try {
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
address: plan.from.address,
|
|
554
|
-
outpoint: {
|
|
555
|
-
transactionId: u.outpoint.transactionId,
|
|
556
|
-
index: u.outpoint.index
|
|
557
|
-
},
|
|
558
|
-
utxoEntry: {
|
|
559
|
-
amount: BigInt(u.amountSompi),
|
|
560
|
-
scriptPublicKey: spk,
|
|
561
|
-
blockDaaScore: BigInt(u.blockDaaScore || "0"),
|
|
562
|
-
isCoinbase: !!u.isCoinbase
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
});
|
|
566
|
-
const outputs = plan.outputs.map((o) => {
|
|
567
|
-
if (!o.address) throw new Error("Output is missing address.");
|
|
568
|
-
return {
|
|
569
|
-
address: o.address,
|
|
570
|
-
amount: BigInt(o.amountSompi)
|
|
571
|
-
};
|
|
587
|
+
const salt = Buffer.from(keystore.kdf.salt, "base64");
|
|
588
|
+
const nonce = Buffer.from(keystore.cipher.nonce, "base64");
|
|
589
|
+
const tag = Buffer.from(keystore.cipher.tag, "base64");
|
|
590
|
+
const encryptedData = Buffer.from(keystore.encryptedPayload, "base64");
|
|
591
|
+
const derivedKeyHex = await argon2id({
|
|
592
|
+
password,
|
|
593
|
+
salt,
|
|
594
|
+
parallelism: keystore.kdf.parallelism,
|
|
595
|
+
iterations: keystore.kdf.iterations,
|
|
596
|
+
memorySize: keystore.kdf.memory,
|
|
597
|
+
hashLength: 32,
|
|
598
|
+
outputType: "hex"
|
|
572
599
|
});
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
600
|
+
const derivedKey = Buffer.from(derivedKeyHex, "hex");
|
|
601
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", derivedKey, nonce);
|
|
602
|
+
decipher.setAuthTag(tag);
|
|
603
|
+
const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
|
|
604
|
+
derivedKey.fill(0);
|
|
605
|
+
const payload = JSON.parse(decrypted.toString("utf8"));
|
|
606
|
+
return { success: true, payload };
|
|
607
|
+
} catch (e) {
|
|
608
|
+
return { success: false, error: "Invalid password or corrupted keystore." };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Verifies if the password is correct for the keystore.
|
|
613
|
+
*/
|
|
614
|
+
static async verifyKeystorePassword(keystore, password) {
|
|
615
|
+
const result = await this.decryptEncryptedKeystore(keystore, password);
|
|
616
|
+
return result.success;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Changes the password of an encrypted keystore.
|
|
620
|
+
*/
|
|
621
|
+
static async changeKeystorePassword(keystore, oldPassword, newPassword) {
|
|
622
|
+
const unlock = await this.decryptEncryptedKeystore(keystore, oldPassword);
|
|
623
|
+
if (!unlock.success || !unlock.payload) {
|
|
624
|
+
throw new Error("Invalid current password.");
|
|
625
|
+
}
|
|
626
|
+
return this.createEncryptedKeystore(unlock.payload, newPassword, {
|
|
627
|
+
label: keystore.metadata.label,
|
|
628
|
+
network: keystore.metadata.network,
|
|
629
|
+
iterations: keystore.kdf.iterations,
|
|
630
|
+
memory: keystore.kdf.memory,
|
|
631
|
+
parallelism: keystore.kdf.parallelism
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Loads an encrypted keystore from the filesystem.
|
|
636
|
+
*/
|
|
637
|
+
static async loadEncryptedKeystore(filePath) {
|
|
638
|
+
try {
|
|
639
|
+
const data = await fs3.promises.readFile(filePath, "utf-8");
|
|
640
|
+
const keystore = JSON.parse(data);
|
|
641
|
+
if (keystore.type !== this.KEYSTORE_FORMAT_TYPE) {
|
|
642
|
+
throw new Error(`Invalid keystore type: ${keystore.type}`);
|
|
643
|
+
}
|
|
644
|
+
return keystore;
|
|
645
|
+
} catch (e) {
|
|
597
646
|
throw new Error(
|
|
598
|
-
`
|
|
647
|
+
`Failed to load keystore at ${filePath}: ${e instanceof Error ? e.message : String(e)}`
|
|
599
648
|
);
|
|
600
649
|
}
|
|
601
650
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// src/signer.ts
|
|
613
|
-
var SimulatedTxPlanSigner = class {
|
|
614
|
-
kind = "simulated";
|
|
615
|
-
async signTxPlan(input) {
|
|
616
|
-
const plan = input.planArtifact;
|
|
617
|
-
return {
|
|
618
|
-
signatureKind: "simulated",
|
|
619
|
-
signerAddress: plan.from.address,
|
|
620
|
-
signedTransaction: {
|
|
621
|
-
format: "simulated",
|
|
622
|
-
payload: `simulated-signed-tx:${plan.planId}`
|
|
651
|
+
/**
|
|
652
|
+
* Saves an encrypted keystore to the filesystem.
|
|
653
|
+
*/
|
|
654
|
+
static async saveEncryptedKeystore(filePath, keystore) {
|
|
655
|
+
try {
|
|
656
|
+
const dir = path3.dirname(filePath);
|
|
657
|
+
if (!fs3.existsSync(dir)) {
|
|
658
|
+
await fs3.promises.mkdir(dir, { recursive: true });
|
|
623
659
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
async signTxPlan(_input) {
|
|
630
|
-
throw new Error(
|
|
631
|
-
"Real Kaspa signing requires an official Kaspa transaction signing library. No supported signer backend is configured."
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
};
|
|
635
|
-
async function signTxPlanArtifact(input) {
|
|
636
|
-
const { planArtifact, account } = input;
|
|
637
|
-
const planRecord = planArtifact;
|
|
638
|
-
if (planArtifact.schema === "hardkas.txPlan") {
|
|
639
|
-
} else if (planRecord.status !== "built" && planRecord.status !== "unsigned") {
|
|
640
|
-
throw new Error(`Cannot sign artifact with status: ${planRecord.status}`);
|
|
641
|
-
}
|
|
642
|
-
if (planArtifact.mode === "simulated") {
|
|
643
|
-
if (account.kind !== "simulated") {
|
|
644
|
-
throw new Error(
|
|
645
|
-
`Simulated plans must be signed with simulated accounts (account '${account.name}' is '${account.kind}').`
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
} else {
|
|
649
|
-
if (account.kind === "simulated") {
|
|
660
|
+
await writeFileAtomic(filePath, JSON.stringify(keystore, null, 2), {
|
|
661
|
+
encoding: "utf-8",
|
|
662
|
+
mode: 384
|
|
663
|
+
});
|
|
664
|
+
} catch (e) {
|
|
650
665
|
throw new Error(
|
|
651
|
-
`
|
|
666
|
+
`Failed to save keystore at ${filePath}: ${e instanceof Error ? e.message : String(e)}`
|
|
652
667
|
);
|
|
653
668
|
}
|
|
654
669
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// src/dev-accounts.ts
|
|
673
|
+
import fs4 from "fs";
|
|
674
|
+
import path4 from "path";
|
|
675
|
+
import crypto2 from "crypto";
|
|
676
|
+
import { deterministicCompare } from "@hardkas/core";
|
|
677
|
+
var DEV_ACCOUNTS_PASSWORD = "hardkas-local-dev";
|
|
678
|
+
var SIMNET_DETERMINISTIC_SEED = "hardkas-deterministic-simnet-seed-v1";
|
|
679
|
+
async function ensureDevAccounts(workspaceDir) {
|
|
680
|
+
const devAccountsDir = path4.join(workspaceDir, ".hardkas", "dev-accounts");
|
|
681
|
+
if (!fs4.existsSync(devAccountsDir)) {
|
|
682
|
+
await fs4.promises.mkdir(devAccountsDir, { recursive: true });
|
|
659
683
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
684
|
+
await getOrCreateDevAccount(workspaceDir, 0, "alice");
|
|
685
|
+
await getOrCreateDevAccount(workspaceDir, 1, "bob");
|
|
686
|
+
}
|
|
687
|
+
async function getOrCreateDevAccount(workspaceDir, index, alias) {
|
|
688
|
+
const devAccountsDir = path4.join(workspaceDir, ".hardkas", "dev-accounts");
|
|
689
|
+
const filePath = path4.join(devAccountsDir, `${alias}.json`);
|
|
690
|
+
if (fs4.existsSync(filePath)) {
|
|
691
|
+
const keystore2 = await KeystoreManager.loadEncryptedKeystore(filePath);
|
|
692
|
+
const unlock = await KeystoreManager.decryptEncryptedKeystore(
|
|
693
|
+
keystore2,
|
|
694
|
+
DEV_ACCOUNTS_PASSWORD
|
|
665
695
|
);
|
|
666
|
-
|
|
667
|
-
if (account.kind === "kaspa-private-key") {
|
|
668
|
-
const status = await getKaspaSigningBackendStatus();
|
|
669
|
-
if (!status.available) {
|
|
696
|
+
if (!unlock.success || !unlock.payload) {
|
|
670
697
|
throw new Error(
|
|
671
|
-
`
|
|
698
|
+
`Failed to decrypt dev account ${alias}. Expected password: ${DEV_ACCOUNTS_PASSWORD}`
|
|
672
699
|
);
|
|
673
700
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const result = await signer.signTxPlan({
|
|
679
|
-
planArtifact,
|
|
680
|
-
accountName: account.name
|
|
681
|
-
});
|
|
682
|
-
const artifact = {
|
|
683
|
-
schema: "hardkas.signedTx",
|
|
684
|
-
hardkasVersion: HARDKAS_VERSION2,
|
|
685
|
-
version: "1.0.0-alpha",
|
|
686
|
-
status: "signed",
|
|
687
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
688
|
-
txId: result.txId || "",
|
|
689
|
-
// Ensure txId is present
|
|
690
|
-
sourcePlanId: planArtifact.planId,
|
|
691
|
-
networkId: planArtifact.networkId,
|
|
692
|
-
mode: planArtifact.mode,
|
|
693
|
-
from: { address: planArtifact.from.address },
|
|
694
|
-
to: { address: planArtifact.to.address },
|
|
695
|
-
amountSompi: planArtifact.amountSompi,
|
|
696
|
-
signedTransaction: {
|
|
697
|
-
format: result.signedTransaction?.format === "hex" ? "hex" : "unknown",
|
|
698
|
-
payload: result.signedTransaction?.payload || ""
|
|
699
|
-
},
|
|
700
|
-
lineage: createLineageTransition(planArtifact, "hardkas.signedTx"),
|
|
701
|
-
...planArtifact.workflowId ? { workflowId: planArtifact.workflowId } : {},
|
|
702
|
-
...planArtifact.assumptionLevel ? { assumptionLevel: planArtifact.assumptionLevel } : {},
|
|
703
|
-
...planArtifact.policyRefs ? { policyRefs: planArtifact.policyRefs } : {},
|
|
704
|
-
...planArtifact.networkProfileRef ? { networkProfileRef: planArtifact.networkProfileRef } : {},
|
|
705
|
-
...planArtifact.assumptionRef ? { assumptionRef: planArtifact.assumptionRef } : {}
|
|
701
|
+
return {
|
|
702
|
+
address: unlock.payload.address,
|
|
703
|
+
privateKey: unlock.payload.privateKey,
|
|
704
|
+
publicKey: unlock.payload.publicKey
|
|
706
705
|
};
|
|
707
|
-
const contentHash = calculateContentHash2(artifact);
|
|
708
|
-
artifact.signedId = `signed-${contentHash.slice(0, 16)}`;
|
|
709
|
-
artifact.contentHash = contentHash;
|
|
710
|
-
if (artifact.lineage) {
|
|
711
|
-
artifact.lineage.artifactId = contentHash;
|
|
712
|
-
}
|
|
713
|
-
return artifact;
|
|
714
|
-
}
|
|
715
|
-
if (account.kind === "external-wallet") {
|
|
716
|
-
throw new Error("External wallet signing is not implemented yet.");
|
|
717
706
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
var KaspaSdkKeyGenerator = class {
|
|
729
|
-
networkId;
|
|
730
|
-
sdkLoader;
|
|
731
|
-
constructor(options) {
|
|
732
|
-
this.networkId = options?.networkId || "simnet";
|
|
733
|
-
const rawLoader = options?.sdkLoader || (async () => {
|
|
734
|
-
return await import("kaspa-wasm");
|
|
735
|
-
});
|
|
736
|
-
this.sdkLoader = async () => {
|
|
707
|
+
const seedString = `${SIMNET_DETERMINISTIC_SEED}-${index}`;
|
|
708
|
+
const privateKeyHex = crypto2.createHash("sha256").update(seedString).digest("hex");
|
|
709
|
+
const network = "simnet";
|
|
710
|
+
const isSimnet = ["simnet", "kaspasim", "local"].includes(network);
|
|
711
|
+
let address = "";
|
|
712
|
+
let privateKey = "";
|
|
713
|
+
let publicKey = "";
|
|
714
|
+
try {
|
|
715
|
+
if (isSimnet) {
|
|
716
|
+
let kaspaWasm;
|
|
737
717
|
try {
|
|
738
|
-
|
|
718
|
+
kaspaWasm = await import(
|
|
719
|
+
/* @vite-ignore */
|
|
720
|
+
"kaspa-wasm"
|
|
721
|
+
);
|
|
739
722
|
} catch (e) {
|
|
740
|
-
|
|
741
|
-
|
|
723
|
+
console.warn(`
|
|
724
|
+
[Warning] kaspa-wasm is not installed. Required for simnet.`);
|
|
725
|
+
return { address: "", privateKey: "", publicKey: "" };
|
|
726
|
+
}
|
|
727
|
+
const privKey = new kaspaWasm.PrivateKey(privateKeyHex);
|
|
728
|
+
const kp = privKey.toKeypair();
|
|
729
|
+
address = kp.toAddress(network).toString();
|
|
730
|
+
publicKey = kp.publicKey;
|
|
731
|
+
privateKey = kp.privateKey;
|
|
732
|
+
} else {
|
|
733
|
+
let sdkModule;
|
|
734
|
+
try {
|
|
735
|
+
sdkModule = await import(
|
|
736
|
+
/* @vite-ignore */
|
|
737
|
+
"@kaspa/core-lib"
|
|
742
738
|
);
|
|
743
|
-
|
|
744
|
-
|
|
739
|
+
} catch (e) {
|
|
740
|
+
console.warn(`
|
|
741
|
+
[Warning] @kaspa/core-lib is not installed.`);
|
|
742
|
+
return { address: "", privateKey: "", publicKey: "" };
|
|
745
743
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const sdk = await this.sdkLoader();
|
|
750
|
-
const network = options?.networkId || this.networkId;
|
|
751
|
-
try {
|
|
752
|
-
if (typeof sdk.PrivateKey === "function") {
|
|
753
|
-
const crypto3 = await import("crypto");
|
|
754
|
-
const randomBytes = crypto3.randomBytes(32);
|
|
755
|
-
const hex = randomBytes.toString("hex");
|
|
756
|
-
const privKey = new sdk.PrivateKey(hex);
|
|
757
|
-
const kp = privKey.toKeypair();
|
|
758
|
-
const address = kp.toAddress(network).toString();
|
|
759
|
-
const privateKeyStr = kp.privateKey;
|
|
760
|
-
const publicKeyStr = kp.publicKey;
|
|
761
|
-
return {
|
|
762
|
-
address,
|
|
763
|
-
publicKey: publicKeyStr,
|
|
764
|
-
privateKey: privateKeyStr
|
|
765
|
-
};
|
|
744
|
+
const sdk = sdkModule.default || sdkModule;
|
|
745
|
+
if (typeof sdk.initRuntime === "function") {
|
|
746
|
+
await sdk.initRuntime();
|
|
766
747
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
748
|
+
const privKey = new sdk.PrivateKey(privateKeyHex);
|
|
749
|
+
const pubKey = privKey.toPublicKey();
|
|
750
|
+
try {
|
|
751
|
+
address = pubKey.toAddress(network).toString();
|
|
752
|
+
} catch (e) {
|
|
753
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
754
|
+
if (msg.includes("Second argument must be") || msg.includes("Unsupported")) {
|
|
755
|
+
const err = new Error("DEV_ACCOUNT_BACKEND_UNSUPPORTED_NETWORK");
|
|
756
|
+
err.code = "DEV_ACCOUNT_BACKEND_UNSUPPORTED_NETWORK";
|
|
757
|
+
throw err;
|
|
758
|
+
}
|
|
759
|
+
throw e;
|
|
760
|
+
}
|
|
761
|
+
publicKey = pubKey.toString();
|
|
762
|
+
privateKey = privKey.toString();
|
|
763
|
+
}
|
|
764
|
+
} catch (e) {
|
|
765
|
+
if (e.message === "DEV_ACCOUNT_BACKEND_UNSUPPORTED_NETWORK") {
|
|
766
|
+
throw e;
|
|
774
767
|
}
|
|
768
|
+
console.warn(`
|
|
769
|
+
[Warning] Could not generate dev account '${alias}'.
|
|
770
|
+
${e.message}`);
|
|
771
|
+
return { address: "", privateKey: "", publicKey: "" };
|
|
775
772
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
773
|
+
const accountData = {
|
|
774
|
+
address,
|
|
775
|
+
privateKey,
|
|
776
|
+
publicKey
|
|
777
|
+
};
|
|
778
|
+
if (!fs4.existsSync(devAccountsDir)) {
|
|
779
|
+
await fs4.promises.mkdir(devAccountsDir, { recursive: true });
|
|
780
|
+
}
|
|
781
|
+
const payload = {
|
|
782
|
+
address: accountData.address,
|
|
783
|
+
privateKey: accountData.privateKey,
|
|
784
|
+
network: "simnet"
|
|
785
|
+
};
|
|
786
|
+
if (accountData.publicKey) {
|
|
787
|
+
payload.publicKey = accountData.publicKey;
|
|
788
|
+
}
|
|
789
|
+
const keystore = await KeystoreManager.createEncryptedKeystore(
|
|
790
|
+
payload,
|
|
791
|
+
DEV_ACCOUNTS_PASSWORD,
|
|
792
|
+
{
|
|
793
|
+
label: alias,
|
|
794
|
+
network: "simnet"
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
await KeystoreManager.saveEncryptedKeystore(filePath, keystore);
|
|
798
|
+
return accountData;
|
|
799
|
+
}
|
|
800
|
+
function listDevAccountsSync(workspaceDir) {
|
|
801
|
+
const devAccountsDir = path4.join(workspaceDir, ".hardkas", "dev-accounts");
|
|
802
|
+
if (!fs4.existsSync(devAccountsDir)) {
|
|
803
|
+
return [];
|
|
804
|
+
}
|
|
805
|
+
const accounts = [];
|
|
806
|
+
const files = fs4.readdirSync(devAccountsDir);
|
|
807
|
+
for (const file of files) {
|
|
808
|
+
if (file.endsWith(".json")) {
|
|
809
|
+
const name = path4.basename(file, ".json");
|
|
810
|
+
try {
|
|
811
|
+
const data = fs4.readFileSync(path4.join(devAccountsDir, file), "utf-8");
|
|
812
|
+
const keystore = JSON.parse(data);
|
|
813
|
+
if (keystore.type === "hardkas.encryptedKeystore.v2") {
|
|
814
|
+
accounts.push({
|
|
815
|
+
name,
|
|
816
|
+
address: keystore.metadata?.address || ""
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
} catch (e) {
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
accounts.sort((a, b) => deterministicCompare(a.name, b.name));
|
|
824
|
+
return accounts;
|
|
784
825
|
}
|
|
785
826
|
|
|
786
|
-
// src/
|
|
787
|
-
|
|
788
|
-
function toHex2(arr) {
|
|
827
|
+
// src/kaspa-wasm-signer.ts
|
|
828
|
+
function toHex(arr) {
|
|
789
829
|
return Buffer.from(arr).toString("hex");
|
|
790
830
|
}
|
|
791
|
-
function
|
|
831
|
+
function parseWasmTxToRpc(wasmTxStr) {
|
|
792
832
|
let parsed = JSON.parse(wasmTxStr);
|
|
793
|
-
|
|
833
|
+
if (typeof parsed === "string") {
|
|
794
834
|
parsed = JSON.parse(parsed);
|
|
795
835
|
}
|
|
796
836
|
const txInner = parsed.tx ? parsed.tx.inner : parsed.inner;
|
|
@@ -802,204 +842,97 @@ function parseWasmTxToRpc2(wasmTxStr) {
|
|
|
802
842
|
transactionId: i.inner.previousOutpoint.inner.transactionId,
|
|
803
843
|
index: i.inner.previousOutpoint.inner.index
|
|
804
844
|
},
|
|
805
|
-
signatureScript:
|
|
845
|
+
signatureScript: toHex(i.inner.signatureScript),
|
|
806
846
|
sequence: i.inner.sequence || 0,
|
|
807
847
|
sigOpCount: i.inner.sigOpCount || 1
|
|
808
848
|
})),
|
|
809
849
|
outputs: (txInner.outputs || []).map((o) => ({
|
|
810
|
-
|
|
850
|
+
amount: o.inner.value.toString(),
|
|
811
851
|
scriptPublicKey: {
|
|
812
852
|
version: parseInt(o.inner.scriptPublicKey.substring(0, 4), 16) || 0,
|
|
813
|
-
|
|
853
|
+
scriptPublicKey: o.inner.scriptPublicKey.substring(4)
|
|
814
854
|
}
|
|
815
855
|
})),
|
|
816
856
|
lockTime: txInner.lockTime || 0,
|
|
817
857
|
subnetworkId: txInner.subnetworkId || "0000000000000000000000000000000000000000",
|
|
818
858
|
gas: txInner.gas || 0,
|
|
819
|
-
payload: txInner.payload && txInner.payload.length > 0 ?
|
|
820
|
-
mass: txInner.mass || 0
|
|
859
|
+
payload: txInner.payload && txInner.payload.length > 0 ? toHex(txInner.payload) : ""
|
|
821
860
|
};
|
|
822
861
|
}
|
|
823
|
-
var
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
FIXTURE_PK = "b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef";
|
|
827
|
-
constructor(networkId = "simnet") {
|
|
828
|
-
this.networkId = networkId;
|
|
829
|
-
if (networkId === "mainnet") {
|
|
830
|
-
throw new Error("FixtureSigner cannot be used on mainnet.");
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
async loadKaspa() {
|
|
834
|
-
try {
|
|
835
|
-
return await import("kaspa-wasm");
|
|
836
|
-
} catch (e) {
|
|
837
|
-
const err = new Error("SIGNER_BACKEND_UNAVAILABLE: Official Kaspa WASM backend is required to sign transactions.\nInstall it via: npm install kaspa-wasm");
|
|
838
|
-
err.code = "SIGNER_BACKEND_UNAVAILABLE";
|
|
839
|
-
throw err;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
async getAddress() {
|
|
843
|
-
const kaspa = await this.loadKaspa();
|
|
844
|
-
const privKey = new kaspa.PrivateKey(this.FIXTURE_PK);
|
|
845
|
-
return privKey.toKeypair().toAddress(this.networkId).toString();
|
|
862
|
+
var KaspaWasmPrivateKeySigner = class {
|
|
863
|
+
constructor(options) {
|
|
864
|
+
this.options = options;
|
|
846
865
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
const spk = u.scriptPublicKey;
|
|
858
|
-
if (!spk) {
|
|
859
|
-
throw new Error("UTXO is missing scriptPublicKey. Real signing flows must never fabricate cryptographic state.");
|
|
860
|
-
}
|
|
861
|
-
return {
|
|
862
|
-
address: plan.from.address,
|
|
863
|
-
outpoint: {
|
|
864
|
-
transactionId: u.outpoint.transactionId,
|
|
865
|
-
index: u.outpoint.index
|
|
866
|
-
},
|
|
867
|
-
utxoEntry: {
|
|
868
|
-
amount: BigInt(u.amountSompi),
|
|
869
|
-
scriptPublicKey: spk,
|
|
870
|
-
blockDaaScore: BigInt(u.blockDaaScore || "0"),
|
|
871
|
-
isCoinbase: !!u.isCoinbase
|
|
872
|
-
}
|
|
873
|
-
};
|
|
874
|
-
});
|
|
875
|
-
const outputs = plan.outputs.map((o) => {
|
|
876
|
-
if (!o.address) throw new Error("Output is missing address.");
|
|
877
|
-
return {
|
|
878
|
-
address: o.address,
|
|
879
|
-
amount: BigInt(o.amountSompi)
|
|
880
|
-
};
|
|
866
|
+
options;
|
|
867
|
+
kind = "kaspa-private-key";
|
|
868
|
+
async signTxPlan(input) {
|
|
869
|
+
const plan = input.planArtifact;
|
|
870
|
+
const account = this.options.account;
|
|
871
|
+
const sdk = await loadKaspaWasm();
|
|
872
|
+
assertSigningNetworkAllowed({
|
|
873
|
+
network: plan.networkId,
|
|
874
|
+
mode: plan.mode,
|
|
875
|
+
allowMainnet: this.options.allowMainnet
|
|
881
876
|
});
|
|
882
|
-
let
|
|
883
|
-
if (
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
877
|
+
let pkValue = account.privateKeyEnv ? process.env[account.privateKeyEnv] : void 0;
|
|
878
|
+
if (!pkValue && account.privateKey) {
|
|
879
|
+
if (plan.networkId === "mainnet") {
|
|
880
|
+
throw new Error(`Mainnet guard: Unsafe plaintext privateKey fallback is forbidden on mainnet for account '${account.name}'. Use privateKeyEnv instead.`);
|
|
881
|
+
}
|
|
882
|
+
pkValue = account.privateKey;
|
|
887
883
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const rawTx = JSON.stringify(parseWasmTxToRpc2(signedTx.toString()));
|
|
898
|
-
const draft = {
|
|
899
|
-
schema: "hardkas.signedTx",
|
|
900
|
-
schemaVersion: "hardkas.artifact.v1",
|
|
901
|
-
hardkasVersion: HARDKAS_VERSION3,
|
|
902
|
-
version: ARTIFACT_VERSION2,
|
|
903
|
-
hashVersion: CURRENT_HASH_VERSION,
|
|
904
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
905
|
-
status: "signed",
|
|
906
|
-
txId: signedTx.id,
|
|
907
|
-
sourcePlanId: plan.planId,
|
|
908
|
-
networkId: plan.networkId,
|
|
909
|
-
mode: plan.mode,
|
|
910
|
-
from: plan.from,
|
|
911
|
-
to: plan.to,
|
|
912
|
-
amountSompi: plan.amountSompi,
|
|
913
|
-
unsignedPayloadHash: plan.contentHash,
|
|
914
|
-
signedTransaction: {
|
|
915
|
-
format: "hex",
|
|
916
|
-
payload: rawTx
|
|
917
|
-
},
|
|
918
|
-
metadata: {
|
|
919
|
-
signerBackend: "kaspa-wasm",
|
|
920
|
-
fixture: true,
|
|
921
|
-
networkGuard: "mainnet_rejected"
|
|
922
|
-
},
|
|
923
|
-
signatureMetadata: [
|
|
924
|
-
{
|
|
925
|
-
signer: "hardkas-local-docker-test-only",
|
|
926
|
-
signedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
884
|
+
if (!pkValue && account.keystorePath) {
|
|
885
|
+
try {
|
|
886
|
+
const keystore = await KeystoreManager.loadEncryptedKeystore(account.keystorePath);
|
|
887
|
+
const unlock = await KeystoreManager.decryptEncryptedKeystore(
|
|
888
|
+
keystore,
|
|
889
|
+
DEV_ACCOUNTS_PASSWORD
|
|
890
|
+
);
|
|
891
|
+
if (unlock.success && unlock.payload) {
|
|
892
|
+
pkValue = unlock.payload.privateKey;
|
|
927
893
|
}
|
|
928
|
-
|
|
929
|
-
lineage: {
|
|
930
|
-
artifactId: "",
|
|
931
|
-
lineageId: plan.lineage?.lineageId || plan.contentHash || "0".repeat(64),
|
|
932
|
-
parentArtifactId: plan.contentHash || plan.planId,
|
|
933
|
-
rootArtifactId: plan.lineage?.rootArtifactId || plan.contentHash || plan.planId
|
|
894
|
+
} catch (e) {
|
|
934
895
|
}
|
|
935
|
-
};
|
|
936
|
-
const hash = calculateContentHash3(draft, CURRENT_HASH_VERSION);
|
|
937
|
-
draft.signedId = `signed-${hash.slice(0, 16)}`;
|
|
938
|
-
draft.contentHash = hash;
|
|
939
|
-
if (draft.lineage) draft.lineage.artifactId = hash;
|
|
940
|
-
return draft;
|
|
941
|
-
}
|
|
942
|
-
};
|
|
943
|
-
|
|
944
|
-
// src/real-signer.ts
|
|
945
|
-
var UnsupportedRealTxSigner = class {
|
|
946
|
-
async sign() {
|
|
947
|
-
throw new Error(
|
|
948
|
-
"Real transaction signing is not configured yet. Install/configure a supported Kaspa SDK signer adapter."
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
};
|
|
952
|
-
|
|
953
|
-
// src/kaspa-sdk-real-signer.ts
|
|
954
|
-
var KaspaSdkRealTxSigner = class {
|
|
955
|
-
sdkLoader;
|
|
956
|
-
constructor(options) {
|
|
957
|
-
this.sdkLoader = options?.sdkLoader || loadKaspaWasm;
|
|
958
|
-
}
|
|
959
|
-
async sign(input) {
|
|
960
|
-
const { plan, account } = input;
|
|
961
|
-
let sdk;
|
|
962
|
-
try {
|
|
963
|
-
sdk = await this.sdkLoader();
|
|
964
|
-
} catch (e) {
|
|
965
|
-
throw new Error(
|
|
966
|
-
"Kaspa SDK real transaction signer dependency is not installed. Install/configure the supported Kaspa WASM SDK adapter."
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
if (!sdk) {
|
|
970
|
-
throw new Error(
|
|
971
|
-
"Kaspa SDK real transaction signer dependency is not installed. Install/configure the supported Kaspa WASM SDK adapter."
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
if (!account.privateKey) {
|
|
975
|
-
throw new Error("Account has no private key available for signing.");
|
|
976
896
|
}
|
|
977
|
-
if (
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
897
|
+
if (!pkValue) {
|
|
898
|
+
const err = new Error(`DEV_ACCOUNT_KEY_UNAVAILABLE: Missing required private key for account '${account.name}'.`);
|
|
899
|
+
err.code = "DEV_ACCOUNT_KEY_UNAVAILABLE";
|
|
900
|
+
throw err;
|
|
981
901
|
}
|
|
982
902
|
try {
|
|
983
|
-
const privateKey = new sdk.PrivateKey(
|
|
903
|
+
const privateKey = new sdk.PrivateKey(pkValue);
|
|
984
904
|
const utxos = plan.inputs.map((u) => {
|
|
985
|
-
if (!u.
|
|
986
|
-
throw new Error(
|
|
987
|
-
`UTXO ${u.outpoint.transactionId}:${u.outpoint.index} is missing scriptPublicKey required for signing.`
|
|
988
|
-
);
|
|
905
|
+
if (!u.outpoint.transactionId || u.outpoint.index === void 0) {
|
|
906
|
+
throw new Error(`UTXO is missing transactionId or index. Re-run tx plan.`);
|
|
989
907
|
}
|
|
990
908
|
const spk = u.scriptPublicKey;
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
909
|
+
if (!spk) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
"UTXO is missing scriptPublicKey. Real signing flows must never fabricate cryptographic state."
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
address: plan.from.address,
|
|
916
|
+
outpoint: {
|
|
917
|
+
transactionId: u.outpoint.transactionId,
|
|
918
|
+
index: u.outpoint.index
|
|
919
|
+
},
|
|
920
|
+
utxoEntry: {
|
|
921
|
+
amount: BigInt(u.amountSompi),
|
|
922
|
+
scriptPublicKey: spk,
|
|
923
|
+
blockDaaScore: BigInt(u.blockDaaScore || "0"),
|
|
924
|
+
isCoinbase: !!u.isCoinbase
|
|
925
|
+
}
|
|
926
|
+
};
|
|
998
927
|
});
|
|
999
|
-
const outputs =
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
928
|
+
const outputs = plan.outputs.map((o) => {
|
|
929
|
+
if (!o.address) throw new Error("Output is missing address.");
|
|
930
|
+
return {
|
|
931
|
+
address: o.address,
|
|
932
|
+
amount: BigInt(o.amountSompi)
|
|
933
|
+
};
|
|
934
|
+
});
|
|
935
|
+
const changeAddress = plan.change?.address ? new sdk.Address(plan.change.address) : void 0;
|
|
1003
936
|
const priorityFee = BigInt(plan.estimatedFeeSompi);
|
|
1004
937
|
const unsignedTx = sdk.createTransaction(
|
|
1005
938
|
utxos,
|
|
@@ -1008,353 +941,464 @@ var KaspaSdkRealTxSigner = class {
|
|
|
1008
941
|
priorityFee
|
|
1009
942
|
);
|
|
1010
943
|
const signedTx = sdk.signTransaction(unsignedTx, [privateKey], true);
|
|
1011
|
-
const
|
|
1012
|
-
const txId = signedTx.id;
|
|
944
|
+
const rawTx = JSON.stringify(parseWasmTxToRpc(signedTx.toString()));
|
|
1013
945
|
return {
|
|
946
|
+
signatureKind: "kaspa-private-key",
|
|
947
|
+
signerAddress: account.address || privateKey.toAddress(plan.networkId).toString(),
|
|
1014
948
|
signedTransaction: {
|
|
1015
|
-
format: "
|
|
1016
|
-
payload
|
|
949
|
+
format: "hex",
|
|
950
|
+
payload: rawTx
|
|
1017
951
|
},
|
|
1018
|
-
txId
|
|
952
|
+
txId: signedTx.id,
|
|
953
|
+
signature: {
|
|
954
|
+
// We use the txid as the signature identifier in the artifact
|
|
955
|
+
value: signedTx.id || calculateContentHash(plan)
|
|
956
|
+
}
|
|
1019
957
|
};
|
|
1020
|
-
} catch (
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
`Kaspa SDK signer adapter could not find required transaction signing primitives: ${msg}`
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1027
|
-
throw new Error(`Real transaction signing failed in Kaspa SDK: ${msg}`);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
throw new Error(
|
|
960
|
+
`Kaspa WASM signing failed: ${error instanceof Error ? error.message : String(error)}`
|
|
961
|
+
);
|
|
1028
962
|
}
|
|
1029
963
|
}
|
|
1030
964
|
};
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
async generateAccount() {
|
|
965
|
+
function assertSigningNetworkAllowed(input) {
|
|
966
|
+
const isMainnet = input.network === "mainnet";
|
|
967
|
+
if (isMainnet && !input.allowMainnet) {
|
|
1035
968
|
throw new Error(
|
|
1036
|
-
"
|
|
969
|
+
"Mainnet signing is disabled by default. Use --allow-mainnet-signing only if you understand the risks."
|
|
1037
970
|
);
|
|
1038
971
|
}
|
|
1039
|
-
}
|
|
972
|
+
}
|
|
1040
973
|
|
|
1041
|
-
// src/
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
import { writeFileAtomic } from "@hardkas/core";
|
|
1047
|
-
var KeystoreManager = class {
|
|
1048
|
-
/**
|
|
1049
|
-
* Keystore container format version. Separate from ARTIFACT_VERSION.
|
|
1050
|
-
* This versions the encrypted keystore envelope, not HardKAS artifacts.
|
|
1051
|
-
*/
|
|
1052
|
-
static KEYSTORE_FORMAT_VERSION = "2.0.0";
|
|
1053
|
-
static KEYSTORE_FORMAT_TYPE = "hardkas.encryptedKeystore.v2";
|
|
1054
|
-
/**
|
|
1055
|
-
* Creates an encrypted keystore from a payload and password.
|
|
1056
|
-
*/
|
|
1057
|
-
static async createEncryptedKeystore(payload, password, options) {
|
|
1058
|
-
if (!password) throw new Error("Password cannot be empty.");
|
|
1059
|
-
if (password.length < 8)
|
|
1060
|
-
throw new Error("Password must be at least 8 characters long.");
|
|
1061
|
-
const salt = crypto.randomBytes(16);
|
|
1062
|
-
const nonce = crypto.randomBytes(12);
|
|
1063
|
-
const iterations = options.iterations || 3;
|
|
1064
|
-
const memory = options.memory || 65536;
|
|
1065
|
-
const parallelism = options.parallelism || 1;
|
|
1066
|
-
const derivedKeyHex = await argon2id({
|
|
1067
|
-
password,
|
|
1068
|
-
salt,
|
|
1069
|
-
parallelism,
|
|
1070
|
-
iterations,
|
|
1071
|
-
memorySize: memory,
|
|
1072
|
-
hashLength: 32,
|
|
1073
|
-
// 256 bits for AES-256
|
|
1074
|
-
outputType: "hex"
|
|
1075
|
-
});
|
|
1076
|
-
const derivedKey = Buffer.from(derivedKeyHex, "hex");
|
|
1077
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", derivedKey, nonce);
|
|
1078
|
-
const encryptedPayload = Buffer.concat([
|
|
1079
|
-
cipher.update(JSON.stringify(payload), "utf8"),
|
|
1080
|
-
cipher.final()
|
|
1081
|
-
]);
|
|
1082
|
-
const tag = cipher.getAuthTag();
|
|
1083
|
-
derivedKey.fill(0);
|
|
974
|
+
// src/signer.ts
|
|
975
|
+
var SimulatedTxPlanSigner = class {
|
|
976
|
+
kind = "simulated";
|
|
977
|
+
async signTxPlan(input) {
|
|
978
|
+
const plan = input.planArtifact;
|
|
1084
979
|
return {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
iterations,
|
|
1091
|
-
parallelism,
|
|
1092
|
-
salt: salt.toString("base64")
|
|
1093
|
-
},
|
|
1094
|
-
cipher: {
|
|
1095
|
-
algorithm: "aes-256-gcm",
|
|
1096
|
-
nonce: nonce.toString("base64"),
|
|
1097
|
-
tag: tag.toString("base64")
|
|
1098
|
-
},
|
|
1099
|
-
encryptedPayload: encryptedPayload.toString("base64"),
|
|
1100
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1101
|
-
metadata: {
|
|
1102
|
-
label: options.label,
|
|
1103
|
-
network: options.network,
|
|
1104
|
-
address: payload.address
|
|
980
|
+
signatureKind: "simulated",
|
|
981
|
+
signerAddress: plan.from.address,
|
|
982
|
+
signedTransaction: {
|
|
983
|
+
format: "simulated",
|
|
984
|
+
payload: `simulated-signed-tx:${plan.planId}`
|
|
1105
985
|
}
|
|
1106
986
|
};
|
|
1107
987
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
error: `Unsupported keystore version: ${keystore.version}`
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
try {
|
|
1119
|
-
const salt = Buffer.from(keystore.kdf.salt, "base64");
|
|
1120
|
-
const nonce = Buffer.from(keystore.cipher.nonce, "base64");
|
|
1121
|
-
const tag = Buffer.from(keystore.cipher.tag, "base64");
|
|
1122
|
-
const encryptedData = Buffer.from(keystore.encryptedPayload, "base64");
|
|
1123
|
-
const derivedKeyHex = await argon2id({
|
|
1124
|
-
password,
|
|
1125
|
-
salt,
|
|
1126
|
-
parallelism: keystore.kdf.parallelism,
|
|
1127
|
-
iterations: keystore.kdf.iterations,
|
|
1128
|
-
memorySize: keystore.kdf.memory,
|
|
1129
|
-
hashLength: 32,
|
|
1130
|
-
outputType: "hex"
|
|
1131
|
-
});
|
|
1132
|
-
const derivedKey = Buffer.from(derivedKeyHex, "hex");
|
|
1133
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", derivedKey, nonce);
|
|
1134
|
-
decipher.setAuthTag(tag);
|
|
1135
|
-
const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
|
|
1136
|
-
derivedKey.fill(0);
|
|
1137
|
-
const payload = JSON.parse(decrypted.toString("utf8"));
|
|
1138
|
-
return { success: true, payload };
|
|
1139
|
-
} catch (e) {
|
|
1140
|
-
return { success: false, error: "Invalid password or corrupted keystore." };
|
|
1141
|
-
}
|
|
988
|
+
};
|
|
989
|
+
var UnsupportedRealKaspaSigner = class {
|
|
990
|
+
kind = "unsupported";
|
|
991
|
+
async signTxPlan(_input) {
|
|
992
|
+
throw new Error(
|
|
993
|
+
"Real Kaspa signing requires an official Kaspa transaction signing library. No supported signer backend is configured."
|
|
994
|
+
);
|
|
1142
995
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
996
|
+
};
|
|
997
|
+
async function signTxPlanArtifact(input) {
|
|
998
|
+
const { planArtifact, account } = input;
|
|
999
|
+
const planRecord = planArtifact;
|
|
1000
|
+
if (planArtifact.schema === "hardkas.txPlan") {
|
|
1001
|
+
} else if (planRecord.status !== "built" && planRecord.status !== "unsigned") {
|
|
1002
|
+
throw new Error(`Cannot sign artifact with status: ${planRecord.status}`);
|
|
1149
1003
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
if (!unlock.success || !unlock.payload) {
|
|
1156
|
-
throw new Error("Invalid current password.");
|
|
1004
|
+
if (planArtifact.mode === "simulated") {
|
|
1005
|
+
if (account.kind !== "simulated") {
|
|
1006
|
+
throw new Error(
|
|
1007
|
+
`Simulated plans must be signed with simulated accounts (account '${account.name}' is '${account.kind}').`
|
|
1008
|
+
);
|
|
1157
1009
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
network: keystore.metadata.network,
|
|
1161
|
-
iterations: keystore.kdf.iterations,
|
|
1162
|
-
memory: keystore.kdf.memory,
|
|
1163
|
-
parallelism: keystore.kdf.parallelism
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
/**
|
|
1167
|
-
* Loads an encrypted keystore from the filesystem.
|
|
1168
|
-
*/
|
|
1169
|
-
static async loadEncryptedKeystore(filePath) {
|
|
1170
|
-
try {
|
|
1171
|
-
const data = await fs3.promises.readFile(filePath, "utf-8");
|
|
1172
|
-
const keystore = JSON.parse(data);
|
|
1173
|
-
if (keystore.type !== this.KEYSTORE_FORMAT_TYPE) {
|
|
1174
|
-
throw new Error(`Invalid keystore type: ${keystore.type}`);
|
|
1175
|
-
}
|
|
1176
|
-
return keystore;
|
|
1177
|
-
} catch (e) {
|
|
1010
|
+
} else {
|
|
1011
|
+
if (account.kind === "simulated") {
|
|
1178
1012
|
throw new Error(
|
|
1179
|
-
`
|
|
1013
|
+
`Real Kaspa transaction plans (mode: ${planArtifact.mode}) cannot be signed with simulated accounts.`
|
|
1180
1014
|
);
|
|
1181
1015
|
}
|
|
1182
1016
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
try {
|
|
1188
|
-
const dir = path3.dirname(filePath);
|
|
1189
|
-
if (!fs3.existsSync(dir)) {
|
|
1190
|
-
await fs3.promises.mkdir(dir, { recursive: true });
|
|
1191
|
-
}
|
|
1192
|
-
await writeFileAtomic(filePath, JSON.stringify(keystore, null, 2), {
|
|
1193
|
-
encoding: "utf-8",
|
|
1194
|
-
mode: 384
|
|
1195
|
-
});
|
|
1196
|
-
} catch (e) {
|
|
1197
|
-
throw new Error(
|
|
1198
|
-
`Failed to save keystore at ${filePath}: ${e instanceof Error ? e.message : String(e)}`
|
|
1199
|
-
);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
};
|
|
1203
|
-
|
|
1204
|
-
// src/dev-accounts.ts
|
|
1205
|
-
import fs4 from "fs";
|
|
1206
|
-
import path4 from "path";
|
|
1207
|
-
import crypto2 from "crypto";
|
|
1208
|
-
import { deterministicCompare } from "@hardkas/core";
|
|
1209
|
-
var DEV_ACCOUNTS_PASSWORD = "hardkas-local-dev";
|
|
1210
|
-
var SIMNET_DETERMINISTIC_SEED = "hardkas-deterministic-simnet-seed-v1";
|
|
1211
|
-
async function ensureDevAccounts(workspaceDir) {
|
|
1212
|
-
const devAccountsDir = path4.join(workspaceDir, ".hardkas", "dev-accounts");
|
|
1213
|
-
if (!fs4.existsSync(devAccountsDir)) {
|
|
1214
|
-
await fs4.promises.mkdir(devAccountsDir, { recursive: true });
|
|
1017
|
+
if (planArtifact.networkId === "mainnet" && !input.allowMainnet) {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
"Mainnet signing is disabled by default. Use --allow-mainnet-signing only if you understand the risks."
|
|
1020
|
+
);
|
|
1215
1021
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
const filePath = path4.join(devAccountsDir, `${alias}.json`);
|
|
1222
|
-
if (fs4.existsSync(filePath)) {
|
|
1223
|
-
const keystore2 = await KeystoreManager.loadEncryptedKeystore(filePath);
|
|
1224
|
-
const unlock = await KeystoreManager.decryptEncryptedKeystore(
|
|
1225
|
-
keystore2,
|
|
1226
|
-
DEV_ACCOUNTS_PASSWORD
|
|
1022
|
+
if (account.kind === "simulated") {
|
|
1023
|
+
return createSimulatedSignedTxArtifact(
|
|
1024
|
+
planArtifact,
|
|
1025
|
+
`simulated-signed-tx:${planArtifact.planId}`,
|
|
1026
|
+
systemRuntimeContext
|
|
1227
1027
|
);
|
|
1228
|
-
|
|
1028
|
+
}
|
|
1029
|
+
if (account.kind === "kaspa-private-key") {
|
|
1030
|
+
const status = await getKaspaSigningBackendStatus();
|
|
1031
|
+
if (!status.available) {
|
|
1229
1032
|
throw new Error(
|
|
1230
|
-
`
|
|
1033
|
+
`Real Kaspa signing is not available: ${status.error || "Unknown error"}. Ensure 'kaspa' package is installed.`
|
|
1231
1034
|
);
|
|
1232
1035
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1036
|
+
const signer = new KaspaWasmPrivateKeySigner({
|
|
1037
|
+
account,
|
|
1038
|
+
allowMainnet: input.allowMainnet
|
|
1039
|
+
});
|
|
1040
|
+
const result = await signer.signTxPlan({
|
|
1041
|
+
planArtifact,
|
|
1042
|
+
accountName: account.name
|
|
1043
|
+
});
|
|
1044
|
+
const artifact = {
|
|
1045
|
+
schema: "hardkas.signedTx",
|
|
1046
|
+
hardkasVersion: HARDKAS_VERSION2,
|
|
1047
|
+
version: "1.0.0-alpha",
|
|
1048
|
+
status: "signed",
|
|
1049
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1050
|
+
txId: result.txId || "",
|
|
1051
|
+
// Ensure txId is present
|
|
1052
|
+
sourcePlanId: planArtifact.planId,
|
|
1053
|
+
networkId: planArtifact.networkId,
|
|
1054
|
+
mode: planArtifact.mode,
|
|
1055
|
+
from: { address: planArtifact.from.address },
|
|
1056
|
+
to: { address: planArtifact.to.address },
|
|
1057
|
+
amountSompi: planArtifact.amountSompi,
|
|
1058
|
+
signedTransaction: {
|
|
1059
|
+
format: result.signedTransaction?.format === "hex" ? "hex" : "unknown",
|
|
1060
|
+
payload: result.signedTransaction?.payload || ""
|
|
1061
|
+
},
|
|
1062
|
+
lineage: createLineageTransition(planArtifact, "hardkas.signedTx"),
|
|
1063
|
+
...planArtifact.workflowId ? { workflowId: planArtifact.workflowId } : {},
|
|
1064
|
+
...planArtifact.assumptionLevel ? { assumptionLevel: planArtifact.assumptionLevel } : {},
|
|
1065
|
+
...planArtifact.policyRefs ? { policyRefs: planArtifact.policyRefs } : {},
|
|
1066
|
+
...planArtifact.networkProfileRef ? { networkProfileRef: planArtifact.networkProfileRef } : {},
|
|
1067
|
+
...planArtifact.assumptionRef ? { assumptionRef: planArtifact.assumptionRef } : {}
|
|
1237
1068
|
};
|
|
1069
|
+
const contentHash = calculateContentHash2(artifact);
|
|
1070
|
+
artifact.signedId = `signed-${contentHash.slice(0, 16)}`;
|
|
1071
|
+
artifact.contentHash = contentHash;
|
|
1072
|
+
if (artifact.lineage) {
|
|
1073
|
+
artifact.lineage.artifactId = contentHash;
|
|
1074
|
+
}
|
|
1075
|
+
return artifact;
|
|
1238
1076
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1077
|
+
if (account.kind === "external-wallet") {
|
|
1078
|
+
throw new Error("External wallet signing is not implemented yet.");
|
|
1079
|
+
}
|
|
1080
|
+
if (account.kind === "evm-private-key") {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
"EVM accounts are reserved for future Igra support and cannot sign Kaspa L1 transactions."
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const accountRecord = account;
|
|
1086
|
+
throw new Error(`Unsupported account kind for signing: ${accountRecord.kind}`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/kaspa-sdk-keygen.ts
|
|
1090
|
+
var KaspaSdkKeyGenerator = class {
|
|
1091
|
+
networkId;
|
|
1092
|
+
sdkLoader;
|
|
1093
|
+
constructor(options) {
|
|
1094
|
+
this.networkId = options?.networkId || "simnet";
|
|
1095
|
+
const rawLoader = options?.sdkLoader || (async () => {
|
|
1096
|
+
return await import("kaspa-wasm");
|
|
1097
|
+
});
|
|
1098
|
+
this.sdkLoader = async () => {
|
|
1249
1099
|
try {
|
|
1250
|
-
|
|
1251
|
-
/* @vite-ignore */
|
|
1252
|
-
"kaspa-wasm"
|
|
1253
|
-
);
|
|
1100
|
+
return await rawLoader();
|
|
1254
1101
|
} catch (e) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
return { address: "", privateKey: "", publicKey: "" };
|
|
1258
|
-
}
|
|
1259
|
-
const privKey = new kaspaWasm.PrivateKey(privateKeyHex);
|
|
1260
|
-
const kp = privKey.toKeypair();
|
|
1261
|
-
address = kp.toAddress(network).toString();
|
|
1262
|
-
publicKey = kp.publicKey;
|
|
1263
|
-
privateKey = kp.privateKey;
|
|
1264
|
-
} else {
|
|
1265
|
-
let sdkModule;
|
|
1266
|
-
try {
|
|
1267
|
-
sdkModule = await import(
|
|
1268
|
-
/* @vite-ignore */
|
|
1269
|
-
"@kaspa/core-lib"
|
|
1102
|
+
const err = new Error(
|
|
1103
|
+
"WALLET_BACKEND_UNAVAILABLE: Kaspa cryptography adapter missing. Real account generation requires WASM execution.\nUse 'hardkas accounts real import' to add a test fixture manually for now."
|
|
1270
1104
|
);
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
[Warning] @kaspa/core-lib is not installed.`);
|
|
1274
|
-
return { address: "", privateKey: "", publicKey: "" };
|
|
1275
|
-
}
|
|
1276
|
-
const sdk = sdkModule.default || sdkModule;
|
|
1277
|
-
if (typeof sdk.initRuntime === "function") {
|
|
1278
|
-
await sdk.initRuntime();
|
|
1105
|
+
err.code = "WALLET_BACKEND_UNAVAILABLE";
|
|
1106
|
+
throw err;
|
|
1279
1107
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
async generateAccount(options) {
|
|
1111
|
+
const sdk = await this.sdkLoader();
|
|
1112
|
+
const network = options?.networkId || this.networkId;
|
|
1113
|
+
try {
|
|
1114
|
+
if (typeof sdk.PrivateKey === "function") {
|
|
1115
|
+
const crypto3 = await import("crypto");
|
|
1116
|
+
const randomBytes = crypto3.randomBytes(32);
|
|
1117
|
+
const hex = randomBytes.toString("hex");
|
|
1118
|
+
const privKey = new sdk.PrivateKey(hex);
|
|
1119
|
+
const kp = privKey.toKeypair();
|
|
1120
|
+
const address = kp.toAddress(network).toString();
|
|
1121
|
+
const privateKeyStr = kp.privateKey;
|
|
1122
|
+
const publicKeyStr = kp.publicKey;
|
|
1123
|
+
return {
|
|
1124
|
+
address,
|
|
1125
|
+
publicKey: publicKeyStr,
|
|
1126
|
+
privateKey: privateKeyStr
|
|
1127
|
+
};
|
|
1292
1128
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
"Loaded Kaspa SDK does not expose expected PrivateKey constructor."
|
|
1131
|
+
);
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
throw new Error(
|
|
1134
|
+
`Failed to generate account using SDK: ${e instanceof Error ? e.message : String(e)}`
|
|
1135
|
+
);
|
|
1299
1136
|
}
|
|
1300
|
-
console.warn(`
|
|
1301
|
-
[Warning] Could not generate dev account '${alias}'.
|
|
1302
|
-
${e.message}`);
|
|
1303
|
-
return { address: "", privateKey: "", publicKey: "" };
|
|
1304
1137
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
// src/kaspa-wallet.ts
|
|
1141
|
+
async function createLocalKaspaWallet(options) {
|
|
1142
|
+
const keygen = new KaspaSdkKeyGenerator({
|
|
1143
|
+
networkId: options?.networkId || "simnet"
|
|
1144
|
+
});
|
|
1145
|
+
return await keygen.generateAccount();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// src/fixture-signer.ts
|
|
1149
|
+
import { calculateContentHash as calculateContentHash3, HARDKAS_VERSION as HARDKAS_VERSION3, ARTIFACT_VERSION as ARTIFACT_VERSION2, CURRENT_HASH_VERSION } from "@hardkas/artifacts";
|
|
1150
|
+
function toHex2(arr) {
|
|
1151
|
+
return Buffer.from(arr).toString("hex");
|
|
1152
|
+
}
|
|
1153
|
+
function parseWasmTxToRpc2(wasmTxStr) {
|
|
1154
|
+
let parsed = JSON.parse(wasmTxStr);
|
|
1155
|
+
while (typeof parsed === "string") {
|
|
1156
|
+
parsed = JSON.parse(parsed);
|
|
1312
1157
|
}
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1158
|
+
const txInner = parsed.tx ? parsed.tx.inner : parsed.inner;
|
|
1159
|
+
if (!txInner) throw new Error("Could not find inner tx data");
|
|
1160
|
+
return {
|
|
1161
|
+
version: txInner.version || 0,
|
|
1162
|
+
inputs: (txInner.inputs || []).map((i) => ({
|
|
1163
|
+
previousOutpoint: {
|
|
1164
|
+
transactionId: i.inner.previousOutpoint.inner.transactionId,
|
|
1165
|
+
index: i.inner.previousOutpoint.inner.index
|
|
1166
|
+
},
|
|
1167
|
+
signatureScript: toHex2(i.inner.signatureScript),
|
|
1168
|
+
sequence: i.inner.sequence || 0,
|
|
1169
|
+
sigOpCount: i.inner.sigOpCount || 1
|
|
1170
|
+
})),
|
|
1171
|
+
outputs: (txInner.outputs || []).map((o) => ({
|
|
1172
|
+
value: o.inner.value,
|
|
1173
|
+
scriptPublicKey: {
|
|
1174
|
+
version: parseInt(o.inner.scriptPublicKey.substring(0, 4), 16) || 0,
|
|
1175
|
+
script: o.inner.scriptPublicKey.substring(4)
|
|
1176
|
+
}
|
|
1177
|
+
})),
|
|
1178
|
+
lockTime: txInner.lockTime || 0,
|
|
1179
|
+
subnetworkId: txInner.subnetworkId || "0000000000000000000000000000000000000000",
|
|
1180
|
+
gas: txInner.gas || 0,
|
|
1181
|
+
payload: txInner.payload && txInner.payload.length > 0 ? toHex2(txInner.payload) : "",
|
|
1182
|
+
mass: txInner.mass || 0
|
|
1317
1183
|
};
|
|
1318
|
-
|
|
1319
|
-
|
|
1184
|
+
}
|
|
1185
|
+
var HardkasFixtureSigner = class {
|
|
1186
|
+
networkId;
|
|
1187
|
+
// A deterministic, known private key exclusively for Docker tests.
|
|
1188
|
+
FIXTURE_PK = "b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef";
|
|
1189
|
+
constructor(networkId = "simnet") {
|
|
1190
|
+
this.networkId = networkId;
|
|
1191
|
+
if (networkId === "mainnet") {
|
|
1192
|
+
throw new Error("FixtureSigner cannot be used on mainnet.");
|
|
1193
|
+
}
|
|
1320
1194
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
{
|
|
1325
|
-
|
|
1326
|
-
|
|
1195
|
+
async loadKaspa() {
|
|
1196
|
+
try {
|
|
1197
|
+
return await import("kaspa-wasm");
|
|
1198
|
+
} catch (e) {
|
|
1199
|
+
const err = new Error("SIGNER_BACKEND_UNAVAILABLE: Official Kaspa WASM backend is required to sign transactions.\nInstall it via: npm install kaspa-wasm");
|
|
1200
|
+
err.code = "SIGNER_BACKEND_UNAVAILABLE";
|
|
1201
|
+
throw err;
|
|
1327
1202
|
}
|
|
1328
|
-
);
|
|
1329
|
-
await KeystoreManager.saveEncryptedKeystore(filePath, keystore);
|
|
1330
|
-
return accountData;
|
|
1331
|
-
}
|
|
1332
|
-
function listDevAccountsSync(workspaceDir) {
|
|
1333
|
-
const devAccountsDir = path4.join(workspaceDir, ".hardkas", "dev-accounts");
|
|
1334
|
-
if (!fs4.existsSync(devAccountsDir)) {
|
|
1335
|
-
return [];
|
|
1336
1203
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1204
|
+
async getAddress() {
|
|
1205
|
+
const kaspa = await this.loadKaspa();
|
|
1206
|
+
const privKey = new kaspa.PrivateKey(this.FIXTURE_PK);
|
|
1207
|
+
return privKey.toKeypair().toAddress(this.networkId).toString();
|
|
1208
|
+
}
|
|
1209
|
+
async signTransaction(plan) {
|
|
1210
|
+
if (plan.networkId === "mainnet") {
|
|
1211
|
+
throw new Error("FixtureSigner refuses to sign mainnet transactions.");
|
|
1212
|
+
}
|
|
1213
|
+
const kaspa = await this.loadKaspa();
|
|
1214
|
+
const privateKey = new kaspa.PrivateKey(this.FIXTURE_PK);
|
|
1215
|
+
const utxos = plan.inputs.map((u) => {
|
|
1216
|
+
if (!u.outpoint.transactionId || u.outpoint.index === void 0) {
|
|
1217
|
+
throw new Error(`UTXO is missing transactionId or index. Re-run tx plan.`);
|
|
1218
|
+
}
|
|
1219
|
+
const spk = u.scriptPublicKey;
|
|
1220
|
+
if (!spk) {
|
|
1221
|
+
throw new Error("UTXO is missing scriptPublicKey. Real signing flows must never fabricate cryptographic state.");
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
address: plan.from.address,
|
|
1225
|
+
outpoint: {
|
|
1226
|
+
transactionId: u.outpoint.transactionId,
|
|
1227
|
+
index: u.outpoint.index
|
|
1228
|
+
},
|
|
1229
|
+
utxoEntry: {
|
|
1230
|
+
amount: BigInt(u.amountSompi),
|
|
1231
|
+
scriptPublicKey: spk,
|
|
1232
|
+
blockDaaScore: BigInt(u.blockDaaScore || "0"),
|
|
1233
|
+
isCoinbase: !!u.isCoinbase
|
|
1350
1234
|
}
|
|
1351
|
-
}
|
|
1235
|
+
};
|
|
1236
|
+
});
|
|
1237
|
+
const outputs = plan.outputs.map((o) => {
|
|
1238
|
+
if (!o.address) throw new Error("Output is missing address.");
|
|
1239
|
+
return {
|
|
1240
|
+
address: o.address,
|
|
1241
|
+
amount: BigInt(o.amountSompi)
|
|
1242
|
+
};
|
|
1243
|
+
});
|
|
1244
|
+
let changeAddress;
|
|
1245
|
+
if (plan.change && plan.change.address) {
|
|
1246
|
+
changeAddress = new kaspa.Address(plan.change.address);
|
|
1247
|
+
} else {
|
|
1248
|
+
changeAddress = new kaspa.Address(plan.from.address);
|
|
1249
|
+
}
|
|
1250
|
+
const priorityFee = BigInt(plan.estimatedFeeSompi || "0");
|
|
1251
|
+
const unsignedTx = kaspa.createTransaction(
|
|
1252
|
+
utxos,
|
|
1253
|
+
outputs,
|
|
1254
|
+
changeAddress,
|
|
1255
|
+
priorityFee
|
|
1256
|
+
);
|
|
1257
|
+
const signedTx = kaspa.signTransaction(unsignedTx, [privateKey], true);
|
|
1258
|
+
console.log("SIGNED TX TOSTRING:", signedTx.toString());
|
|
1259
|
+
const rawTx = JSON.stringify(parseWasmTxToRpc2(signedTx.toString()));
|
|
1260
|
+
const draft = {
|
|
1261
|
+
schema: "hardkas.signedTx",
|
|
1262
|
+
schemaVersion: "hardkas.artifact.v1",
|
|
1263
|
+
hardkasVersion: HARDKAS_VERSION3,
|
|
1264
|
+
version: ARTIFACT_VERSION2,
|
|
1265
|
+
hashVersion: CURRENT_HASH_VERSION,
|
|
1266
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1267
|
+
status: "signed",
|
|
1268
|
+
txId: signedTx.id,
|
|
1269
|
+
sourcePlanId: plan.planId,
|
|
1270
|
+
networkId: plan.networkId,
|
|
1271
|
+
mode: plan.mode,
|
|
1272
|
+
from: plan.from,
|
|
1273
|
+
to: plan.to,
|
|
1274
|
+
amountSompi: plan.amountSompi,
|
|
1275
|
+
unsignedPayloadHash: plan.contentHash,
|
|
1276
|
+
signedTransaction: {
|
|
1277
|
+
format: "hex",
|
|
1278
|
+
payload: rawTx
|
|
1279
|
+
},
|
|
1280
|
+
metadata: {
|
|
1281
|
+
signerBackend: "kaspa-wasm",
|
|
1282
|
+
fixture: true,
|
|
1283
|
+
networkGuard: "mainnet_rejected"
|
|
1284
|
+
},
|
|
1285
|
+
signatureMetadata: [
|
|
1286
|
+
{
|
|
1287
|
+
signer: "hardkas-local-docker-test-only",
|
|
1288
|
+
signedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1289
|
+
}
|
|
1290
|
+
],
|
|
1291
|
+
lineage: {
|
|
1292
|
+
artifactId: "",
|
|
1293
|
+
lineageId: plan.lineage?.lineageId || plan.contentHash || "0".repeat(64),
|
|
1294
|
+
parentArtifactId: plan.contentHash || plan.planId,
|
|
1295
|
+
rootArtifactId: plan.lineage?.rootArtifactId || plan.contentHash || plan.planId
|
|
1352
1296
|
}
|
|
1297
|
+
};
|
|
1298
|
+
const hash = calculateContentHash3(draft, CURRENT_HASH_VERSION);
|
|
1299
|
+
draft.signedId = `signed-${hash.slice(0, 16)}`;
|
|
1300
|
+
draft.contentHash = hash;
|
|
1301
|
+
if (draft.lineage) draft.lineage.artifactId = hash;
|
|
1302
|
+
return draft;
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
// src/real-signer.ts
|
|
1307
|
+
var UnsupportedRealTxSigner = class {
|
|
1308
|
+
async sign() {
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
"Real transaction signing is not configured yet. Install/configure a supported Kaspa SDK signer adapter."
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
// src/kaspa-sdk-real-signer.ts
|
|
1316
|
+
var KaspaSdkRealTxSigner = class {
|
|
1317
|
+
sdkLoader;
|
|
1318
|
+
constructor(options) {
|
|
1319
|
+
this.sdkLoader = options?.sdkLoader || loadKaspaWasm;
|
|
1320
|
+
}
|
|
1321
|
+
async sign(input) {
|
|
1322
|
+
const { plan, account } = input;
|
|
1323
|
+
let sdk;
|
|
1324
|
+
try {
|
|
1325
|
+
sdk = await this.sdkLoader();
|
|
1326
|
+
} catch (e) {
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
"Kaspa SDK real transaction signer dependency is not installed. Install/configure the supported Kaspa WASM SDK adapter."
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
if (!sdk) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
"Kaspa SDK real transaction signer dependency is not installed. Install/configure the supported Kaspa WASM SDK adapter."
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (!account.privateKey) {
|
|
1337
|
+
throw new Error("Account has no private key available for signing.");
|
|
1338
|
+
}
|
|
1339
|
+
if (plan.from.address !== account.address) {
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
`Address mismatch: Plan requires ${plan.from.address}, but account has ${account.address}.`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
try {
|
|
1345
|
+
const privateKey = new sdk.PrivateKey(account.privateKey);
|
|
1346
|
+
const utxos = plan.inputs.map((u) => {
|
|
1347
|
+
if (!u.scriptPublicKey) {
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
`UTXO ${u.outpoint.transactionId}:${u.outpoint.index} is missing scriptPublicKey required for signing.`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
const spk = u.scriptPublicKey;
|
|
1353
|
+
return new sdk.UtxoEntry(
|
|
1354
|
+
BigInt(u.amountSompi),
|
|
1355
|
+
spk,
|
|
1356
|
+
u.outpoint.transactionId,
|
|
1357
|
+
u.outpoint.index,
|
|
1358
|
+
plan.from.address
|
|
1359
|
+
);
|
|
1360
|
+
});
|
|
1361
|
+
const outputs = [
|
|
1362
|
+
new sdk.PaymentOutput(new sdk.Address(plan.to.address), BigInt(plan.amountSompi))
|
|
1363
|
+
];
|
|
1364
|
+
const changeAddress = plan.change ? new sdk.Address(plan.change.address) : void 0;
|
|
1365
|
+
const priorityFee = BigInt(plan.estimatedFeeSompi);
|
|
1366
|
+
const unsignedTx = sdk.createTransaction(
|
|
1367
|
+
utxos,
|
|
1368
|
+
outputs,
|
|
1369
|
+
changeAddress,
|
|
1370
|
+
priorityFee
|
|
1371
|
+
);
|
|
1372
|
+
const signedTx = sdk.signTransaction(unsignedTx, [privateKey], true);
|
|
1373
|
+
const payload = signedTx.serialize ? signedTx.serialize() : JSON.stringify(signedTx.toRpcTransaction());
|
|
1374
|
+
const txId = signedTx.id;
|
|
1375
|
+
return {
|
|
1376
|
+
signedTransaction: {
|
|
1377
|
+
format: "kaspa-sdk",
|
|
1378
|
+
payload
|
|
1379
|
+
},
|
|
1380
|
+
txId
|
|
1381
|
+
};
|
|
1382
|
+
} catch (e) {
|
|
1383
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1384
|
+
if (msg.includes("is not a constructor") || msg.includes("is not a function")) {
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
`Kaspa SDK signer adapter could not find required transaction signing primitives: ${msg}`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
throw new Error(`Real transaction signing failed in Kaspa SDK: ${msg}`);
|
|
1353
1390
|
}
|
|
1354
1391
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
// src/real-keygen.ts
|
|
1395
|
+
var UnsupportedKaspaKeyGenerator = class {
|
|
1396
|
+
async generateAccount() {
|
|
1397
|
+
throw new Error(
|
|
1398
|
+
"Real Kaspa key generation is not configured. Install/configure a supported Kaspa SDK adapter."
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1358
1402
|
export {
|
|
1359
1403
|
DEV_ACCOUNTS_PASSWORD,
|
|
1360
1404
|
HardkasFixtureSigner,
|
|
@@ -1393,5 +1437,6 @@ export {
|
|
|
1393
1437
|
saveRealAccountStore,
|
|
1394
1438
|
signTxPlanArtifact,
|
|
1395
1439
|
validateAccountName,
|
|
1440
|
+
validateAddressNetwork,
|
|
1396
1441
|
validateAddressPrefix
|
|
1397
1442
|
};
|