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