diff --git a/README.md b/README.md index c6b92b9..bf10c36 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Get Kaspa address (public key) for a BIP32 path. ##### Examples ```javascript -kaspa.getAddress("44'/501'/0'").then(r => r.address) +kaspa.getAddress("44'/111111'/0'").then(r => r.address) ``` Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{address: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the address field @@ -80,19 +80,26 @@ const txin = new TransactionInput({ }); const txout = new TransactionOutput({ - value: 1090000, + value: 1000000, + scriptPublicKey: "2011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac", +}); + +// By convention, the second output MUST be the change address +// It MUST set both addressType and addressIndex +const txoutchange = new TransactionOutput({ + value: 90000, scriptPublicKey: "2011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac", }); const tx = new Transaction({ version: 0, + changeAddressType: 0, + changeAddressIndex: 0, inputs: [txin], - outputs: [txout], + outputs: [txout, txoutchange], }); kaspa.signTransaction(tx); ``` -Updates the - - +Updates the transaction by filling in the `signature` property of each `TransactionInput` in the `Transaction` object. diff --git a/src/kaspa.js b/src/kaspa.js index 82b0bb5..7878c2e 100644 --- a/src/kaspa.js +++ b/src/kaspa.js @@ -94,9 +94,6 @@ class Kaspa { if (!(transaction instanceof Transaction)) { throw new Error("transaction must be an instance of Transaction"); } - // Ledger app supports only a single derivation path per call ATM - const pathsCountBuffer = Buffer.alloc(1); - pathsCountBuffer.writeUInt8(1, 0); const header = transaction.serialize(); diff --git a/src/transaction.js b/src/transaction.js index f648800..b136127 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -14,6 +14,17 @@ class Transaction { * @type {int} */ this.version = txData.version; + + this.changeAddressType = Number.isInteger(txData.changeAddressType) ? txData.changeAddressType : 0; + this.changeAddressIndex = Number.isInteger(txData.changeAddressIndex) ? txData.changeAddressIndex : 0; + + if (!(this.changeAddressType === 0 || this.changeAddressType === 1)) { + throw new Error(`changeAddressType must be 0 or 1 if set`); + } + + if (this.changeAddressIndex < 0x00000000 || this.changeAddressIndex > 0xFFFFFFFF) { + throw new Error(`changeAddressIndex must be between 0x00000000 and 0xFFFFFFFF`); + } } serialize() { @@ -26,10 +37,18 @@ class Transaction { const inputLenBuf = Buffer.alloc(1); inputLenBuf.writeUInt8(this.inputs.length); + const changeAddressTypeBuf = Buffer.alloc(1); + changeAddressTypeBuf.writeUInt8(this.changeAddressType || 0); + + const changeAddressIndexBuf = Buffer.alloc(4); + changeAddressIndexBuf.writeUInt32BE(this.changeAddressIndex || 0); + return Buffer.concat([ versionBuf, outputLenBuf, inputLenBuf, + changeAddressTypeBuf, + changeAddressIndexBuf, ]); } @@ -116,7 +135,15 @@ class TransactionInput { class TransactionOutput { constructor(outputData = {}) { + if (!outputData.value || outputData.value < 0 || outputData.value > 0xFFFFFFFFFFFFFFFF) { + throw new Error('value must be set to a value greater than 0 and less than 0xFFFFFFFFFFFFFFFF'); + } this.value = outputData.value; + if (!outputData.scriptPublicKey) { + throw new Error('scriptPublicKey must be set'); + } + + // Only then do we care about the script public key this.scriptPublicKey = outputData.scriptPublicKey; } diff --git a/tests/kaspa.test.js b/tests/kaspa.test.js index 41e48b2..bf1aded 100644 --- a/tests/kaspa.test.js +++ b/tests/kaspa.test.js @@ -47,7 +47,7 @@ describe("kaspa", () => { it("signTransaction with simple data", async () => { const transport = await openTransportReplayer( RecordStore.fromString(` - => e00600800400000101 + => e006008009000001010102030405 <= 9000 => e00601802a000000000010a1d02011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac <= 9000 @@ -74,6 +74,8 @@ describe("kaspa", () => { version: 0, inputs: [txin], outputs: [txout], + changeAddressType: 1, + changeAddressIndex: 0x02030405, }); await kaspa.signTransaction(tx); @@ -81,3 +83,117 @@ describe("kaspa", () => { expect(txin.sighash).toEqual("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"); }); }); + +describe("Transaction", () => { + it("should serialize", () => { + const txin = new TransactionInput({ + prevTxId: "40b022362f1a303518e2b49f86f87a317c87b514ca0f3d08ad2e7cf49d08cc70", + value: 1100000, + addressType: 0, + addressIndex: 0, + outpointIndex: 0, + }); + + const txout = new TransactionOutput({ + value: 1090000, + scriptPublicKey: "2011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac", + }); + + const tx = new Transaction({ + version: 0, + inputs: [txin], + outputs: [txout], + changeAddressType: 1, + changeAddressIndex: 0x02030405 + }); + + const expectation = Buffer.from([0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05]); + + expect(tx.serialize().equals(expectation)).toBeTruthy(); + }); +}); + +describe("TransactionOutput", () => { + it("should throw no error if only scriptPublicKey and value is set", () => { + let err = null; + try { + new TransactionOutput({ + value: 1090000, + scriptPublicKey: "2011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac", + }); + } catch (e) { + err = e; + } + + expect(err).toBe(null); + }); + + it("should serialize value and scriptPublicKey", () => { + const serial = new TransactionOutput({ + value: 1090000, + scriptPublicKey: "2011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac", + }).serialize(); + + const expectation = Buffer.from([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xa1, 0xd0, + 0x20, 0x11, 0xa7, 0x21, 0x5f, 0x66, 0x8e, 0x92, + 0x10, 0x13, 0xeb, 0x7a, 0xac, 0x9b, 0x7e, 0x64, + 0xb9, 0xec, 0x6e, 0x75, 0x7c, 0x1b, 0x64, 0x8e, + 0x89, 0x38, 0x8c, 0x91, 0x9f, 0x67, 0x6a, 0xa8, + 0x8c, 0xac, + ]); + + expect(serial.equals(expectation)).toBeTruthy(); + }); + + it("should throw errors if none of scriptPublicKey or (addressType, addressIndex) are set", () => { + let err = null; + try { + new TransactionOutput({ + value: 1090000 + }); + } catch (e) { + err = e; + } + + expect(err).not.toBe(null); + }); + + it("should throw errors if value is not set", () => { + let err = null; + try { + new TransactionOutput({ + scriptPublicKey: "2011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac", + }); + } catch (e) { + err = e; + } + + expect(err).not.toBe(null); + }); + + it("should throw errors if value is < 0 or > 0xFFFFFFFFFFFFFFFF", () => { + let err = null; + try { + new TransactionOutput({ + value: 0, + scriptPublicKey: "2011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac", + }); + } catch (e) { + err = e; + } + + expect(err).not.toBe(null); + + try { + new TransactionOutput({ + value: 0xFFFFFFFFFFFFFFFF + 1, + scriptPublicKey: "2011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac", + }); + } catch (e) { + err = e; + } + + expect(err).not.toBe(null); + }); +});