@hardkas/accounts 0.8.16-alpha → 0.8.18-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.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
- throw new Error(
117
- `Invalid address '${address}'. Must start with one of: ${validPrefixes.join(", ")}`
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
- function toHex(arr) {
482
- return Buffer.from(arr).toString("hex");
483
- }
484
- function parseWasmTxToRpc(wasmTxStr) {
485
- let parsed = JSON.parse(wasmTxStr);
486
- if (typeof parsed === "string") {
487
- parsed = JSON.parse(parsed);
488
- }
489
- const txInner = parsed.tx ? parsed.tx.inner : parsed.inner;
490
- if (!txInner) throw new Error("Could not find inner tx data");
491
- return {
492
- version: txInner.version || 0,
493
- inputs: (txInner.inputs || []).map((i) => ({
494
- previousOutpoint: {
495
- transactionId: i.inner.previousOutpoint.inner.transactionId,
496
- index: i.inner.previousOutpoint.inner.index
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
- signatureScript: toHex(i.inner.signatureScript),
499
- sequence: i.inner.sequence || 0,
500
- sigOpCount: i.inner.sigOpCount || 1
501
- })),
502
- outputs: (txInner.outputs || []).map((o) => ({
503
- amount: o.inner.value.toString(),
504
- scriptPublicKey: {
505
- version: parseInt(o.inner.scriptPublicKey.substring(0, 4), 16) || 0,
506
- scriptPublicKey: o.inner.scriptPublicKey.substring(4)
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
- options;
520
- kind = "kaspa-private-key";
521
- async signTxPlan(input) {
522
- const plan = input.planArtifact;
523
- const account = this.options.account;
524
- const sdk = await loadKaspaWasm();
525
- assertSigningNetworkAllowed({
526
- network: plan.networkId,
527
- mode: plan.mode,
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 privateKey = new sdk.PrivateKey(pkValue);
542
- const utxos = plan.inputs.map((u) => {
543
- if (!u.outpoint.transactionId || u.outpoint.index === void 0) {
544
- throw new Error(`UTXO is missing transactionId or index. Re-run tx plan.`);
545
- }
546
- const spk = u.scriptPublicKey;
547
- if (!spk) {
548
- throw new Error(
549
- "UTXO is missing scriptPublicKey. Real signing flows must never fabricate cryptographic state."
550
- );
551
- }
552
- return {
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 changeAddress = plan.change?.address ? new sdk.Address(plan.change.address) : void 0;
574
- const priorityFee = BigInt(plan.estimatedFeeSompi);
575
- const unsignedTx = sdk.createTransaction(
576
- utxos,
577
- outputs,
578
- changeAddress,
579
- priorityFee
580
- );
581
- const signedTx = sdk.signTransaction(unsignedTx, [privateKey], true);
582
- const rawTx = JSON.stringify(parseWasmTxToRpc(signedTx.toString()));
583
- return {
584
- signatureKind: "kaspa-private-key",
585
- signerAddress: account.address || privateKey.toAddress(plan.networkId).toString(),
586
- signedTransaction: {
587
- format: "hex",
588
- payload: rawTx
589
- },
590
- txId: signedTx.id,
591
- signature: {
592
- // We use the txid as the signature identifier in the artifact
593
- value: signedTx.id || calculateContentHash(plan)
594
- }
595
- };
596
- } catch (error) {
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
- `Kaspa WASM signing failed: ${error instanceof Error ? error.message : String(error)}`
647
+ `Failed to load keystore at ${filePath}: ${e instanceof Error ? e.message : String(e)}`
599
648
  );
600
649
  }
601
650
  }
602
- };
603
- function assertSigningNetworkAllowed(input) {
604
- const isMainnet = input.network === "mainnet";
605
- if (isMainnet && !input.allowMainnet) {
606
- throw new Error(
607
- "Mainnet signing is disabled by default. Use --allow-mainnet-signing only if you understand the risks."
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
- var UnsupportedRealKaspaSigner = class {
628
- kind = "unsupported";
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
- `Real Kaspa transaction plans (mode: ${planArtifact.mode}) cannot be signed with simulated accounts.`
666
+ `Failed to save keystore at ${filePath}: ${e instanceof Error ? e.message : String(e)}`
652
667
  );
653
668
  }
654
669
  }
655
- if (planArtifact.networkId === "mainnet" && !input.allowMainnet) {
656
- throw new Error(
657
- "Mainnet signing is disabled by default. Use --allow-mainnet-signing only if you understand the risks."
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
- if (account.kind === "simulated") {
661
- return createSimulatedSignedTxArtifact(
662
- planArtifact,
663
- `simulated-signed-tx:${planArtifact.planId}`,
664
- systemRuntimeContext
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
- `Real Kaspa signing is not available: ${status.error || "Unknown error"}. Ensure 'kaspa' package is installed.`
698
+ `Failed to decrypt dev account ${alias}. Expected password: ${DEV_ACCOUNTS_PASSWORD}`
672
699
  );
673
700
  }
674
- const signer = new KaspaWasmPrivateKeySigner({
675
- account,
676
- allowMainnet: input.allowMainnet
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
- if (account.kind === "evm-private-key") {
719
- throw new Error(
720
- "EVM accounts are reserved for future Igra support and cannot sign Kaspa L1 transactions."
721
- );
722
- }
723
- const accountRecord = account;
724
- throw new Error(`Unsupported account kind for signing: ${accountRecord.kind}`);
725
- }
726
-
727
- // src/kaspa-sdk-keygen.ts
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
- return await rawLoader();
718
+ kaspaWasm = await import(
719
+ /* @vite-ignore */
720
+ "kaspa-wasm"
721
+ );
739
722
  } catch (e) {
740
- const err = new Error(
741
- "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."
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
- err.code = "WALLET_BACKEND_UNAVAILABLE";
744
- throw err;
739
+ } catch (e) {
740
+ console.warn(`
741
+ [Warning] @kaspa/core-lib is not installed.`);
742
+ return { address: "", privateKey: "", publicKey: "" };
745
743
  }
746
- };
747
- }
748
- async generateAccount(options) {
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
- throw new Error(
768
- "Loaded Kaspa SDK does not expose expected PrivateKey constructor."
769
- );
770
- } catch (e) {
771
- throw new Error(
772
- `Failed to generate account using SDK: ${e instanceof Error ? e.message : String(e)}`
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
- // src/kaspa-wallet.ts
779
- async function createLocalKaspaWallet(options) {
780
- const keygen = new KaspaSdkKeyGenerator({
781
- networkId: options?.networkId || "simnet"
782
- });
783
- return await keygen.generateAccount();
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/fixture-signer.ts
787
- import { calculateContentHash as calculateContentHash3, HARDKAS_VERSION as HARDKAS_VERSION3, ARTIFACT_VERSION as ARTIFACT_VERSION2, CURRENT_HASH_VERSION } from "@hardkas/artifacts";
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 parseWasmTxToRpc2(wasmTxStr) {
831
+ function parseWasmTxToRpc(wasmTxStr) {
792
832
  let parsed = JSON.parse(wasmTxStr);
793
- while (typeof parsed === "string") {
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: toHex2(i.inner.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
- value: o.inner.value,
850
+ amount: o.inner.value.toString(),
811
851
  scriptPublicKey: {
812
852
  version: parseInt(o.inner.scriptPublicKey.substring(0, 4), 16) || 0,
813
- script: o.inner.scriptPublicKey.substring(4)
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 ? toHex2(txInner.payload) : "",
820
- mass: txInner.mass || 0
859
+ payload: txInner.payload && txInner.payload.length > 0 ? toHex(txInner.payload) : ""
821
860
  };
822
861
  }
823
- var HardkasFixtureSigner = class {
824
- networkId;
825
- // A deterministic, known private key exclusively for Docker tests.
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
- async signTransaction(plan) {
848
- if (plan.networkId === "mainnet") {
849
- throw new Error("FixtureSigner refuses to sign mainnet transactions.");
850
- }
851
- const kaspa = await this.loadKaspa();
852
- const privateKey = new kaspa.PrivateKey(this.FIXTURE_PK);
853
- const utxos = plan.inputs.map((u) => {
854
- if (!u.outpoint.transactionId || u.outpoint.index === void 0) {
855
- throw new Error(`UTXO is missing transactionId or index. Re-run tx plan.`);
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 changeAddress;
883
- if (plan.change && plan.change.address) {
884
- changeAddress = new kaspa.Address(plan.change.address);
885
- } else {
886
- changeAddress = new kaspa.Address(plan.from.address);
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
- const priorityFee = BigInt(plan.estimatedFeeSompi || "0");
889
- const unsignedTx = kaspa.createTransaction(
890
- utxos,
891
- outputs,
892
- changeAddress,
893
- priorityFee
894
- );
895
- const signedTx = kaspa.signTransaction(unsignedTx, [privateKey], true);
896
- console.log("SIGNED TX TOSTRING:", signedTx.toString());
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 (plan.from.address !== account.address) {
978
- throw new Error(
979
- `Address mismatch: Plan requires ${plan.from.address}, but account has ${account.address}.`
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(account.privateKey);
903
+ const privateKey = new sdk.PrivateKey(pkValue);
984
904
  const utxos = plan.inputs.map((u) => {
985
- if (!u.scriptPublicKey) {
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
- return new sdk.UtxoEntry(
992
- BigInt(u.amountSompi),
993
- spk,
994
- u.outpoint.transactionId,
995
- u.outpoint.index,
996
- plan.from.address
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
- new sdk.PaymentOutput(new sdk.Address(plan.to.address), BigInt(plan.amountSompi))
1001
- ];
1002
- const changeAddress = plan.change ? new sdk.Address(plan.change.address) : void 0;
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 payload = signedTx.serialize ? signedTx.serialize() : JSON.stringify(signedTx.toRpcTransaction());
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: "kaspa-sdk",
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 (e) {
1021
- const msg = e instanceof Error ? e.message : String(e);
1022
- if (msg.includes("is not a constructor") || msg.includes("is not a function")) {
1023
- throw new Error(
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
- // src/real-keygen.ts
1033
- var UnsupportedKaspaKeyGenerator = class {
1034
- async generateAccount() {
965
+ function assertSigningNetworkAllowed(input) {
966
+ const isMainnet = input.network === "mainnet";
967
+ if (isMainnet && !input.allowMainnet) {
1035
968
  throw new Error(
1036
- "Real Kaspa key generation is not configured. Install/configure a supported Kaspa SDK adapter."
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/keystore.ts
1042
- import fs3 from "fs";
1043
- import path3 from "path";
1044
- import crypto from "crypto";
1045
- import { argon2id } from "hash-wasm";
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
- version: this.KEYSTORE_FORMAT_VERSION,
1086
- type: this.KEYSTORE_FORMAT_TYPE,
1087
- kdf: {
1088
- algorithm: "argon2id",
1089
- memory,
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
- * Decrypts an encrypted keystore using a password.
1110
- */
1111
- static async decryptEncryptedKeystore(keystore, password) {
1112
- if (keystore.version !== this.KEYSTORE_FORMAT_VERSION) {
1113
- return {
1114
- success: false,
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
- * Verifies if the password is correct for the keystore.
1145
- */
1146
- static async verifyKeystorePassword(keystore, password) {
1147
- const result = await this.decryptEncryptedKeystore(keystore, password);
1148
- return result.success;
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
- * Changes the password of an encrypted keystore.
1152
- */
1153
- static async changeKeystorePassword(keystore, oldPassword, newPassword) {
1154
- const unlock = await this.decryptEncryptedKeystore(keystore, oldPassword);
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
- return this.createEncryptedKeystore(unlock.payload, newPassword, {
1159
- label: keystore.metadata.label,
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
- `Failed to load keystore at ${filePath}: ${e instanceof Error ? e.message : String(e)}`
1013
+ `Real Kaspa transaction plans (mode: ${planArtifact.mode}) cannot be signed with simulated accounts.`
1180
1014
  );
1181
1015
  }
1182
1016
  }
1183
- /**
1184
- * Saves an encrypted keystore to the filesystem.
1185
- */
1186
- static async saveEncryptedKeystore(filePath, keystore) {
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
- await getOrCreateDevAccount(workspaceDir, 0, "alice");
1217
- await getOrCreateDevAccount(workspaceDir, 1, "bob");
1218
- }
1219
- async function getOrCreateDevAccount(workspaceDir, index, alias) {
1220
- const devAccountsDir = path4.join(workspaceDir, ".hardkas", "dev-accounts");
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
- if (!unlock.success || !unlock.payload) {
1028
+ }
1029
+ if (account.kind === "kaspa-private-key") {
1030
+ const status = await getKaspaSigningBackendStatus();
1031
+ if (!status.available) {
1229
1032
  throw new Error(
1230
- `Failed to decrypt dev account ${alias}. Expected password: ${DEV_ACCOUNTS_PASSWORD}`
1033
+ `Real Kaspa signing is not available: ${status.error || "Unknown error"}. Ensure 'kaspa' package is installed.`
1231
1034
  );
1232
1035
  }
1233
- return {
1234
- address: unlock.payload.address,
1235
- privateKey: unlock.payload.privateKey,
1236
- publicKey: unlock.payload.publicKey
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
- const seedString = `${SIMNET_DETERMINISTIC_SEED}-${index}`;
1240
- const privateKeyHex = crypto2.createHash("sha256").update(seedString).digest("hex");
1241
- const network = "simnet";
1242
- const isSimnet = ["simnet", "kaspasim", "local"].includes(network);
1243
- let address = "";
1244
- let privateKey = "";
1245
- let publicKey = "";
1246
- try {
1247
- if (isSimnet) {
1248
- let kaspaWasm;
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
- kaspaWasm = await import(
1251
- /* @vite-ignore */
1252
- "kaspa-wasm"
1253
- );
1100
+ return await rawLoader();
1254
1101
  } catch (e) {
1255
- console.warn(`
1256
- [Warning] kaspa-wasm is not installed. Required for simnet.`);
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
- } catch (e) {
1272
- console.warn(`
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
- const privKey = new sdk.PrivateKey(privateKeyHex);
1281
- const pubKey = privKey.toPublicKey();
1282
- try {
1283
- address = pubKey.toAddress(network).toString();
1284
- } catch (e) {
1285
- const msg = e instanceof Error ? e.message : String(e);
1286
- if (msg.includes("Second argument must be") || msg.includes("Unsupported")) {
1287
- const err = new Error("DEV_ACCOUNT_BACKEND_UNSUPPORTED_NETWORK");
1288
- err.code = "DEV_ACCOUNT_BACKEND_UNSUPPORTED_NETWORK";
1289
- throw err;
1290
- }
1291
- throw e;
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
- publicKey = pubKey.toString();
1294
- privateKey = privKey.toString();
1295
- }
1296
- } catch (e) {
1297
- if (e.message === "DEV_ACCOUNT_BACKEND_UNSUPPORTED_NETWORK") {
1298
- throw e;
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
- const accountData = {
1306
- address,
1307
- privateKey,
1308
- publicKey
1309
- };
1310
- if (!fs4.existsSync(devAccountsDir)) {
1311
- await fs4.promises.mkdir(devAccountsDir, { recursive: true });
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 payload = {
1314
- address: accountData.address,
1315
- privateKey: accountData.privateKey,
1316
- network: "simnet"
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
- if (accountData.publicKey) {
1319
- payload.publicKey = accountData.publicKey;
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
- const keystore = await KeystoreManager.createEncryptedKeystore(
1322
- payload,
1323
- DEV_ACCOUNTS_PASSWORD,
1324
- {
1325
- label: alias,
1326
- network: "simnet"
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
- const accounts = [];
1338
- const files = fs4.readdirSync(devAccountsDir);
1339
- for (const file of files) {
1340
- if (file.endsWith(".json")) {
1341
- const name = path4.basename(file, ".json");
1342
- try {
1343
- const data = fs4.readFileSync(path4.join(devAccountsDir, file), "utf-8");
1344
- const keystore = JSON.parse(data);
1345
- if (keystore.type === "hardkas.encryptedKeystore.v2") {
1346
- accounts.push({
1347
- name,
1348
- address: keystore.metadata?.address || ""
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
- } catch (e) {
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
- accounts.sort((a, b) => deterministicCompare(a.name, b.name));
1356
- return accounts;
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
  };