From 05f3b852816aad6e92631b3ccd24c03b300248a7 Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Mon, 26 Aug 2024 14:19:38 +0300 Subject: [PATCH 01/13] feat: Added removeSignature/clearAllSignatures methods to Transaction class, wrote some unit tests for both Signed-off-by: ivaylogarnev-limechain --- src/transaction/Transaction.js | 101 +++++++++++++++++++++++++++++++++ test/unit/Transaction.js | 66 +++++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/src/transaction/Transaction.js b/src/transaction/Transaction.js index e08a0d36e..a25668595 100644 --- a/src/transaction/Transaction.js +++ b/src/transaction/Transaction.js @@ -851,6 +851,107 @@ export default class Transaction extends Executable { return this; } + /** + * This method removes a signature from the transaction based on the public key or signature provided. + * + * @param {PublicKey} publicKey The public key associated with the signature to remove. + * @param {Uint8Array} signature - The signature to remove. If omitted, all signatures for the given public key will be removed. + * @returns {this} + */ + removeSignature(publicKey, signature) { + // If the transaction isn't frozen, freeze it. + if (!this.isFrozen()) { + this.freeze(); + } + + const publicKeyData = publicKey.toBytesRaw(); + const publicKeyHex = hex.encode(publicKeyData); + + if (!this._signerPublicKeys.has(publicKeyHex)) { + // If the public key hasn't signed the transaction, there's nothing to remove + return this; + } + + // Iterate over the signed transactions and remove matching signatures + for (const transaction of this._signedTransactions.list) { + if (transaction.sigMap && transaction.sigMap.sigPair) { + transaction.sigMap.sigPair = transaction.sigMap.sigPair.filter( + (sigPair) => { + let sigPairPublicKeyHex; + if (sigPair.pubKeyPrefix) { + sigPairPublicKeyHex = hex.encode( + sigPair.pubKeyPrefix, + ); + } + + const matchesPublicKey = + sigPairPublicKeyHex === publicKeyHex; + + if (signature) { + let matchesSignature = false; + + if (sigPair.ed25519) { + matchesSignature = + hex.encode(sigPair.ed25519) === + hex.encode(signature); + } else if (sigPair.ECDSASecp256k1) { + matchesSignature = + hex.encode(sigPair.ECDSASecp256k1) === + hex.encode(signature); + } + + // Remove the signature only if both the public key and signature match + return !(matchesPublicKey && matchesSignature); + } + + return !matchesPublicKey; + }, + ); + } + } + + // Remove the public key from internal tracking + this._signerPublicKeys.delete(publicKeyHex); + this._publicKeys = this._publicKeys.filter( + (key) => !key.equals(publicKey), + ); + + // Update transaction signers array + this._transactionSigners.pop(); + + return this; + } + + /** + * This method clears all signatures from the transaction. + * + * It will iterate through all signed transactions, remove all signature pairs from the `sigMap`, + * and clear the internal tracking of signer public keys. + * + * @returns {this} + */ + clearAllSignatures() { + // If the transaction isn't frozen, freeze it. + if (!this.isFrozen()) { + this.freeze(); + } + + // Iterate over the signed transactions and clear all signatures + for (const transaction of this._signedTransactions.list) { + if (transaction.sigMap && transaction.sigMap.sigPair) { + // Clear all signature pairs from the transaction's signature map + transaction.sigMap.sigPair = []; + } + } + + // Clear the internal tracking of signer public keys and other relevant arrays + this._signerPublicKeys.clear(); // Clear the map of signer public keys + this._publicKeys = []; // Clear the array of public keys + this._transactionSigners = []; // Clear the array of transaction signers + + return this; + } + /** * Get the current signatures on the request * diff --git a/test/unit/Transaction.js b/test/unit/Transaction.js index cddded978..5c4c54851 100644 --- a/test/unit/Transaction.js +++ b/test/unit/Transaction.js @@ -313,4 +313,70 @@ describe("Transaction", function () { }); }); }); + + describe.only("Transaction Signature Management", function () { + let key1, key2, key3; + let transaction; + + beforeEach(async function () { + const nodeAccountId = new AccountId(3); + const account = AccountId.fromString("0.0.1004"); + const validStart = new Timestamp(1451, 590); + const transactionId = new TransactionId(account, validStart); + + key1 = PrivateKey.generateED25519(); + key2 = PrivateKey.generateED25519(); + key3 = PrivateKey.generateED25519(); + + transaction = new AccountCreateTransaction() + .setInitialBalance(new Hbar(2)) + .setTransactionId(transactionId) + .setNodeAccountIds([nodeAccountId]) + .freeze(); + }); + + it("should remove a specific signature", async function () { + // Sign the transaction with multiple keys + const signature1 = await key1.signTransaction(transaction); + const signature2 = await key2.signTransaction(transaction); + const signature3 = await key3.signTransaction(transaction); + + transaction.addSignature(key1.publicKey, signature1); + transaction.addSignature(key2.publicKey, signature2); + transaction.addSignature(key3.publicKey, signature3); + + // Ensure all signatures are present before removal + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); + + // Remove one signature + transaction.removeSignature(key1.publicKey, signature1); + + // Ensure the specific signature has been removed + const signaturesAfter = transaction.getSignatures(); + expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); + }); + + it("should clear all signatures", async function () { + // Sign the transaction with multiple keys + const signature1 = await key1.signTransaction(transaction); + const signature2 = await key2.signTransaction(transaction); + const signature3 = await key3.signTransaction(transaction); + + transaction.addSignature(key1.publicKey, signature1); + transaction.addSignature(key2.publicKey, signature2); + transaction.addSignature(key3.publicKey, signature3); + + // Ensure all signatures are present before clearing + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); + + // Clear all signatures + transaction.clearAllSignatures(); + + // Ensure all signatures have been cleared + const signaturesAfter = transaction.getSignatures(); + expect(signaturesAfter.get(new AccountId(3)).size).to.equal(0); + }); + }); }); From 9febd0edebbfb55ed470dea4f1f11719530c15a6 Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Tue, 27 Aug 2024 09:13:08 +0300 Subject: [PATCH 02/13] feat: Added more unit tests for removeSignature/clearAllSignatures and throwing an error if trying to remove non-existing signature Signed-off-by: ivaylogarnev-limechain --- src/transaction/Transaction.js | 3 +- test/unit/Transaction.js | 94 ++++++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 18 deletions(-) diff --git a/src/transaction/Transaction.js b/src/transaction/Transaction.js index a25668595..c75adf413 100644 --- a/src/transaction/Transaction.js +++ b/src/transaction/Transaction.js @@ -868,8 +868,7 @@ export default class Transaction extends Executable { const publicKeyHex = hex.encode(publicKeyData); if (!this._signerPublicKeys.has(publicKeyHex)) { - // If the public key hasn't signed the transaction, there's nothing to remove - return this; + throw new Error("The public key has not signed this transaction"); } // Iterate over the signed transactions and remove matching signatures diff --git a/test/unit/Transaction.js b/test/unit/Transaction.js index 5c4c54851..c5e4e71b2 100644 --- a/test/unit/Transaction.js +++ b/test/unit/Transaction.js @@ -314,7 +314,7 @@ describe("Transaction", function () { }); }); - describe.only("Transaction Signature Management", function () { + describe.only("Transaction removeSignature/clearAllSignatures methods", function () { let key1, key2, key3; let transaction; @@ -335,15 +335,28 @@ describe("Transaction", function () { .freeze(); }); - it("should remove a specific signature", async function () { + function signAndAddSignatures(transaction, ...keys) { + // Map through the keys to sign the transaction and add signatures + const signatures = keys.map((key) => { + const signature = key.signTransaction(transaction); + transaction.addSignature(key.publicKey, signature); + }); + + return signatures; + } + + it("should remove a specific signature", function () { // Sign the transaction with multiple keys - const signature1 = await key1.signTransaction(transaction); - const signature2 = await key2.signTransaction(transaction); - const signature3 = await key3.signTransaction(transaction); + const [signature1] = signAndAddSignatures( + transaction, + key1, + key2, + key3, + ); - transaction.addSignature(key1.publicKey, signature1); - transaction.addSignature(key2.publicKey, signature2); - transaction.addSignature(key3.publicKey, signature3); + expect(transaction._signerPublicKeys.size).to.equal(3); + expect(transaction._publicKeys.length).to.equal(3); + expect(transaction._transactionSigners.length).to.equal(3); // Ensure all signatures are present before removal const signaturesBefore = transaction.getSignatures(); @@ -352,20 +365,21 @@ describe("Transaction", function () { // Remove one signature transaction.removeSignature(key1.publicKey, signature1); + //Check if the transaction is frozen + expect(transaction.isFrozen()).to.be.true; + + expect(transaction._signerPublicKeys.size).to.equal(2); + expect(transaction._publicKeys.length).to.equal(2); + expect(transaction._transactionSigners.length).to.equal(2); + // Ensure the specific signature has been removed const signaturesAfter = transaction.getSignatures(); expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); }); - it("should clear all signatures", async function () { + it("should clear all signatures", function () { // Sign the transaction with multiple keys - const signature1 = await key1.signTransaction(transaction); - const signature2 = await key2.signTransaction(transaction); - const signature3 = await key3.signTransaction(transaction); - - transaction.addSignature(key1.publicKey, signature1); - transaction.addSignature(key2.publicKey, signature2); - transaction.addSignature(key3.publicKey, signature3); + signAndAddSignatures(transaction, key1, key2, key3); // Ensure all signatures are present before clearing const signaturesBefore = transaction.getSignatures(); @@ -374,9 +388,57 @@ describe("Transaction", function () { // Clear all signatures transaction.clearAllSignatures(); + //Check if the transaction is frozen + expect(transaction.isFrozen()).to.be.true; + + //Check if the transaction keys are empty + expect(transaction._signerPublicKeys.size).to.equal(0); + expect(transaction._publicKeys.length).to.equal(0); + expect(transaction._transactionSigners.length).to.equal(0); + // Ensure all signatures have been cleared const signaturesAfter = transaction.getSignatures(); expect(signaturesAfter.get(new AccountId(3)).size).to.equal(0); }); + + it("should not remove a non-existing signature", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2); + + // Attempt to remove a non-existing signature + expect(() => { + transaction.removeSignature(key3.publicKey); + }).to.throw("The public key has not signed this transaction"); + + // Ensure signatures are not affected + const signaturesAfter = transaction.getSignatures(); + expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); + }); + + it("should clear and re-sign after all signatures are cleared", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2); + + // Ensure all signatures are present before clearing + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(2); + + // Clear all signatures + transaction.clearAllSignatures(); + + // Ensure all signatures have been cleared + const signaturesAfterClear = transaction.getSignatures(); + expect(signaturesAfterClear.get(new AccountId(3)).size).to.equal(0); + + // Re-sign the transaction with a different key + const signature3 = key3.signTransaction(transaction); + transaction.addSignature(key3.publicKey, signature3); + + // Ensure only one signature exists after re-signing + const signaturesAfterResign = transaction.getSignatures(); + expect(signaturesAfterResign.get(new AccountId(3)).size).to.equal( + 1, + ); + }); }); }); From 350b988605c36a753e62e45c79c41203f36f69d3 Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Tue, 27 Aug 2024 09:14:02 +0300 Subject: [PATCH 03/13] refactor: Turned signAndAddSignatures function into arrow Signed-off-by: ivaylogarnev-limechain --- test/unit/Transaction.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/Transaction.js b/test/unit/Transaction.js index c5e4e71b2..317eb0d0e 100644 --- a/test/unit/Transaction.js +++ b/test/unit/Transaction.js @@ -335,7 +335,7 @@ describe("Transaction", function () { .freeze(); }); - function signAndAddSignatures(transaction, ...keys) { + const signAndAddSignatures = (transaction, ...keys) => { // Map through the keys to sign the transaction and add signatures const signatures = keys.map((key) => { const signature = key.signTransaction(transaction); @@ -343,7 +343,7 @@ describe("Transaction", function () { }); return signatures; - } + }; it("should remove a specific signature", function () { // Sign the transaction with multiple keys From af3474848d9afab15f80d54fe9209f7f8a3eacfd Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Tue, 27 Aug 2024 09:24:25 +0300 Subject: [PATCH 04/13] refactor: Cleared some unnecessary comments and changed variable assigment Signed-off-by: ivaylogarnev-limechain --- src/transaction/Transaction.js | 15 ++++++--------- test/unit/Transaction.js | 6 ++++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/transaction/Transaction.js b/src/transaction/Transaction.js index c75adf413..bde3450b3 100644 --- a/src/transaction/Transaction.js +++ b/src/transaction/Transaction.js @@ -876,12 +876,9 @@ export default class Transaction extends Executable { if (transaction.sigMap && transaction.sigMap.sigPair) { transaction.sigMap.sigPair = transaction.sigMap.sigPair.filter( (sigPair) => { - let sigPairPublicKeyHex; - if (sigPair.pubKeyPrefix) { - sigPairPublicKeyHex = hex.encode( - sigPair.pubKeyPrefix, - ); - } + let sigPairPublicKeyHex = hex.encode( + sigPair?.pubKeyPrefix || new Uint8Array(), + ); const matchesPublicKey = sigPairPublicKeyHex === publicKeyHex; @@ -944,9 +941,9 @@ export default class Transaction extends Executable { } // Clear the internal tracking of signer public keys and other relevant arrays - this._signerPublicKeys.clear(); // Clear the map of signer public keys - this._publicKeys = []; // Clear the array of public keys - this._transactionSigners = []; // Clear the array of transaction signers + this._signerPublicKeys.clear(); + this._publicKeys = []; + this._transactionSigners = []; return this; } diff --git a/test/unit/Transaction.js b/test/unit/Transaction.js index 317eb0d0e..2fd629f78 100644 --- a/test/unit/Transaction.js +++ b/test/unit/Transaction.js @@ -314,7 +314,7 @@ describe("Transaction", function () { }); }); - describe.only("Transaction removeSignature/clearAllSignatures methods", function () { + describe("Transaction removeSignature/clearAllSignatures methods", function () { let key1, key2, key3; let transaction; @@ -354,6 +354,7 @@ describe("Transaction", function () { key3, ); + //Check if the transaction internal tracking of signer public keys is correct expect(transaction._signerPublicKeys.size).to.equal(3); expect(transaction._publicKeys.length).to.equal(3); expect(transaction._transactionSigners.length).to.equal(3); @@ -368,6 +369,7 @@ describe("Transaction", function () { //Check if the transaction is frozen expect(transaction.isFrozen()).to.be.true; + //Check if the transaction internal tracking of signer public keys is correct expect(transaction._signerPublicKeys.size).to.equal(2); expect(transaction._publicKeys.length).to.equal(2); expect(transaction._transactionSigners.length).to.equal(2); @@ -391,7 +393,7 @@ describe("Transaction", function () { //Check if the transaction is frozen expect(transaction.isFrozen()).to.be.true; - //Check if the transaction keys are empty + //Check if the transaction internal tracking of signer public keys is cleared expect(transaction._signerPublicKeys.size).to.equal(0); expect(transaction._publicKeys.length).to.equal(0); expect(transaction._transactionSigners.length).to.equal(0); From ff24587d4486d0669c86cad3d4283f63a35d0534 Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Thu, 29 Aug 2024 15:04:36 +0300 Subject: [PATCH 05/13] feat: Enhancement removeSignature/clearAllSignatures to return the removedSignatures and wrote tests for both Signed-off-by: ivaylogarnev-limechain --- src/transaction/Transaction.js | 221 +++++++++++++++++++++++++++------ test/unit/Transaction.js | 93 ++++++++++++++ 2 files changed, 274 insertions(+), 40 deletions(-) diff --git a/src/transaction/Transaction.js b/src/transaction/Transaction.js index bde3450b3..ea67cf75e 100644 --- a/src/transaction/Transaction.js +++ b/src/transaction/Transaction.js @@ -854,9 +854,9 @@ export default class Transaction extends Executable { /** * This method removes a signature from the transaction based on the public key or signature provided. * - * @param {PublicKey} publicKey The public key associated with the signature to remove. - * @param {Uint8Array} signature - The signature to remove. If omitted, all signatures for the given public key will be removed. - * @returns {this} + * @param {PublicKey} publicKey - The public key associated with the signature to remove. + * @param {Uint8Array} [signature] - The signature to remove. If omitted, all signatures for the given public key will be removed. + * @returns {Uint8Array[]} The removed signatures. */ removeSignature(publicKey, signature) { // If the transaction isn't frozen, freeze it. @@ -871,42 +871,22 @@ export default class Transaction extends Executable { throw new Error("The public key has not signed this transaction"); } + /** @type {Uint8Array[]} */ + const removedSignatures = []; + // Iterate over the signed transactions and remove matching signatures for (const transaction of this._signedTransactions.list) { - if (transaction.sigMap && transaction.sigMap.sigPair) { - transaction.sigMap.sigPair = transaction.sigMap.sigPair.filter( - (sigPair) => { - let sigPairPublicKeyHex = hex.encode( - sigPair?.pubKeyPrefix || new Uint8Array(), - ); - - const matchesPublicKey = - sigPairPublicKeyHex === publicKeyHex; - - if (signature) { - let matchesSignature = false; - - if (sigPair.ed25519) { - matchesSignature = - hex.encode(sigPair.ed25519) === - hex.encode(signature); - } else if (sigPair.ECDSASecp256k1) { - matchesSignature = - hex.encode(sigPair.ECDSASecp256k1) === - hex.encode(signature); - } - - // Remove the signature only if both the public key and signature match - return !(matchesPublicKey && matchesSignature); - } - - return !matchesPublicKey; - }, + const removedSignaturesFromTransaction = + this._removeSignaturesFromTransaction( + transaction, + publicKeyHex, + signature, ); - } + + removedSignatures.push(...removedSignaturesFromTransaction); } - // Remove the public key from internal tracking + // Remove the public key from internal tracking if no signatures remain this._signerPublicKeys.delete(publicKeyHex); this._publicKeys = this._publicKeys.filter( (key) => !key.equals(publicKey), @@ -915,16 +895,16 @@ export default class Transaction extends Executable { // Update transaction signers array this._transactionSigners.pop(); - return this; + return removedSignatures; } /** - * This method clears all signatures from the transaction. + * This method clears all signatures from the transaction and returns them in a specific format. * - * It will iterate through all signed transactions, remove all signature pairs from the `sigMap`, - * and clear the internal tracking of signer public keys. + * It will call collectSignatures to get the removed signatures, then clear all signatures + * from the internal tracking. * - * @returns {this} + * @returns {{ [transactionId: string]: { [nodeAccountId: string]: Uint8Array[] } }} The removed signatures in the specified format. */ clearAllSignatures() { // If the transaction isn't frozen, freeze it. @@ -932,6 +912,9 @@ export default class Transaction extends Executable { this.freeze(); } + // Collect all signatures + const removedSignatures = this._collectSignaturesInSpecificFormat(); + // Iterate over the signed transactions and clear all signatures for (const transaction of this._signedTransactions.list) { if (transaction.sigMap && transaction.sigMap.sigPair) { @@ -945,7 +928,7 @@ export default class Transaction extends Executable { this._publicKeys = []; this._transactionSigners = []; - return this; + return removedSignatures; } /** @@ -1842,6 +1825,164 @@ export default class Transaction extends Executable { response, ).finish(); } + + /** + * Removes signatures from a transaction and collects the removed signatures. + * + * @param {HashgraphProto.proto.ISignedTransaction} transaction - The transaction object to process. + * @param {string} publicKeyHex - The hexadecimal representation of the public key. + * @param {Uint8Array} [signature] - The signature to remove (optional). + * @returns {Uint8Array[]} An array of removed signatures. + */ + _removeSignaturesFromTransaction(transaction, publicKeyHex, signature) { + /** @type {Uint8Array[]} */ + const removedSignatures = []; + + if (transaction.sigMap && transaction.sigMap.sigPair) { + transaction.sigMap.sigPair = transaction.sigMap.sigPair.filter( + (sigPair) => { + const shouldRemove = this._shouldRemoveSignature( + sigPair, + publicKeyHex, + signature, + ); + + if (shouldRemove) { + if (sigPair.ed25519) { + removedSignatures.push(sigPair.ed25519); + } else if (sigPair.ECDSASecp256k1) { + removedSignatures.push(sigPair.ECDSASecp256k1); + } + } + + return !shouldRemove; + }, + ); + } + + return removedSignatures; + } + + /** + * Determines whether a signature should be removed based on the provided signature pair, + * signature, and public key. + * + * This function compares the public key and optional signature of a signature pair against + * the provided public key and signature. It returns `true` if the signature should be removed + * (i.e., it matches both the provided public key and signature), otherwise `false`. + * + * @param {HashgraphProto.proto.ISignaturePair} sigPair - The signature pair object to be evaluated. + * @param {string} publicKeyHex - The hexadecimal representation of the public key to compare against. + * @param {Uint8Array} [signature] - The signature to compare against. (optional) + * @returns {boolean} `true` if the signature should be removed based on the public key and signature match, otherwise `false`. + */ + _shouldRemoveSignature = (sigPair, publicKeyHex, signature) => { + const sigPairPublicKeyHex = hex.encode( + sigPair?.pubKeyPrefix || new Uint8Array(), + ); + + const matchesPublicKey = sigPairPublicKeyHex === publicKeyHex; + + if (signature) { + let matchesSignature = false; + + if (sigPair.ed25519) { + matchesSignature = + hex.encode(sigPair.ed25519) === hex.encode(signature); + } else if (sigPair.ECDSASecp256k1) { + matchesSignature = + hex.encode(sigPair.ECDSASecp256k1) === + hex.encode(signature); + } + + // Remove the signature only if both the public key and signature match + return matchesPublicKey && matchesSignature; + } + + return matchesPublicKey; + }; + + /** + * Collects all signatures from signed transactions and returns them in a specific format. + * + * @returns {{ [transactionId: string]: { [nodeAccountId: string]: Uint8Array[] } }} The collected signatures in the specified format. + */ + _collectSignaturesInSpecificFormat() { + /** @type {{ [transactionId: string]: { [nodeAccountId: string]: Uint8Array[] } }} */ + const collectedSignatures = {}; + + // Iterate over the signed transactions and collect signatures + for (const transaction of this._signedTransactions.list) { + if ( + transaction?.bodyBytes?.byteLength === 0 || + transaction.bodyBytes == null + ) { + continue; + } + + // Extract transaction details + const { transactionId, nodeAccountId } = + this._extractSignedTransactionDetails(transaction.bodyBytes); + + if (!transactionId || !nodeAccountId) { + continue; + } + + // Initialize the structure for this transactionId and nodeAccountId + if (!collectedSignatures[transactionId]) { + collectedSignatures[transactionId] = {}; + } + if (!collectedSignatures[transactionId][nodeAccountId]) { + collectedSignatures[transactionId][nodeAccountId] = []; + } + + // Collect the signatures + if (transaction.sigMap && transaction.sigMap.sigPair) { + for (const sigPair of transaction.sigMap.sigPair) { + if (sigPair.ed25519) { + collectedSignatures[transactionId][nodeAccountId].push( + sigPair.ed25519, + ); + } else if (sigPair.ECDSASecp256k1) { + collectedSignatures[transactionId][nodeAccountId].push( + sigPair.ECDSASecp256k1, + ); + } + } + } + } + + return collectedSignatures; + } + + /** + * Extracts the transactionId and nodeAccountId from a TransactionBody. + * + * @param {Uint8Array} bodyBytes - The bodyBytes to deserialize. + * @returns {{ transactionId: string, nodeAccountId: string }} The extracted transactionId and nodeAccountId. + */ + _extractSignedTransactionDetails(bodyBytes) { + // Deserialize bodyBytes into TransactionBody + const transactionBody = HashgraphProto.proto.TransactionBody.decode( + bodyBytes || new Uint8Array(0), + ); + + // Extract the transactionId + const transactionId = TransactionId._fromProtobuf( + /** @type {HashgraphProto.proto.ITransactionID} */ ( + transactionBody.transactionID + ), + ).toString(); + + // Extract the nodeAccountId + const nodeAccountId = AccountId._fromProtobuf( + /** @type {HashgraphProto.proto.IAccountID} */ ( + transactionBody.nodeAccountID + ), + ).toString(); + + return { transactionId, nodeAccountId }; + } } /** diff --git a/test/unit/Transaction.js b/test/unit/Transaction.js index 2fd629f78..24e58facf 100644 --- a/test/unit/Transaction.js +++ b/test/unit/Transaction.js @@ -340,6 +340,7 @@ describe("Transaction", function () { const signatures = keys.map((key) => { const signature = key.signTransaction(transaction); transaction.addSignature(key.publicKey, signature); + return signature; }); return signatures; @@ -442,5 +443,97 @@ describe("Transaction", function () { 1, ); }); + + it("should return the removed signature in Uint8Array format when removing a specific signature", function () { + // Sign the transaction with multiple keys + const [signature1] = signAndAddSignatures( + transaction, + key1, + key2, + key3, + ); + + // Remove one signature and capture the returned value + const removedSignatures = transaction.removeSignature( + key1.publicKey, + signature1, + ); + + // Check the format of the returned value + expect(removedSignatures).to.be.an("array"); + expect(removedSignatures.length).to.equal(1); + expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); + + // Ensure the returned signature is the one that was removed + expect(removedSignatures[0]).to.deep.equal(signature1); + }); + + it("should return the signatures for a public key in Uint8Array format when only the public key is provided", function () { + // Sign the transaction with multiple keys + const [signature1] = signAndAddSignatures(transaction, key1); + + // Remove all signatures for key1 and capture the returned value + const removedSignatures = transaction.removeSignature( + key1.publicKey, + ); + + // Check the format of the returned value + expect(removedSignatures).to.be.an("array"); + expect(removedSignatures.length).to.equal(1); + expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); + + // Ensure the returned signatures are the ones that were removed + expect(removedSignatures).to.include.members([signature1]); + }); + + it("should return all removed signatures in the expected object format when clearing all signatures", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2, key3); + + // Clear all signatures and capture the returned value + const removedSignatures = transaction.clearAllSignatures(); + + // Check the format of the returned value + expect(removedSignatures).to.be.an("object"); + + // Ensure the object has the correct structure + const nodeAccountId = new AccountId(3).toString(); + const transactionId = transaction.transactionId.toString(); + + expect(removedSignatures).to.have.property(transactionId); + expect(removedSignatures[transactionId]).to.have.property( + nodeAccountId, + ); + expect(removedSignatures[transactionId][nodeAccountId]).to.be.an( + "array", + ); + + // Ensure the removed signatures are in the expected format + const signaturesArray = + removedSignatures[transactionId][nodeAccountId]; + expect(signaturesArray.length).to.equal(3); + signaturesArray.forEach((sig) => { + expect(sig).to.be.instanceOf(Uint8Array); + }); + }); + + it("should return an empty object when no signatures are present", function () { + // Ensure the transaction has no signatures + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(0); + + // Ensure the object has the correct structure + const nodeAccountId = new AccountId(3).toString(); + const transactionId = transaction.transactionId.toString(); + + // Clear all signatures and capture the returned value + const removedSignatures = transaction.clearAllSignatures(); + + // Check the format of the returned value + expect(removedSignatures).to.be.an("object"); + expect( + Object.keys(removedSignatures[transactionId][nodeAccountId]), + ).to.have.lengthOf(0); + }); }); }); From e81ef5a65862145d43db5a6a0609d54bc7ab5cb6 Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Fri, 30 Aug 2024 16:49:54 +0300 Subject: [PATCH 06/13] refactor: Removed the signature param from removeSignature method and adjusted the logic, renamed clearAllSignatures and changed the return type, adjusted some tests Signed-off-by: ivaylogarnev-limechain --- src/transaction/Transaction.js | 129 ++++++++------------------------- test/unit/Transaction.js | 74 ++++++++++++------- 2 files changed, 78 insertions(+), 125 deletions(-) diff --git a/src/transaction/Transaction.js b/src/transaction/Transaction.js index ea67cf75e..df98a5bc9 100644 --- a/src/transaction/Transaction.js +++ b/src/transaction/Transaction.js @@ -852,13 +852,12 @@ export default class Transaction extends Executable { } /** - * This method removes a signature from the transaction based on the public key or signature provided. + * This method removes all signatures from the transaction based on the public key provided. * * @param {PublicKey} publicKey - The public key associated with the signature to remove. - * @param {Uint8Array} [signature] - The signature to remove. If omitted, all signatures for the given public key will be removed. * @returns {Uint8Array[]} The removed signatures. */ - removeSignature(publicKey, signature) { + removeSignature(publicKey) { // If the transaction isn't frozen, freeze it. if (!this.isFrozen()) { this.freeze(); @@ -880,7 +879,6 @@ export default class Transaction extends Executable { this._removeSignaturesFromTransaction( transaction, publicKeyHex, - signature, ); removedSignatures.push(...removedSignaturesFromTransaction); @@ -906,14 +904,14 @@ export default class Transaction extends Executable { * * @returns {{ [transactionId: string]: { [nodeAccountId: string]: Uint8Array[] } }} The removed signatures in the specified format. */ - clearAllSignatures() { + removeAllSignatures() { // If the transaction isn't frozen, freeze it. if (!this.isFrozen()) { this.freeze(); } // Collect all signatures - const removedSignatures = this._collectSignaturesInSpecificFormat(); + const removedSignatures = this._collectSignaturesByPublicKey(); // Iterate over the signed transactions and clear all signatures for (const transaction of this._signedTransactions.list) { @@ -1827,14 +1825,13 @@ export default class Transaction extends Executable { } /** - * Removes signatures from a transaction and collects the removed signatures. + * Removes all signatures from a transaction and collects the removed signatures. * * @param {HashgraphProto.proto.ISignedTransaction} transaction - The transaction object to process. * @param {string} publicKeyHex - The hexadecimal representation of the public key. - * @param {Uint8Array} [signature] - The signature to remove (optional). * @returns {Uint8Array[]} An array of removed signatures. */ - _removeSignaturesFromTransaction(transaction, publicKeyHex, signature) { + _removeSignaturesFromTransaction(transaction, publicKeyHex) { /** @type {Uint8Array[]} */ const removedSignatures = []; @@ -1844,7 +1841,6 @@ export default class Transaction extends Executable { const shouldRemove = this._shouldRemoveSignature( sigPair, publicKeyHex, - signature, ); if (shouldRemove) { @@ -1864,89 +1860,55 @@ export default class Transaction extends Executable { } /** - * Determines whether a signature should be removed based on the provided signature pair, - * signature, and public key. + * Determines whether a signature should be removed based on the provided public key. * - * This function compares the public key and optional signature of a signature pair against - * the provided public key and signature. It returns `true` if the signature should be removed - * (i.e., it matches both the provided public key and signature), otherwise `false`. - * - * @param {HashgraphProto.proto.ISignaturePair} sigPair - The signature pair object to be evaluated. + * @param {HashgraphProto.proto.ISignaturePair} sigPair - The signature pair object that contains + * the public key prefix and signature to be evaluated. * @param {string} publicKeyHex - The hexadecimal representation of the public key to compare against. - * @param {Uint8Array} [signature] - The signature to compare against. (optional) - * @returns {boolean} `true` if the signature should be removed based on the public key and signature match, otherwise `false`. + * @returns {boolean} `true` if the public key prefix in the signature pair matches the provided public key, + * indicating that the signature should be removed; otherwise, `false`. */ - _shouldRemoveSignature = (sigPair, publicKeyHex, signature) => { + _shouldRemoveSignature = (sigPair, publicKeyHex) => { const sigPairPublicKeyHex = hex.encode( sigPair?.pubKeyPrefix || new Uint8Array(), ); const matchesPublicKey = sigPairPublicKeyHex === publicKeyHex; - if (signature) { - let matchesSignature = false; - - if (sigPair.ed25519) { - matchesSignature = - hex.encode(sigPair.ed25519) === hex.encode(signature); - } else if (sigPair.ECDSASecp256k1) { - matchesSignature = - hex.encode(sigPair.ECDSASecp256k1) === - hex.encode(signature); - } - - // Remove the signature only if both the public key and signature match - return matchesPublicKey && matchesSignature; - } - return matchesPublicKey; }; /** - * Collects all signatures from signed transactions and returns them in a specific format. + * Collects all signatures from signed transactions and returns them in a format keyed by public key. * - * @returns {{ [transactionId: string]: { [nodeAccountId: string]: Uint8Array[] } }} The collected signatures in the specified format. + * @returns {{ [publicKey: PublicKey]: Uint8Array[] }} The collected signatures keyed by public key. */ - _collectSignaturesInSpecificFormat() { - /** @type {{ [transactionId: string]: { [nodeAccountId: string]: Uint8Array[] } }} */ + _collectSignaturesByPublicKey() { + /** @type {{ [publicKey: string]: Uint8Array[] }} */ const collectedSignatures = {}; // Iterate over the signed transactions and collect signatures for (const transaction of this._signedTransactions.list) { - if ( - transaction?.bodyBytes?.byteLength === 0 || - transaction.bodyBytes == null - ) { - continue; - } - - // Extract transaction details - const { transactionId, nodeAccountId } = - this._extractSignedTransactionDetails(transaction.bodyBytes); - - if (!transactionId || !nodeAccountId) { - continue; - } - - // Initialize the structure for this transactionId and nodeAccountId - if (!collectedSignatures[transactionId]) { - collectedSignatures[transactionId] = {}; - } - if (!collectedSignatures[transactionId][nodeAccountId]) { - collectedSignatures[transactionId][nodeAccountId] = []; - } - // Collect the signatures if (transaction.sigMap && transaction.sigMap.sigPair) { for (const sigPair of transaction.sigMap.sigPair) { + let signature; if (sigPair.ed25519) { - collectedSignatures[transactionId][nodeAccountId].push( - sigPair.ed25519, - ); + signature = sigPair.ed25519; } else if (sigPair.ECDSASecp256k1) { - collectedSignatures[transactionId][nodeAccountId].push( - sigPair.ECDSASecp256k1, - ); + signature = sigPair.ECDSASecp256k1; + } + + if (signature && sigPair.pubKeyPrefix) { + const publicKeyHex = hex.encode(sigPair.pubKeyPrefix); + + // Initialize the structure for this publicKey if it doesn't exist + if (!collectedSignatures[publicKeyHex]) { + collectedSignatures[publicKeyHex] = []; + } + + // Add the signature to the corresponding public key + collectedSignatures[publicKeyHex].push(signature); } } } @@ -1954,35 +1916,6 @@ export default class Transaction extends Executable { return collectedSignatures; } - - /** - * Extracts the transactionId and nodeAccountId from a TransactionBody. - * - * @param {Uint8Array} bodyBytes - The bodyBytes to deserialize. - * @returns {{ transactionId: string, nodeAccountId: string }} The extracted transactionId and nodeAccountId. - */ - _extractSignedTransactionDetails(bodyBytes) { - // Deserialize bodyBytes into TransactionBody - const transactionBody = HashgraphProto.proto.TransactionBody.decode( - bodyBytes || new Uint8Array(0), - ); - - // Extract the transactionId - const transactionId = TransactionId._fromProtobuf( - /** @type {HashgraphProto.proto.ITransactionID} */ ( - transactionBody.transactionID - ), - ).toString(); - - // Extract the nodeAccountId - const nodeAccountId = AccountId._fromProtobuf( - /** @type {HashgraphProto.proto.IAccountID} */ ( - transactionBody.nodeAccountID - ), - ).toString(); - - return { transactionId, nodeAccountId }; - } } /** diff --git a/test/unit/Transaction.js b/test/unit/Transaction.js index 24e58facf..85c7862f7 100644 --- a/test/unit/Transaction.js +++ b/test/unit/Transaction.js @@ -16,6 +16,7 @@ import Client from "../../src/client/NodeClient.js"; import * as HashgraphProto from "@hashgraph/proto"; import Long from "long"; import BigNumber from "bignumber.js"; +import { PublicKey } from "@hashgraph/cryptography"; describe("Transaction", function () { it("toBytes", async function () { @@ -314,7 +315,7 @@ describe("Transaction", function () { }); }); - describe("Transaction removeSignature/clearAllSignatures methods", function () { + describe("Transaction removeSignature/removeAllSignatures methods", function () { let key1, key2, key3; let transaction; @@ -389,7 +390,7 @@ describe("Transaction", function () { expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); // Clear all signatures - transaction.clearAllSignatures(); + transaction.removeAllSignatures(); //Check if the transaction is frozen expect(transaction.isFrozen()).to.be.true; @@ -427,7 +428,7 @@ describe("Transaction", function () { expect(signaturesBefore.get(new AccountId(3)).size).to.equal(2); // Clear all signatures - transaction.clearAllSignatures(); + transaction.removeAllSignatures(); // Ensure all signatures have been cleared const signaturesAfterClear = transaction.getSignatures(); @@ -491,49 +492,68 @@ describe("Transaction", function () { signAndAddSignatures(transaction, key1, key2, key3); // Clear all signatures and capture the returned value - const removedSignatures = transaction.clearAllSignatures(); + const removedSignatures = transaction.removeAllSignatures(); // Check the format of the returned value expect(removedSignatures).to.be.an("object"); // Ensure the object has the correct structure - const nodeAccountId = new AccountId(3).toString(); - const transactionId = transaction.transactionId.toString(); - - expect(removedSignatures).to.have.property(transactionId); - expect(removedSignatures[transactionId]).to.have.property( - nodeAccountId, + expect(removedSignatures).to.have.property( + key1.publicKey.toStringRaw(), + ); + expect(removedSignatures[key1.publicKey.toStringRaw()]).to.be.an( + "array", + ); + expect(removedSignatures).to.have.property( + key2.publicKey.toStringRaw(), + ); + expect(removedSignatures[key2.publicKey.toStringRaw()]).to.be.an( + "array", + ); + expect(removedSignatures).to.have.property( + key3.publicKey.toStringRaw(), ); - expect(removedSignatures[transactionId][nodeAccountId]).to.be.an( + expect(removedSignatures[key3.publicKey.toStringRaw()]).to.be.an( "array", ); // Ensure the removed signatures are in the expected format - const signaturesArray = - removedSignatures[transactionId][nodeAccountId]; - expect(signaturesArray.length).to.equal(3); - signaturesArray.forEach((sig) => { - expect(sig).to.be.instanceOf(Uint8Array); - }); + const signaturesArray1 = + removedSignatures[key1.publicKey.toStringRaw()]; + const signaturesArray2 = + removedSignatures[key2.publicKey.toStringRaw()]; + const signaturesArray3 = + removedSignatures[key3.publicKey.toStringRaw()]; + + [signaturesArray1, signaturesArray2, signaturesArray3].forEach( + (signaturesArray) => { + signaturesArray.forEach((sig) => { + expect(sig).to.be.instanceOf(Uint8Array); + }); + }, + ); }); it("should return an empty object when no signatures are present", function () { - // Ensure the transaction has no signatures - const signaturesBefore = transaction.getSignatures(); - expect(signaturesBefore.get(new AccountId(3)).size).to.equal(0); + expect(transaction._signerPublicKeys.size).to.equal(0); - // Ensure the object has the correct structure - const nodeAccountId = new AccountId(3).toString(); - const transactionId = transaction.transactionId.toString(); + // Clear all signatures and capture the returned value + const removedSignatures = transaction.removeAllSignatures(); + + // Check the format of the returned value + expect(removedSignatures).to.be.an("object"); + expect(Object.keys(removedSignatures)).to.have.lengthOf(0); + }); + + it("should return an empty object when no signatures are present", function () { + expect(transaction._signerPublicKeys.size).to.equal(0); // Clear all signatures and capture the returned value - const removedSignatures = transaction.clearAllSignatures(); + const removedSignatures = transaction.removeAllSignatures(); // Check the format of the returned value expect(removedSignatures).to.be.an("object"); - expect( - Object.keys(removedSignatures[transactionId][nodeAccountId]), - ).to.have.lengthOf(0); + expect(Object.keys(removedSignatures)).to.have.lengthOf(0); }); }); }); From e24e2def0eec1ed1b11c57ace509a236a5dc35e3 Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Mon, 2 Sep 2024 10:25:20 +0300 Subject: [PATCH 07/13] refactor: Remove redundant code Signed-off-by: ivaylogarnev-limechain --- test/unit/Transaction.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/unit/Transaction.js b/test/unit/Transaction.js index 85c7862f7..ceece7c91 100644 --- a/test/unit/Transaction.js +++ b/test/unit/Transaction.js @@ -16,7 +16,6 @@ import Client from "../../src/client/NodeClient.js"; import * as HashgraphProto from "@hashgraph/proto"; import Long from "long"; import BigNumber from "bignumber.js"; -import { PublicKey } from "@hashgraph/cryptography"; describe("Transaction", function () { it("toBytes", async function () { @@ -544,16 +543,5 @@ describe("Transaction", function () { expect(removedSignatures).to.be.an("object"); expect(Object.keys(removedSignatures)).to.have.lengthOf(0); }); - - it("should return an empty object when no signatures are present", function () { - expect(transaction._signerPublicKeys.size).to.equal(0); - - // Clear all signatures and capture the returned value - const removedSignatures = transaction.removeAllSignatures(); - - // Check the format of the returned value - expect(removedSignatures).to.be.an("object"); - expect(Object.keys(removedSignatures)).to.have.lengthOf(0); - }); }); }); From ef868b13d395d89244d73060ca1cf9681667b7ed Mon Sep 17 00:00:00 2001 From: ivaylonikolov7 Date: Mon, 16 Sep 2024 00:57:07 +0300 Subject: [PATCH 08/13] Merge branch 'main' into feat/2461-remove-clear-signatures Signed-off-by: ivaylogarnev-limechain --- CHANGELOG.md | 13 + common_js_test/pnpm-lock.yaml | 12 +- examples/react-native-example/yarn.lock | 16 +- examples/token-airdrop-example.js | 419 ++++++++++++ package.json | 6 +- packages/cryptography/package.json | 2 +- .../src/primitive/bip32.native.js | 5 +- packages/proto/package.json | 2 +- packages/proto/pnpm-lock.yaml | 14 +- packages/proto/src/proto | 2 +- src/Status.js | 47 ++ src/account/TransferTransaction.js | 542 +--------------- src/exports.js | 3 + src/token/AbstractTokenTransferTransaction.js | 585 +++++++++++++++++ src/token/AirdropPendingTransaction.js | 77 +++ src/token/PendingAirdropId.js | 167 +++++ src/token/PendingAirdropRecord.js | 69 ++ src/token/TokenAirdropTransaction.js | 187 ++++++ src/token/TokenCancelAirdropTransaction.js | 133 ++++ src/token/TokenClaimAirdropTransaction.js | 135 ++++ src/token/TokenNftTransfer.js | 1 - src/transaction/TransactionRecord.js | 21 + tck/methods/account.ts | 42 +- tck/params/account.ts | 6 + .../TokenAirdropIntegrationTest.js | 533 +++++++++++++++ .../TokenCancelAirdropTransaction.js | 586 +++++++++++++++++ .../TokenClaimAirdropTransaction.js | 606 ++++++++++++++++++ test/unit/AirdropCancelTransaction.js | 23 + test/unit/AirdropClaimTransaction.js | 21 + test/unit/TokenAirdropTransaction.js | 103 +++ 30 files changed, 3810 insertions(+), 568 deletions(-) create mode 100644 examples/token-airdrop-example.js create mode 100644 src/token/AbstractTokenTransferTransaction.js create mode 100644 src/token/AirdropPendingTransaction.js create mode 100644 src/token/PendingAirdropId.js create mode 100644 src/token/PendingAirdropRecord.js create mode 100644 src/token/TokenAirdropTransaction.js create mode 100644 src/token/TokenCancelAirdropTransaction.js create mode 100644 src/token/TokenClaimAirdropTransaction.js create mode 100644 test/integration/TokenAirdropIntegrationTest.js create mode 100644 test/integration/TokenCancelAirdropTransaction.js create mode 100644 test/integration/TokenClaimAirdropTransaction.js create mode 100644 test/unit/AirdropCancelTransaction.js create mode 100644 test/unit/AirdropClaimTransaction.js create mode 100644 test/unit/TokenAirdropTransaction.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 06f56b09f..8a61600f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v2.51.0 + +## What's Changed + +* feat: token airdrop transactions by @ivaylonikolov7 in https://github.com/hashgraph/hedera-sdk-js/pull/2492 +* fix: react native's bip32 should use array instead of buffer due to incompatibility by @ivaylonikolov7 in https://github.com/hashgraph/hedera-sdk-js/pull/2502 +* ci: update workflows to use latitude.sh based runners by @nathanklick in https://github.com/hashgraph/hedera-sdk-js/pull/2495 +* chore: Added prerequisites for building the project by @ivaylogarnev-limechain in https://github.com/hashgraph/hedera-sdk-js/pull/2478 +* chore(deps-dev): bump braces in /packages/cryptography by @dependabot in https://github.com/hashgraph/hedera-sdk-js/pull/2347 +* chore(deps): bump braces from 3.0.2 to 3.0.3 in /common_js_test by @dependabot in https://github.com/hashgraph/hedera-sdk-js/pull/2344 +* chore(deps-dev): bump braces from 3.0.2 to 3.0.3 in /packages/proto by @dependabot in https://github.com/hashgraph/hedera-sdk-js/pull/2345 +* chore(deps): bump braces in /examples/react-native-example by @dependabot in https://github.com/hashgraph/hedera-sdk-js/pull/2346 + ## v2.50.0 ## What's Changed diff --git a/common_js_test/pnpm-lock.yaml b/common_js_test/pnpm-lock.yaml index 5de2bf069..dbd3b3781 100644 --- a/common_js_test/pnpm-lock.yaml +++ b/common_js_test/pnpm-lock.yaml @@ -70,11 +70,11 @@ packages: concat-map: 0.0.1 dev: false - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} dependencies: - fill-range: 7.0.1 + fill-range: 7.1.1 dev: false /browser-stdout@1.3.1: @@ -116,7 +116,7 @@ packages: engines: {node: '>= 8.10.0'} dependencies: anymatch: 3.1.3 - braces: 3.0.2 + braces: 3.0.3 glob-parent: 5.1.2 is-binary-path: 2.1.0 is-glob: 4.0.3 @@ -193,8 +193,8 @@ packages: engines: {node: '>=10'} dev: false - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 diff --git a/examples/react-native-example/yarn.lock b/examples/react-native-example/yarn.lock index ea6e9112a..abc691f36 100644 --- a/examples/react-native-example/yarn.lock +++ b/examples/react-native-example/yarn.lock @@ -2866,11 +2866,11 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" brorand@^1.1.0: version "1.1.0" @@ -3844,10 +3844,10 @@ fetch-retry@^4.1.1: resolved "https://registry.npmjs.org/fetch-retry/-/fetch-retry-4.1.1.tgz" integrity sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" diff --git a/examples/token-airdrop-example.js b/examples/token-airdrop-example.js new file mode 100644 index 000000000..eda6b674b --- /dev/null +++ b/examples/token-airdrop-example.js @@ -0,0 +1,419 @@ +import { + Client, + PrivateKey, + AccountId, + AccountCreateTransaction, + TokenAirdropTransaction, + Hbar, + TokenCreateTransaction, + TokenType, + TokenMintTransaction, + AccountBalanceQuery, + TokenClaimAirdropTransaction, + TokenCancelAirdropTransaction, + TokenRejectTransaction, + NftId, +} from "@hashgraph/sdk"; + +import dotenv from "dotenv"; + +dotenv.config(); + +async function main() { + if ( + process.env.OPERATOR_ID == null || + process.env.OPERATOR_KEY == null || + process.env.HEDERA_NETWORK == null + ) { + throw new Error( + "Environment variables OPERATOR_ID, HEDERA_NETWORK, and OPERATOR_KEY are required.", + ); + } + + const client = Client.forName(process.env.HEDERA_NETWORK).setOperator( + AccountId.fromString(process.env.OPERATOR_ID), + PrivateKey.fromStringDer(process.env.OPERATOR_KEY), + ); + + const CID = [ + "QmNPCiNA3Dsu3K5FxDPMG5Q3fZRwVTg14EXA92uqEeSRXn", + "QmZ4dgAgt8owvnULxnKxNe8YqpavtVCXmc1Lt2XajFpJs9", + "QmPzY5GxevjyfMUF5vEAjtyRoigzWp47MiKAtLBduLMC1T", + "Qmd3kGgSrAwwSrhesYcY7K54f3qD7MDo38r7Po2dChtQx5", + "QmWgkKz3ozgqtnvbCLeh7EaR1H8u5Sshx3ZJzxkcrT3jbw", + ]; + + /** + * STEP 1: + * Create 4 accounts + */ + + const privateKey = PrivateKey.generateED25519(); + const { accountId: accountId1 } = await ( + await new AccountCreateTransaction() + .setKey(privateKey) + .setInitialBalance(new Hbar(10)) + .setMaxAutomaticTokenAssociations(-1) + .execute(client) + ).getReceipt(client); + + const privateKey2 = PrivateKey.generateED25519(); + const { accountId: accountId2 } = await ( + await new AccountCreateTransaction() + .setKey(privateKey2) + .setInitialBalance(new Hbar(10)) + .setMaxAutomaticTokenAssociations(1) + .execute(client) + ).getReceipt(client); + + const privateKey3 = PrivateKey.generateED25519(); + const { accountId: accountId3 } = await ( + await new AccountCreateTransaction() + .setKey(privateKey3) + .setInitialBalance(new Hbar(10)) + .setMaxAutomaticTokenAssociations(0) + .execute(client) + ).getReceipt(client); + + const treasuryKey = PrivateKey.generateED25519(); + const { accountId: treasuryAccount } = await ( + await new AccountCreateTransaction() + .setKey(treasuryKey) + .setInitialBalance(new Hbar(10)) + .setMaxAutomaticTokenAssociations(-1) + .execute(client) + ).getReceipt(client); + + /** + * STEP 2: + * Create FT and NFT mint + */ + + const INITIAL_SUPPLY = 300; + + const tokenCreateTx = await new TokenCreateTransaction() + .setTokenName("Fungible Token") + .setTokenSymbol("TFT") + .setTokenMemo("Example memo") + .setDecimals(3) + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(treasuryAccount) + .setAdminKey(client.operatorPublicKey) + .setFreezeKey(client.operatorPublicKey) + .setSupplyKey(client.operatorPublicKey) + .setMetadataKey(client.operatorPublicKey) + .setPauseKey(client.operatorPublicKey) + .freezeWith(client) + .sign(treasuryKey); + + const { tokenId } = await ( + await tokenCreateTx.execute(client) + ).getReceipt(client); + + const { tokenId: nftId } = await ( + await ( + await new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NonFungibleUnique) + .setTreasuryAccountId(treasuryAccount) + .setAdminKey(client.operatorPublicKey) + .setFreezeKey(client.operatorPublicKey) + .setSupplyKey(client.operatorPublicKey) + .setMetadataKey(client.operatorPublicKey) + .setPauseKey(client.operatorPublicKey) + .freezeWith(client) + .sign(treasuryKey) + ).execute(client) + ).getReceipt(client); + + let serialsNfts = []; + for (let i = 0; i < CID.length; i++) { + const { serials } = await ( + await new TokenMintTransaction() + .setTokenId(nftId) + .addMetadata(Buffer.from("-")) + .execute(client) + ).getReceipt(client); + + serialsNfts.push(serials[0]); + } + /** + * STEP 3: + * Airdrop fungible tokens to 3 accounts + */ + const AIRDROP_SUPPLY_PER_ACCOUNT = INITIAL_SUPPLY / 3; + const airdropRecord = await ( + await ( + await new TokenAirdropTransaction() + .addTokenTransfer( + tokenId, + treasuryAccount, + -AIRDROP_SUPPLY_PER_ACCOUNT, + ) + .addTokenTransfer( + tokenId, + accountId1, + AIRDROP_SUPPLY_PER_ACCOUNT, + ) + .addTokenTransfer( + tokenId, + treasuryAccount, + -AIRDROP_SUPPLY_PER_ACCOUNT, + ) + .addTokenTransfer( + tokenId, + accountId2, + AIRDROP_SUPPLY_PER_ACCOUNT, + ) + .addTokenTransfer( + tokenId, + treasuryAccount, + -AIRDROP_SUPPLY_PER_ACCOUNT, + ) + .addTokenTransfer( + tokenId, + accountId3, + AIRDROP_SUPPLY_PER_ACCOUNT, + ) + .freezeWith(client) + .sign(treasuryKey) + ).execute(client) + ).getRecord(client); + + /** + * STEP 4: Get the transaction record and see the pending airdrops + */ + + const { newPendingAirdrops } = airdropRecord; + console.log("Pending airdrops length", newPendingAirdrops.length); + console.log("Pending airdrop", newPendingAirdrops[0]); + + /** + * STEP 5: + * Query to verify account 1 and Account 2 have received the airdrops and Account 3 has not + */ + let account1Balance = await new AccountBalanceQuery() + .setAccountId(accountId1) + .execute(client); + + let account2Balance = await new AccountBalanceQuery() + .setAccountId(accountId2) + .execute(client); + + let account3Balance = await new AccountBalanceQuery() + .setAccountId(accountId3) + .execute(client); + + console.log( + "Account1 balance after airdrop: ", + account1Balance.tokens.get(tokenId).toInt(), + ); + console.log( + "Account2 balance after airdrop: ", + account2Balance.tokens.get(tokenId).toInt(), + ); + console.log( + "Account3 balance after airdrop: ", + account3Balance.tokens.get(tokenId), + ); + + /** + * Step 6: Claim the airdrop for Account 3 + */ + await ( + await ( + await new TokenClaimAirdropTransaction() + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .freezeWith(client) + .sign(privateKey3) + ).execute(client) + ).getReceipt(client); + + const account3BalanceAfterClaim = await new AccountBalanceQuery() + .setAccountId(accountId3) + .execute(client); + + console.log( + "Account3 balance after airdrop claim", + account3BalanceAfterClaim.tokens.get(tokenId).toInt(), + ); + + /** + * Step 7: + * Airdrop the NFTs to the 3 accounts + */ + const { newPendingAirdrops: newPendingAirdropsNfts } = await ( + await ( + await new TokenAirdropTransaction() + .addNftTransfer( + nftId, + serialsNfts[0], + treasuryAccount, + accountId1, + ) + .addNftTransfer( + nftId, + serialsNfts[1], + treasuryAccount, + accountId2, + ) + .addNftTransfer( + nftId, + serialsNfts[2], + treasuryAccount, + accountId3, + ) + .freezeWith(client) + .sign(treasuryKey) + ).execute(client) + ).getRecord(client); + + /** + * Step 8: + * Get the transaction record and verify two pending airdrops (for Account 2 & 3) + */ + console.log("Pending airdrops length", newPendingAirdropsNfts.length); + console.log("Pending airdrop for Account 0:", newPendingAirdropsNfts[0]); + console.log("Pending airdrop for Account 1:", newPendingAirdropsNfts[1]); + + /** + * Step 9: + * Query to verify Account 1 received the airdrop and Account 2 and Account 3 did not + */ + account1Balance = await new AccountBalanceQuery() + .setAccountId(accountId1) + .execute(client); + + account2Balance = await new AccountBalanceQuery() + .setAccountId(accountId2) + .execute(client); + + console.log( + "Account 1 NFT Balance after airdrop", + account1Balance.tokens.get(nftId).toInt(), + ); + console.log( + "Account 2 NFT Balance after airdrop", + account2Balance.tokens.get(nftId), + ); + console.log( + "Account 3 NFT Balance after airdrop", + account3Balance.tokens.get(nftId), + ); + + /** + * Step 10: + * Claim the airdrop for Account 2 + */ + await ( + await ( + await new TokenClaimAirdropTransaction() + .addPendingAirdropId(newPendingAirdropsNfts[0].airdropId) + .freezeWith(client) + .sign(privateKey2) + ).execute(client) + ).getReceipt(client); + + account2Balance = await new AccountBalanceQuery() + .setAccountId(accountId2) + .execute(client); + + console.log( + "Account 2 nft balance after claim: ", + account2Balance.tokens.get(nftId).toInt(), + ); + + /** + * Step 11: + * Cancel the airdrop for Account 3 + */ + console.log("Cancelling airdrop for account 3"); + await new TokenCancelAirdropTransaction() + .addPendingAirdropId(newPendingAirdropsNfts[1].airdropId) + .execute(client); + + account3Balance = await new AccountBalanceQuery() + .setAccountId(accountId3) + .execute(client); + + console.log( + "Account 3 nft balance after cancel: ", + account3Balance.tokens.get(nftId), + ); + + /** + * Step 12: + * Reject the NFT for Account 2 + */ + console.log("Rejecting NFT for account 2"); + await ( + await ( + await new TokenRejectTransaction() + .setOwnerId(accountId2) + .addNftId(new NftId(nftId, serialsNfts[1])) + .freezeWith(client) + .sign(privateKey2) + ).execute(client) + ).getReceipt(client); + + /** + * Step 13: + * Query to verify Account 2 no longer has the NFT + */ + account2Balance = await new AccountBalanceQuery() + .setAccountId(accountId2) + .execute(client); + console.log( + "Account 2 nft balance after reject: ", + account2Balance.tokens.get(nftId).toInt(), + ); + + /** + * Step 14: + * Query to verify treasury no longer has the NFT + */ + let treasuryBalance = await new AccountBalanceQuery() + .setAccountId(treasuryAccount) + .execute(client); + console.log( + "Treasury nft balance after reject: ", + treasuryBalance.tokens.get(nftId).toInt(), + ); + + /** + * Step 15: + * Reject the fungible tokens for Account 3 + */ + console.log("Rejecting fungible tokens for account 3: "); + await ( + await ( + await new TokenRejectTransaction() + .setOwnerId(accountId3) + .addTokenId(tokenId) + .freezeWith(client) + .sign(privateKey3) + ).execute(client) + ).getReceipt(client); + + account3Balance = await new AccountBalanceQuery() + .setAccountId(accountId3) + .execute(client); + + console.log( + "Account 3 balance after reject: ", + account3Balance.tokens.get(tokenId).toInt(), + ); + + treasuryBalance = await new AccountBalanceQuery() + .setAccountId(treasuryAccount) + .execute(client); + + console.log( + "Treasury balance after reject: ", + treasuryBalance.tokens.get(tokenId).toInt(), + ); + client.close(); +} + +void main(); diff --git a/package.json b/package.json index c5eadf5e5..86126e463 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/sdk", - "version": "2.50.0", + "version": "2.51.0", "description": "Hedera™ Hashgraph SDK", "types": "./lib/index.d.ts", "main": "./lib/index.cjs", @@ -57,8 +57,8 @@ "@ethersproject/bytes": "^5.7.0", "@ethersproject/rlp": "^5.7.0", "@grpc/grpc-js": "1.8.2", - "@hashgraph/cryptography": "1.4.8-beta.7", - "@hashgraph/proto": "2.15.0-beta.3", + "@hashgraph/cryptography": "1.4.8-beta.8", + "@hashgraph/proto": "2.15.0-beta.4", "axios": "^1.6.4", "bignumber.js": "^9.1.1", "bn.js": "^5.1.1", diff --git a/packages/cryptography/package.json b/packages/cryptography/package.json index 1b8831ecb..21d5cbaa7 100644 --- a/packages/cryptography/package.json +++ b/packages/cryptography/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/cryptography", - "version": "1.4.8-beta.7", + "version": "1.4.8-beta.8", "description": "Cryptographic utilities and primitives for the Hedera™ Hashgraph SDK", "main": "./lib/index.cjs", "types": "./lib/index.d.ts", diff --git a/packages/cryptography/src/primitive/bip32.native.js b/packages/cryptography/src/primitive/bip32.native.js index 5bf97c0d9..e7438a22c 100644 --- a/packages/cryptography/src/primitive/bip32.native.js +++ b/packages/cryptography/src/primitive/bip32.native.js @@ -60,7 +60,10 @@ export async function derive(parentKey, chainCode, index) { .getPrivate() .add(secp256k1.keyFromPrivate(IL).getPrivate()) .mod(N); - const hexZeroPadded = hex.hexZeroPadded(ki.toBuffer(), 32); + const hexZeroPadded = hex.hexZeroPadded( + Uint8Array.from(ki.toArray()), + 32, + ); // const ki = Buffer.from(ecc.privateAdd(this.privateKey!, IL)!); // In case ki == 0, proceed with the next value for i diff --git a/packages/proto/package.json b/packages/proto/package.json index ca407aaf2..ba4cead46 100644 --- a/packages/proto/package.json +++ b/packages/proto/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/proto", - "version": "2.15.0-beta.3", + "version": "2.15.0-beta.4", "description": "Protobufs for the Hedera™ Hashgraph SDK", "main": "lib/index.js", "browser": "src/index.js", diff --git a/packages/proto/pnpm-lock.yaml b/packages/proto/pnpm-lock.yaml index f7d79554c..96418148a 100644 --- a/packages/proto/pnpm-lock.yaml +++ b/packages/proto/pnpm-lock.yaml @@ -1238,11 +1238,11 @@ packages: balanced-match: 1.0.2 dev: true - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} dependencies: - fill-range: 7.0.1 + fill-range: 7.1.1 dev: true /browserslist@4.21.10: @@ -1333,7 +1333,7 @@ packages: requiresBuild: true dependencies: anymatch: 3.1.2 - braces: 3.0.2 + braces: 3.0.3 glob-parent: 5.1.2 is-binary-path: 2.1.0 is-glob: 4.0.3 @@ -2037,8 +2037,8 @@ packages: flat-cache: 3.0.4 dev: true - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} requiresBuild: true dependencies: @@ -2715,7 +2715,7 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 dev: true diff --git a/packages/proto/src/proto b/packages/proto/src/proto index 141302ce2..d88484a3b 160000 --- a/packages/proto/src/proto +++ b/packages/proto/src/proto @@ -1 +1 @@ -Subproject commit 141302ce26bd0c2023d4d031ed207d1e05917688 +Subproject commit d88484a3b2e2100b1b9a7ed77d476baf80a58303 diff --git a/src/Status.js b/src/Status.js index 9a4d70cd7..b3c9b8924 100644 --- a/src/Status.js +++ b/src/Status.js @@ -689,6 +689,14 @@ export default class Status { return "PENDING_NFT_AIRDROP_ALREADY_EXISTS"; case Status.AccountHasPendingAirdrops: return "ACCOUNT_HAS_PENDING_AIRDROPS"; + case Status.ThrottledAtConsensus: + return "THROTTLED_AT_CONSENSUS"; + case Status.InvalidPendingAirdropId: + return "INVALID_PENDING_AIRDROP_ID"; + case Status.TokenAirdropWithFallbackRoyalty: + return "TOKEN_AIRDROP_WITH_FALLBACK_ROYALTY"; + case Status.InvalidTokenInPendingAirdrop: + return "INVALID_TOKEN_IN_PENDING_AIRDROP"; default: return `UNKNOWN (${this._code})`; } @@ -1349,6 +1357,14 @@ export default class Status { return Status.PendingNftAirdropAlreadyExists; case 365: return Status.AccountHasPendingAirdrops; + case 366: + return Status.ThrottledAtConsensus; + case 367: + return Status.InvalidPendingAirdropId; + case 368: + return Status.TokenAirdropWithFallbackRoyalty; + case 369: + return Status.InvalidTokenInPendingAirdrop; default: throw new Error( `(BUG) Status.fromCode() does not handle code: ${code}`, @@ -3030,3 +3046,34 @@ Status.PendingNftAirdropAlreadyExists = new Status(364); * this transaction. */ Status.AccountHasPendingAirdrops = new Status(365); + +/** + * Consensus throttle did not allow execution of this transaction.
+ * The transaction should be retried after a modest delay. + */ +Status.ThrottledAtConsensus = new Status(366); + +/** + * The provided pending airdrop id is invalid.
+ * This pending airdrop MAY already be claimed or cancelled. + *

+ * The client SHOULD query a mirror node to determine the current status of + * the pending airdrop. + */ +Status.InvalidPendingAirdropId = new Status(367); + +/** + * The token to be airdropped has a fallback royalty fee and cannot be + * sent or claimed via an airdrop transaction. + */ +Status.TokenAirdropWithFallbackRoyalty = new Status(368); + +/** + * This airdrop claim is for a pending airdrop with an invalid token.
+ * The token might be deleted, or the sender may not have enough tokens + * to fulfill the offer. + *

+ * The client SHOULD query mirror node to determine the status of the pending + * airdrop and whether the sender can fulfill the offer. + */ +Status.InvalidTokenInPendingAirdrop = new Status(369); diff --git a/src/account/TransferTransaction.js b/src/account/TransferTransaction.js index 7df47038f..ac07e0769 100644 --- a/src/account/TransferTransaction.js +++ b/src/account/TransferTransaction.js @@ -24,16 +24,12 @@ import AccountId from "./AccountId.js"; import Transaction, { TRANSACTION_REGISTRY, } from "../transaction/Transaction.js"; -import Long from "long"; -import NullableTokenDecimalMap from "./NullableTokenDecimalMap.js"; import Transfer from "../Transfer.js"; import TokenTransfer from "../token/TokenTransfer.js"; -import TokenTransferMap from "./TokenTransferMap.js"; import HbarTransferMap from "./HbarTransferMap.js"; -import TokenNftTransferMap from "./TokenNftTransferMap.js"; -import TokenTransferAccountMap from "./TokenTransferAccountMap.js"; import TokenNftTransfer from "../token/TokenNftTransfer.js"; import NftId from "../token/NftId.js"; +import AbstractTokenTransferTransaction from "../token/AbstractTokenTransferTransaction.js"; /** * @typedef {import("../long.js").LongObject} LongObject @@ -48,10 +44,6 @@ import NftId from "../token/NftId.js"; * @typedef {import("@hashgraph/proto").proto.ITransactionBody} HashgraphProto.proto.ITransactionBody * @typedef {import("@hashgraph/proto").proto.ITransactionResponse} HashgraphProto.proto.ITransactionResponse * @typedef {import("@hashgraph/proto").proto.ICryptoTransferTransactionBody} HashgraphProto.proto.ICryptoTransferTransactionBody - * @typedef {import("@hashgraph/proto").proto.ITokenID} HashgraphProto.proto.ITokenID - * @typedef {import("@hashgraph/proto").proto.IAccountID} HashgraphProto.proto.IAccountID - * @typedef {import("@hashgraph/proto").proto.IAccountAmount} HashgraphProto.proto.IAccountAmount - * @typedef {import("@hashgraph/proto").proto.ITokenTransferList} HashgraphProto.proto.ITokenTransferList */ /** @@ -91,7 +83,7 @@ import NftId from "../token/NftId.js"; /** * Transfers a new Hedera™ crypto-currency token. */ -export default class TransferTransaction extends Transaction { +export default class TransferTransaction extends AbstractTokenTransferTransaction { /** * @param {object} [props] * @param {(TransferTokensInput)[]} [props.tokenTransfers] @@ -101,52 +93,19 @@ export default class TransferTransaction extends Transaction { constructor(props = {}) { super(); - /** - * @private - * @type {TokenTransfer[]} - */ - this._tokenTransfers = []; - /** * @private * @type {Transfer[]} */ this._hbarTransfers = []; - /** - * @private - * @type {TokenNftTransfer[]} - */ - this._nftTransfers = []; - this._defaultMaxTransactionFee = new Hbar(1); - for (const transfer of props.tokenTransfers != null - ? props.tokenTransfers - : []) { - this.addTokenTransfer( - transfer.tokenId, - transfer.accountId, - transfer.amount, - ); - } - for (const transfer of props.hbarTransfers != null ? props.hbarTransfers : []) { this.addHbarTransfer(transfer.accountId, transfer.amount); } - - for (const transfer of props.nftTransfers != null - ? props.nftTransfers - : []) { - this.addNftTransfer( - transfer.tokenId, - transfer.serial, - transfer.sender, - transfer.recipient, - ); - } } /** @@ -203,158 +162,6 @@ export default class TransferTransaction extends Transaction { ); } - /** - * @returns {TokenTransferMap} - */ - get tokenTransfers() { - const map = new TokenTransferMap(); - - for (const transfer of this._tokenTransfers) { - let transferMap = map.get(transfer.tokenId); - - if (transferMap != null) { - transferMap._set(transfer.accountId, transfer.amount); - } else { - transferMap = new TokenTransferAccountMap(); - transferMap._set(transfer.accountId, transfer.amount); - map._set(transfer.tokenId, transferMap); - } - } - - return map; - } - - /** - * @param {TokenId | string} tokenId - * @param {AccountId | string} accountId - * @param {number | Long} amount - * @param {boolean} isApproved - * @returns {this} - */ - _addTokenTransfer(tokenId, accountId, amount, isApproved) { - this._requireNotFrozen(); - - const token = - tokenId instanceof TokenId ? tokenId : TokenId.fromString(tokenId); - const account = - accountId instanceof AccountId - ? accountId - : AccountId.fromString(accountId); - const value = amount instanceof Long ? amount : Long.fromNumber(amount); - - for (const tokenTransfer of this._tokenTransfers) { - if ( - tokenTransfer.tokenId.compare(token) === 0 && - tokenTransfer.accountId.compare(account) === 0 - ) { - tokenTransfer.amount = tokenTransfer.amount.add(value); - tokenTransfer.expectedDecimals = null; - return this; - } - } - - this._tokenTransfers.push( - new TokenTransfer({ - tokenId, - accountId, - expectedDecimals: null, - amount, - isApproved, - }), - ); - - return this; - } - - /** - * @param {TokenId | string} tokenId - * @param {AccountId | string} accountId - * @param {number | Long} amount - * @returns {this} - */ - addTokenTransfer(tokenId, accountId, amount) { - return this._addTokenTransfer(tokenId, accountId, amount, false); - } - - /** - * @param {TokenId | string} tokenId - * @param {AccountId | string} accountId - * @param {number | Long} amount - * @returns {this} - */ - addApprovedTokenTransfer(tokenId, accountId, amount) { - return this._addTokenTransfer(tokenId, accountId, amount, true); - } - - /** - * @param {TokenId | string} tokenId - * @param {AccountId | string} accountId - * @param {number | Long} amount - * @param {number} decimals - * @returns {this} - */ - addTokenTransferWithDecimals(tokenId, accountId, amount, decimals) { - this._requireNotFrozen(); - - const token = - tokenId instanceof TokenId ? tokenId : TokenId.fromString(tokenId); - const account = - accountId instanceof AccountId - ? accountId - : AccountId.fromString(accountId); - const value = amount instanceof Long ? amount : Long.fromNumber(amount); - - let found = false; - - for (const tokenTransfer of this._tokenTransfers) { - if (tokenTransfer.tokenId.compare(token) === 0) { - if ( - tokenTransfer.expectedDecimals != null && - tokenTransfer.expectedDecimals !== decimals - ) { - throw new Error("expected decimals mis-match"); - } else { - tokenTransfer.expectedDecimals = decimals; - } - - if (tokenTransfer.accountId.compare(account) === 0) { - tokenTransfer.amount = tokenTransfer.amount.add(value); - tokenTransfer.expectedDecimals = decimals; - found = true; - } - } - } - - if (found) { - return this; - } - - this._tokenTransfers.push( - new TokenTransfer({ - tokenId, - accountId, - expectedDecimals: decimals, - amount, - isApproved: false, - }), - ); - - return this; - } - - /** - * @returns {NullableTokenDecimalMap} - */ - get tokenIdDecimals() { - const map = new NullableTokenDecimalMap(); - - for (const transfer of this._tokenTransfers) { - map._set(transfer.tokenId, transfer.expectedDecimals); - } - - return map; - } - /** * @returns {HbarTransferMap} */ @@ -452,186 +259,6 @@ export default class TransferTransaction extends Transaction { } } - /** - * @returns {TokenNftTransferMap} - */ - get nftTransfers() { - const map = new TokenNftTransferMap(); - - for (const transfer of this._nftTransfers) { - const transferList = map.get(transfer.tokenId); - - const nftTransfer = { - sender: transfer.senderAccountId, - recipient: transfer.receiverAccountId, - serial: transfer.serialNumber, - isApproved: transfer.isApproved, - }; - - if (transferList != null) { - transferList.push(nftTransfer); - } else { - map._set(transfer.tokenId, [nftTransfer]); - } - } - - return map; - } - - /** - * @param {boolean} isApproved - * @param {NftId | TokenId | string} tokenIdOrNftId - * @param {AccountId | string | Long | number} senderAccountIdOrSerialNumber - * @param {AccountId | string} receiverAccountIdOrSenderAccountId - * @param {(AccountId | string)=} receiver - * @returns {TransferTransaction} - */ - _addNftTransfer( - isApproved, - tokenIdOrNftId, - senderAccountIdOrSerialNumber, - receiverAccountIdOrSenderAccountId, - receiver, - ) { - this._requireNotFrozen(); - - let nftId; - let senderAccountId; - let receiverAccountId; - - if (tokenIdOrNftId instanceof NftId) { - nftId = tokenIdOrNftId; - senderAccountId = - typeof senderAccountIdOrSerialNumber === "string" - ? AccountId.fromString(senderAccountIdOrSerialNumber) - : /** @type {AccountId} */ (senderAccountIdOrSerialNumber); - receiverAccountId = - typeof receiverAccountIdOrSenderAccountId === "string" - ? AccountId.fromString(receiverAccountIdOrSenderAccountId) - : /** @type {AccountId} */ ( - receiverAccountIdOrSenderAccountId - ); - } else if (tokenIdOrNftId instanceof TokenId) { - nftId = new NftId( - tokenIdOrNftId, - /** @type {Long} */ (senderAccountIdOrSerialNumber), - ); - senderAccountId = - typeof receiverAccountIdOrSenderAccountId === "string" - ? AccountId.fromString(receiverAccountIdOrSenderAccountId) - : /** @type {AccountId} */ ( - receiverAccountIdOrSenderAccountId - ); - receiverAccountId = - typeof receiver === "string" - ? AccountId.fromString(receiver) - : /** @type {AccountId} */ (receiver); - } else { - try { - nftId = NftId.fromString(tokenIdOrNftId); - senderAccountId = - typeof senderAccountIdOrSerialNumber === "string" - ? AccountId.fromString(senderAccountIdOrSerialNumber) - : /** @type {AccountId} */ ( - senderAccountIdOrSerialNumber - ); - receiverAccountId = - typeof receiverAccountIdOrSenderAccountId === "string" - ? AccountId.fromString( - receiverAccountIdOrSenderAccountId, - ) - : /** @type {AccountId} */ ( - receiverAccountIdOrSenderAccountId - ); - } catch (_) { - const tokenId = TokenId.fromString(tokenIdOrNftId); - nftId = new NftId( - tokenId, - /** @type {Long} */ (senderAccountIdOrSerialNumber), - ); - senderAccountId = - typeof receiverAccountIdOrSenderAccountId === "string" - ? AccountId.fromString( - receiverAccountIdOrSenderAccountId, - ) - : /** @type {AccountId} */ ( - receiverAccountIdOrSenderAccountId - ); - receiverAccountId = - typeof receiver === "string" - ? AccountId.fromString(receiver) - : /** @type {AccountId} */ (receiver); - } - } - - for (const nftTransfer of this._nftTransfers) { - if ( - nftTransfer.tokenId.compare(nftId.tokenId) === 0 && - nftTransfer.serialNumber.compare(nftId.serial) === 0 - ) { - nftTransfer.senderAccountId = senderAccountId; - nftTransfer.receiverAccountId = receiverAccountId; - return this; - } - } - - this._nftTransfers.push( - new TokenNftTransfer({ - tokenId: nftId.tokenId, - serialNumber: nftId.serial, - senderAccountId, - receiverAccountId, - isApproved, - }), - ); - - return this; - } - - /** - * @param {NftId | TokenId | string} tokenIdOrNftId - * @param {AccountId | string | Long | number} senderAccountIdOrSerialNumber - * @param {AccountId | string} receiverAccountIdOrSenderAccountId - * @param {(AccountId | string)=} receiver - * @returns {TransferTransaction} - */ - addNftTransfer( - tokenIdOrNftId, - senderAccountIdOrSerialNumber, - receiverAccountIdOrSenderAccountId, - receiver, - ) { - return this._addNftTransfer( - false, - tokenIdOrNftId, - senderAccountIdOrSerialNumber, - receiverAccountIdOrSenderAccountId, - receiver, - ); - } - - /** - * @param {NftId | TokenId | string} tokenIdOrNftId - * @param {AccountId | string | Long | number} senderAccountIdOrSerialNumber - * @param {AccountId | string} receiverAccountIdOrSenderAccountId - * @param {(AccountId | string)=} receiver - * @returns {TransferTransaction} - */ - addApprovedNftTransfer( - tokenIdOrNftId, - senderAccountIdOrSerialNumber, - receiverAccountIdOrSenderAccountId, - receiver, - ) { - return this._addNftTransfer( - true, - tokenIdOrNftId, - senderAccountIdOrSerialNumber, - receiverAccountIdOrSenderAccountId, - receiver, - ); - } - /** * @deprecated - Use `addApprovedHbarTransfer()` instead * @param {AccountId | string} accountId @@ -727,154 +354,7 @@ export default class TransferTransaction extends Transaction { * @returns {HashgraphProto.proto.ICryptoTransferTransactionBody} */ _makeTransactionData() { - /** @type {{tokenId: TokenId; expectedDecimals: number | null; transfers: TokenTransfer[]; nftTransfers: TokenNftTransfer[];}[]} */ - const tokenTransferList = []; - - this._tokenTransfers.sort((a, b) => { - const compare = a.tokenId.compare(b.tokenId); - - if (compare !== 0) { - return compare; - } - - return a.accountId.compare(b.accountId); - }); - - this._nftTransfers.sort((a, b) => { - const senderComparision = a.senderAccountId.compare( - b.senderAccountId, - ); - if (senderComparision != 0) { - return senderComparision; - } - - const recipientComparision = a.receiverAccountId.compare( - b.receiverAccountId, - ); - if (recipientComparision != 0) { - return recipientComparision; - } - - return a.serialNumber.compare(b.serialNumber); - }); - - let i = 0; - let j = 0; - while ( - i < this._tokenTransfers.length || - j < this._nftTransfers.length - ) { - if ( - i < this._tokenTransfers.length && - j < this._nftTransfers.length - ) { - const iTokenId = this._tokenTransfers[i].tokenId; - const jTokenId = this._nftTransfers[j].tokenId; - - const last = - tokenTransferList.length > 0 - ? tokenTransferList[tokenTransferList.length - 1] - : null; - const lastTokenId = last != null ? last.tokenId : null; - - if ( - last != null && - lastTokenId != null && - lastTokenId.compare(iTokenId) === 0 - ) { - last.transfers.push(this._tokenTransfers[i++]); - continue; - } - - if ( - last != null && - lastTokenId != null && - lastTokenId.compare(jTokenId) === 0 - ) { - last.nftTransfers.push(this._nftTransfers[j++]); - continue; - } - - const result = iTokenId.compare(jTokenId); - - if (result === 0) { - tokenTransferList.push({ - tokenId: iTokenId, - expectedDecimals: - this._tokenTransfers[i].expectedDecimals, - transfers: [this._tokenTransfers[i++]], - nftTransfers: [this._nftTransfers[j++]], - }); - } else if (result < 0) { - tokenTransferList.push({ - tokenId: iTokenId, - expectedDecimals: - this._tokenTransfers[i].expectedDecimals, - transfers: [this._tokenTransfers[i++]], - nftTransfers: [], - }); - } else { - tokenTransferList.push({ - tokenId: jTokenId, - expectedDecimals: null, - transfers: [], - nftTransfers: [this._nftTransfers[j++]], - }); - } - } else if (i < this._tokenTransfers.length) { - const iTokenId = this._tokenTransfers[i].tokenId; - - let last; - for (const transfer of tokenTransferList) { - if (transfer.tokenId.compare(iTokenId) === 0) { - last = transfer; - } - } - const lastTokenId = last != null ? last.tokenId : null; - - if ( - last != null && - lastTokenId != null && - lastTokenId.compare(iTokenId) === 0 - ) { - last.transfers.push(this._tokenTransfers[i++]); - continue; - } - - tokenTransferList.push({ - tokenId: iTokenId, - expectedDecimals: this._tokenTransfers[i].expectedDecimals, - transfers: [this._tokenTransfers[i++]], - nftTransfers: [], - }); - } else if (j < this._nftTransfers.length) { - const jTokenId = this._nftTransfers[j].tokenId; - - let last; - for (const transfer of tokenTransferList) { - if (transfer.tokenId.compare(jTokenId) === 0) { - last = transfer; - } - } - const lastTokenId = last != null ? last.tokenId : null; - - if ( - last != null && - lastTokenId != null && - lastTokenId.compare(jTokenId) === 0 - ) { - last.nftTransfers.push(this._nftTransfers[j++]); - continue; - } - - tokenTransferList.push({ - tokenId: jTokenId, - expectedDecimals: null, - transfers: [], - nftTransfers: [this._nftTransfers[j++]], - }); - } - } + const { tokenTransfers } = super._makeTransactionData(); this._hbarTransfers.sort((a, b) => a.accountId.compare(b.accountId)); @@ -888,21 +368,7 @@ export default class TransferTransaction extends Transaction { }; }), }, - tokenTransfers: tokenTransferList.map((tokenTransfer) => { - return { - token: tokenTransfer.tokenId._toProtobuf(), - expectedDecimals: - tokenTransfer.expectedDecimals != null - ? { value: tokenTransfer.expectedDecimals } - : null, - transfers: tokenTransfer.transfers.map((transfer) => - transfer._toProtobuf(), - ), - nftTransfers: tokenTransfer.nftTransfers.map((transfer) => - transfer._toProtobuf(), - ), - }; - }), + tokenTransfers, }; } diff --git a/src/exports.js b/src/exports.js index c82db744c..cb0a1e93a 100644 --- a/src/exports.js +++ b/src/exports.js @@ -33,6 +33,9 @@ export { default as PublicKey } from "./PublicKey.js"; export { default as KeyList } from "./KeyList.js"; export { default as Key } from "./Key.js"; export { default as Mnemonic } from "./Mnemonic.js"; +export { default as TokenAirdropTransaction } from "./token/TokenAirdropTransaction.js"; +export { default as TokenClaimAirdropTransaction } from "./token/TokenClaimAirdropTransaction.js"; +export { default as TokenCancelAirdropTransaction } from "./token/TokenCancelAirdropTransaction.js"; // eslint-disable-next-line deprecation/deprecation export { default as AccountAllowanceAdjustTransaction } from "./account/AccountAllowanceAdjustTransaction.js"; export { default as AccountAllowanceApproveTransaction } from "./account/AccountAllowanceApproveTransaction.js"; diff --git a/src/token/AbstractTokenTransferTransaction.js b/src/token/AbstractTokenTransferTransaction.js new file mode 100644 index 000000000..7a0432fdb --- /dev/null +++ b/src/token/AbstractTokenTransferTransaction.js @@ -0,0 +1,585 @@ +/*- + * ‌ + * Hedera JavaScript SDK + * ​ + * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +import TokenTransfer from "./TokenTransfer.js"; +import TokenNftTransfer from "../token/TokenNftTransfer.js"; +import TokenId from "./TokenId.js"; +import NftId from "./NftId.js"; +import AccountId from "../account/AccountId.js"; +import Transaction from "../transaction/Transaction.js"; +import Long from "long"; +import NullableTokenDecimalMap from "../account/NullableTokenDecimalMap.js"; +import TokenNftTransferMap from "../account/TokenNftTransferMap.js"; + +/** + * @namespace proto + * @typedef {import("@hashgraph/proto").proto.ITokenAirdropTransactionBody} HashgraphProto.proto.ITokenAirdropTransactionBody + */ + +/** + * @typedef {object} TransferTokensInput + * @property {TokenId | string} tokenId + * @property {AccountId | string} accountId + * @property {Long | number} amount + */ + +/** + * @typedef {object} TransferNftInput + * @property {TokenId | string} tokenId + * @property {AccountId | string} sender + * @property {AccountId | string} recipient + * @property {Long | number} serial + */ + +export default class AbstractTokenTransferTransaction extends Transaction { + /** + * @param {object} [props] + * @param {(TransferTokensInput)[]} [props.tokenTransfers] + * @param {(TransferNftInput)[]} [props.nftTransfers] + */ + constructor(props = {}) { + super(); + + /** + * @protected + * @type {TokenTransfer[]} + */ + this._tokenTransfers = []; + + /** + * @protected + * @type {TokenNftTransfer[]} + */ + this._nftTransfers = []; + + for (const transfer of props.tokenTransfers != null + ? props.tokenTransfers + : []) { + this.addTokenTransfer( + transfer.tokenId, + transfer.accountId, + transfer.amount, + ); + } + + for (const transfer of props.nftTransfers != null + ? props.nftTransfers + : []) { + this.addNftTransfer( + transfer.tokenId, + transfer.serial, + transfer.sender, + transfer.recipient, + ); + } + } + + /** + * @param {NftId | TokenId | string} tokenIdOrNftId + * @param {AccountId | string | Long | number} senderAccountIdOrSerialNumber + * @param {AccountId | string} receiverAccountIdOrSenderAccountId + * @param {(AccountId | string)=} receiver + * @returns {this} + */ + addNftTransfer( + tokenIdOrNftId, + senderAccountIdOrSerialNumber, + receiverAccountIdOrSenderAccountId, + receiver, + ) { + return this._addNftTransfer( + false, + tokenIdOrNftId, + senderAccountIdOrSerialNumber, + receiverAccountIdOrSenderAccountId, + receiver, + ); + } + + /** + * @param {TokenId | string} tokenId + * @param {AccountId | string} accountId + * @param {number | Long} amount + * @param {boolean} isApproved + * @param {number | null} expectedDecimals + * @returns {this} + */ + _addTokenTransfer( + tokenId, + accountId, + amount, + isApproved, + expectedDecimals, + ) { + this._requireNotFrozen(); + + const token = + tokenId instanceof TokenId ? tokenId : TokenId.fromString(tokenId); + const account = + accountId instanceof AccountId + ? accountId + : AccountId.fromString(accountId); + const value = amount instanceof Long ? amount : Long.fromNumber(amount); + + for (const tokenTransfer of this._tokenTransfers) { + if ( + tokenTransfer.tokenId.compare(token) === 0 && + tokenTransfer.accountId.compare(account) === 0 + ) { + tokenTransfer.amount = tokenTransfer.amount.add(value); + tokenTransfer.expectedDecimals = expectedDecimals; + return this; + } + } + + this._tokenTransfers.push( + new TokenTransfer({ + tokenId, + accountId, + expectedDecimals: expectedDecimals, + amount, + isApproved, + }), + ); + + return this; + } + + /** + * @param {TokenId | string} tokenId + * @param {AccountId | string} accountId + * @param {number | Long} amount + * @returns {this} + */ + addTokenTransfer(tokenId, accountId, amount) { + return this._addTokenTransfer(tokenId, accountId, amount, false, null); + } + + /** + * @param {boolean} isApproved + * @param {NftId | TokenId | string} tokenIdOrNftId + * @param {AccountId | string | Long | number} senderAccountIdOrSerialNumber + * @param {AccountId | string} receiverAccountIdOrSenderAccountId + * @param {(AccountId | string)=} receiver + * @returns {this} + */ + _addNftTransfer( + isApproved, + tokenIdOrNftId, + senderAccountIdOrSerialNumber, + receiverAccountIdOrSenderAccountId, + receiver, + ) { + this._requireNotFrozen(); + + let nftId; + let senderAccountId; + let receiverAccountId; + + if (tokenIdOrNftId instanceof NftId) { + nftId = tokenIdOrNftId; + senderAccountId = + typeof senderAccountIdOrSerialNumber === "string" + ? AccountId.fromString(senderAccountIdOrSerialNumber) + : /** @type {AccountId} */ (senderAccountIdOrSerialNumber); + receiverAccountId = + typeof receiverAccountIdOrSenderAccountId === "string" + ? AccountId.fromString(receiverAccountIdOrSenderAccountId) + : /** @type {AccountId} */ ( + receiverAccountIdOrSenderAccountId + ); + } else if (tokenIdOrNftId instanceof TokenId) { + nftId = new NftId( + tokenIdOrNftId, + /** @type {Long} */ (senderAccountIdOrSerialNumber), + ); + senderAccountId = + typeof receiverAccountIdOrSenderAccountId === "string" + ? AccountId.fromString(receiverAccountIdOrSenderAccountId) + : /** @type {AccountId} */ ( + receiverAccountIdOrSenderAccountId + ); + receiverAccountId = + typeof receiver === "string" + ? AccountId.fromString(receiver) + : /** @type {AccountId} */ (receiver); + } else { + try { + nftId = NftId.fromString(tokenIdOrNftId); + senderAccountId = + typeof senderAccountIdOrSerialNumber === "string" + ? AccountId.fromString(senderAccountIdOrSerialNumber) + : /** @type {AccountId} */ ( + senderAccountIdOrSerialNumber + ); + receiverAccountId = + typeof receiverAccountIdOrSenderAccountId === "string" + ? AccountId.fromString( + receiverAccountIdOrSenderAccountId, + ) + : /** @type {AccountId} */ ( + receiverAccountIdOrSenderAccountId + ); + } catch (_) { + const tokenId = TokenId.fromString(tokenIdOrNftId); + nftId = new NftId( + tokenId, + /** @type {Long} */ (senderAccountIdOrSerialNumber), + ); + senderAccountId = + typeof receiverAccountIdOrSenderAccountId === "string" + ? AccountId.fromString( + receiverAccountIdOrSenderAccountId, + ) + : /** @type {AccountId} */ ( + receiverAccountIdOrSenderAccountId + ); + receiverAccountId = + typeof receiver === "string" + ? AccountId.fromString(receiver) + : /** @type {AccountId} */ (receiver); + } + } + + for (const nftTransfer of this._nftTransfers) { + if ( + nftTransfer.tokenId.compare(nftId.tokenId) === 0 && + nftTransfer.serialNumber.compare(nftId.serial) === 0 + ) { + nftTransfer.senderAccountId = senderAccountId; + nftTransfer.receiverAccountId = receiverAccountId; + return this; + } + } + + this._nftTransfers.push( + new TokenNftTransfer({ + tokenId: nftId.tokenId, + serialNumber: nftId.serial, + senderAccountId, + receiverAccountId, + isApproved, + }), + ); + + return this; + } + + /** + * @param {NftId | TokenId | string} tokenIdOrNftId + * @param {AccountId | string | Long | number} senderAccountIdOrSerialNumber + * @param {AccountId | string} receiverAccountIdOrSenderAccountId + * @param {(AccountId | string)=} receiver + * @returns {this} + */ + addApprovedNftTransfer( + tokenIdOrNftId, + senderAccountIdOrSerialNumber, + receiverAccountIdOrSenderAccountId, + receiver, + ) { + return this._addNftTransfer( + true, + tokenIdOrNftId, + senderAccountIdOrSerialNumber, + receiverAccountIdOrSenderAccountId, + receiver, + ); + } + + /** + * @param {TokenId | string} tokenId + * @param {AccountId | string} accountId + * @param {number | Long} amount + * @returns {this} + */ + addApprovedTokenTransfer(tokenId, accountId, amount) { + return this._addTokenTransfer(tokenId, accountId, amount, true, null); + } + + /** + * @param {TokenId | string} tokenId + * @param {AccountId | string} accountId + * @param {number | Long} amount + * @param {number} decimals + * @returns {this} + */ + addTokenTransferWithDecimals(tokenId, accountId, amount, decimals) { + this._requireNotFrozen(); + + const token = + tokenId instanceof TokenId ? tokenId : TokenId.fromString(tokenId); + const account = + accountId instanceof AccountId + ? accountId + : AccountId.fromString(accountId); + const value = amount instanceof Long ? amount : Long.fromNumber(amount); + + let found = false; + + for (const tokenTransfer of this._tokenTransfers) { + if (tokenTransfer.tokenId.compare(token) === 0) { + if ( + tokenTransfer.expectedDecimals != null && + tokenTransfer.expectedDecimals !== decimals + ) { + throw new Error("expected decimals mis-match"); + } else { + tokenTransfer.expectedDecimals = decimals; + } + + if (tokenTransfer.accountId.compare(account) === 0) { + tokenTransfer.amount = tokenTransfer.amount.add(value); + tokenTransfer.expectedDecimals = decimals; + found = true; + } + } + } + + if (found) { + return this; + } + + this._tokenTransfers.push( + new TokenTransfer({ + tokenId, + accountId, + expectedDecimals: decimals, + amount, + isApproved: false, + }), + ); + + return this; + } + + /** + * @returns {NullableTokenDecimalMap} + */ + get tokenIdDecimals() { + const map = new NullableTokenDecimalMap(); + + for (const transfer of this._tokenTransfers) { + map._set(transfer.tokenId, transfer.expectedDecimals); + } + + return map; + } + + /** + * @returns {TokenNftTransferMap} + */ + get nftTransfers() { + const map = new TokenNftTransferMap(); + + for (const transfer of this._nftTransfers) { + const transferList = map.get(transfer.tokenId); + + const nftTransfer = { + sender: transfer.senderAccountId, + recipient: transfer.receiverAccountId, + serial: transfer.serialNumber, + isApproved: transfer.isApproved, + }; + + if (transferList != null) { + transferList.push(nftTransfer); + } else { + map._set(transfer.tokenId, [nftTransfer]); + } + } + + return map; + } + + /** + * @override + * @protected + * @returns {HashgraphProto.proto.ITokenAirdropTransactionBody} + */ + _makeTransactionData() { + /** @type {{tokenId: TokenId; expectedDecimals: number | null; transfers: TokenTransfer[]; nftTransfers: TokenNftTransfer[];}[]} */ + const tokenTransferList = []; + + this._tokenTransfers.sort((a, b) => { + const compare = a.tokenId.compare(b.tokenId); + + if (compare !== 0) { + return compare; + } + + return a.accountId.compare(b.accountId); + }); + + this._nftTransfers.sort((a, b) => { + const senderComparision = a.senderAccountId.compare( + b.senderAccountId, + ); + if (senderComparision != 0) { + return senderComparision; + } + + const recipientComparision = a.receiverAccountId.compare( + b.receiverAccountId, + ); + if (recipientComparision != 0) { + return recipientComparision; + } + + return a.serialNumber.compare(b.serialNumber); + }); + + let i = 0; + let j = 0; + while ( + i < this._tokenTransfers.length || + j < this._nftTransfers.length + ) { + if ( + i < this._tokenTransfers.length && + j < this._nftTransfers.length + ) { + const iTokenId = this._tokenTransfers[i].tokenId; + const jTokenId = this._nftTransfers[j].tokenId; + + const last = + tokenTransferList.length > 0 + ? tokenTransferList[tokenTransferList.length - 1] + : null; + const lastTokenId = last != null ? last.tokenId : null; + + if ( + last != null && + lastTokenId != null && + lastTokenId.compare(iTokenId) === 0 + ) { + last.transfers.push(this._tokenTransfers[i++]); + continue; + } + + if ( + last != null && + lastTokenId != null && + lastTokenId.compare(jTokenId) === 0 + ) { + last.nftTransfers.push(this._nftTransfers[j++]); + continue; + } + + const result = iTokenId.compare(jTokenId); + + if (result === 0) { + tokenTransferList.push({ + tokenId: iTokenId, + expectedDecimals: + this._tokenTransfers[i].expectedDecimals, + transfers: [this._tokenTransfers[i++]], + nftTransfers: [this._nftTransfers[j++]], + }); + } else if (result < 0) { + tokenTransferList.push({ + tokenId: iTokenId, + expectedDecimals: + this._tokenTransfers[i].expectedDecimals, + transfers: [this._tokenTransfers[i++]], + nftTransfers: [], + }); + } else { + tokenTransferList.push({ + tokenId: jTokenId, + expectedDecimals: null, + transfers: [], + nftTransfers: [this._nftTransfers[j++]], + }); + } + } else if (i < this._tokenTransfers.length) { + const iTokenId = this._tokenTransfers[i].tokenId; + + let last; + for (const transfer of tokenTransferList) { + if (transfer.tokenId.compare(iTokenId) === 0) { + last = transfer; + } + } + const lastTokenId = last != null ? last.tokenId : null; + + if ( + last != null && + lastTokenId != null && + lastTokenId.compare(iTokenId) === 0 + ) { + last.transfers.push(this._tokenTransfers[i++]); + continue; + } + + tokenTransferList.push({ + tokenId: iTokenId, + expectedDecimals: this._tokenTransfers[i].expectedDecimals, + transfers: [this._tokenTransfers[i++]], + nftTransfers: [], + }); + } else if (j < this._nftTransfers.length) { + const jTokenId = this._nftTransfers[j].tokenId; + + let last; + for (const transfer of tokenTransferList) { + if (transfer.tokenId.compare(jTokenId) === 0) { + last = transfer; + } + } + const lastTokenId = last != null ? last.tokenId : null; + + if ( + last != null && + lastTokenId != null && + lastTokenId.compare(jTokenId) === 0 + ) { + last.nftTransfers.push(this._nftTransfers[j++]); + continue; + } + + tokenTransferList.push({ + tokenId: jTokenId, + expectedDecimals: null, + transfers: [], + nftTransfers: [this._nftTransfers[j++]], + }); + } + } + + return { + tokenTransfers: tokenTransferList.map((tokenTransfer) => { + return { + token: tokenTransfer.tokenId._toProtobuf(), + expectedDecimals: + tokenTransfer.expectedDecimals != null + ? { value: tokenTransfer.expectedDecimals } + : null, + transfers: tokenTransfer.transfers.map((transfer) => + transfer._toProtobuf(), + ), + nftTransfers: tokenTransfer.nftTransfers.map((transfer) => + transfer._toProtobuf(), + ), + }; + }), + }; + } +} diff --git a/src/token/AirdropPendingTransaction.js b/src/token/AirdropPendingTransaction.js new file mode 100644 index 000000000..6a7a4cd0b --- /dev/null +++ b/src/token/AirdropPendingTransaction.js @@ -0,0 +1,77 @@ +/*- + * ‌ + * Hedera JavaScript SDK + * ​ + * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +import Transaction from "../transaction/Transaction.js"; + +/** + * @typedef {import("../token/PendingAirdropId.js").default} PendingAirdropId + */ +export default class AirdropPendingTransaction extends Transaction { + /** + * @param {object} [props] + * @param {PendingAirdropId[]} [props.pendingAirdropIds] + */ + constructor(props) { + /** + * @private + * @type {PendingAirdropId[]} + */ + super(); + + /** + * @private + * @type {PendingAirdropId[]} + */ + this._pendingAirdropIds = []; + + if (props?.pendingAirdropIds != null) { + this._pendingAirdropIds = props.pendingAirdropIds; + } + } + + /** + * @returns {PendingAirdropId[]} + */ + get pendingAirdropIds() { + return this._pendingAirdropIds; + } + + /** + * + * @param {PendingAirdropId} pendingAirdropId + * @returns {this} + */ + addPendingAirdropId(pendingAirdropId) { + this._requireNotFrozen(); + this._pendingAirdropIds.push(pendingAirdropId); + return this; + } + + /** + * + * @param {PendingAirdropId[]} pendingAirdropIds + * @returns {this} + */ + setPendingAirdropIds(pendingAirdropIds) { + this._requireNotFrozen(); + this._pendingAirdropIds = pendingAirdropIds; + return this; + } +} diff --git a/src/token/PendingAirdropId.js b/src/token/PendingAirdropId.js new file mode 100644 index 000000000..18d357f51 --- /dev/null +++ b/src/token/PendingAirdropId.js @@ -0,0 +1,167 @@ +/*- + * ‌ + * Hedera JavaScript SDK + * ​ + * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +/** + * @namespace proto + * @typedef {import("@hashgraph/proto").proto.PendingAirdropId} HashgraphProto.proto.PendingAirdropId + */ + +import AccountId from "../account/AccountId.js"; +import TokenId from "./TokenId.js"; +import NftId from "./NftId.js"; + +export default class PendingAirdropId { + /** + * + * @param {object} props + * @param {AccountId} [props.senderId] + * @param {AccountId} [props.receiverId] + * @param {TokenId?} props.tokenId + * @param {NftId?} props.nftId + */ + constructor(props) { + this._senderId = null; + this._receiverId = null; + this._tokenId = null; + this._nftId = null; + + if (props.receiverId) { + this._receiverId = props.receiverId; + } + if (props.senderId) { + this._senderId = props.senderId; + } + if (props.tokenId) { + this._tokenId = new TokenId(props.tokenId); + } else if (props.nftId) { + this._nftId = new NftId(props.nftId?.tokenId, props.nftId?.serial); + } + } + + /** + * @param {HashgraphProto.proto.PendingAirdropId} pb + * @returns {PendingAirdropId} + */ + static fromBytes(pb) { + if (pb.senderId == null) { + throw new Error("senderId is required"); + } + + if (pb.receiverId == null) { + throw new Error("receiverId is required"); + } + + if (pb.fungibleTokenType == null && pb.nonFungibleToken == null) { + throw new Error( + "Either fungibleTokenType or nonFungibleToken is required", + ); + } + + return new PendingAirdropId({ + senderId: AccountId._fromProtobuf(pb.senderId), + receiverId: AccountId._fromProtobuf(pb.receiverId), + nftId: + pb.nonFungibleToken != null + ? NftId._fromProtobuf(pb.nonFungibleToken) + : null, + tokenId: + pb.fungibleTokenType != null + ? TokenId._fromProtobuf(pb.fungibleTokenType) + : null, + }); + } + + /** + * + * @param {AccountId} senderId + * @returns {this} + */ + setSenderid(senderId) { + this._senderId = senderId; + return this; + } + + /** + * @param {AccountId} receiverId + * @returns {this} + */ + setReceiverId(receiverId) { + this._receiverId = receiverId; + return this; + } + + /** + * @param {TokenId} tokenId + * @returns {this} + */ + setTokenId(tokenId) { + this._tokenId = tokenId; + return this; + } + + /** + * @param {NftId} nftId + * @returns {this} + */ + setNftId(nftId) { + this._nftId = nftId; + return this; + } + + /** + * @returns {?AccountId} + */ + get senderId() { + return this._senderId; + } + + /** + * @returns {?AccountId} + */ + get receiverId() { + return this._receiverId; + } + + /** + * @returns {?TokenId} + */ + get tokenId() { + return this._tokenId; + } + + /** + * @returns {?NftId} + */ + get nftId() { + return this._nftId; + } + + /** + * @returns {HashgraphProto.proto.PendingAirdropId} + */ + toBytes() { + return { + senderId: this.senderId?._toProtobuf(), + receiverId: this._receiverId?._toProtobuf(), + fungibleTokenType: this._tokenId?._toProtobuf(), + nonFungibleToken: this._nftId?._toProtobuf(), + }; + } +} diff --git a/src/token/PendingAirdropRecord.js b/src/token/PendingAirdropRecord.js new file mode 100644 index 000000000..33338aa53 --- /dev/null +++ b/src/token/PendingAirdropRecord.js @@ -0,0 +1,69 @@ +/*- + * ‌ + * Hedera JavaScript SDK + * ​ + * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +/** + * @namespace proto + * @typedef {import("@hashgraph/proto").proto.PendingAirdropRecord} HashgraphProto.proto.PendingAirdropRecord + */ + +import Long from "long"; +import PendingAirdropId from "./PendingAirdropId.js"; + +export default class PendingAirdropRecord { + /** + * @param {object} props + * @param {PendingAirdropId} props.airdropId + * @param {Long} props.amount + */ + constructor(props) { + this.airdropId = props.airdropId; + this.amount = props.amount; + } + + /** + * @returns {HashgraphProto.proto.PendingAirdropRecord} + */ + toBytes() { + return { + pendingAirdropId: this.airdropId.toBytes(), + pendingAirdropValue: { + amount: this.amount, + }, + }; + } + + /** + * @param {HashgraphProto.proto.PendingAirdropRecord} pb + * @returns {PendingAirdropRecord} + */ + static fromBytes(pb) { + if (pb.pendingAirdropId == null) { + throw new Error("pendingAirdropId is required"); + } + + const airdropId = PendingAirdropId.fromBytes(pb.pendingAirdropId); + const amount = pb.pendingAirdropValue?.amount; + + return new PendingAirdropRecord({ + airdropId: airdropId, + amount: amount ? amount : Long.ZERO, + }); + } +} diff --git a/src/token/TokenAirdropTransaction.js b/src/token/TokenAirdropTransaction.js new file mode 100644 index 000000000..805a69b7c --- /dev/null +++ b/src/token/TokenAirdropTransaction.js @@ -0,0 +1,187 @@ +/*- + * ‌ + * Hedera JavaScript SDK + * ​ + * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +import Transaction, { + TRANSACTION_REGISTRY, +} from "../transaction/Transaction.js"; +import TokenTransfer from "./TokenTransfer.js"; +import NftTransfer from "./TokenNftTransfer.js"; +import AbstractTokenTransferTransaction from "./AbstractTokenTransferTransaction.js"; + +/** + * @namespace proto + * @typedef {import("@hashgraph/proto").proto.ITokenAirdropTransactionBody} HashgraphProto.proto.ITokenAirdropTransactionBody + * @typedef {import("@hashgraph/proto").proto.ITransaction} HashgraphProto.proto.ITransaction + * @typedef {import("@hashgraph/proto").proto.TransactionID} HashgraphProto.proto.TransactionID + * @typedef {import("@hashgraph/proto").proto.AccountID} HashgraphProto.proto.AccountID + * @typedef {import("@hashgraph/proto").proto.ISignedTransaction} HashgraphProto.proto.ISignedTransaction + * @typedef {import("@hashgraph/proto").proto.ITransactionBody} HashgraphProto.proto.ITransactionBody + * @typedef {import("@hashgraph/proto").proto.TransactionBody} HashgraphProto.proto.TransactionBody + * @typedef {import("@hashgraph/proto").proto.ITransactionResponse} HashgraphProto.proto.ITransactionResponse + */ + +/** + * @typedef {import("../channel/Channel.js").default} Channel + * @typedef {import("../transaction/TransactionId.js").default} TransactionId + * @typedef {import("../account/AccountId.js").default} AccountId + * @typedef {import("./NftId.js").default} NftId + * @typedef {import("./TokenId.js").default} TokenId + */ +export default class TokenAirdropTransaction extends AbstractTokenTransferTransaction { + /** + * @param {object} props + * @param {TokenTransfer[]} [props.tokenTransfers] + * @param {NftTransfer[]} [props.nftTransfers] + */ + constructor(props = {}) { + super(); + + if (props.tokenTransfers != null) { + for (const tokenTransfer of props.tokenTransfers) { + this._addTokenTransfer( + tokenTransfer.tokenId, + tokenTransfer.accountId, + tokenTransfer.amount, + tokenTransfer.isApproved, + tokenTransfer.expectedDecimals, + ); + } + } + /** + * @private + * @type {NftTransfer[]} + */ + this._nftTransfers = []; + if (props.nftTransfers != null) { + for (const nftTransfer of props.nftTransfers) { + this._addNftTransfer( + nftTransfer.isApproved, + nftTransfer.tokenId, + nftTransfer.serialNumber, + nftTransfer.senderAccountId, + nftTransfer.receiverAccountId, + ); + } + } + } + + /** + * + * @param {TokenId} tokenId + * @param {AccountId} accountId + * @param {Long} amount + * @param {number} expectedDecimals + * @returns {this} + */ + addApprovedTokenTransferWithDecimals( + tokenId, + accountId, + amount, + expectedDecimals, + ) { + this._requireNotFrozen(); + this._addTokenTransfer( + tokenId, + accountId, + amount, + true, + expectedDecimals, + ); + return this; + } + + /** + * @internal + * @param {HashgraphProto.proto.ITransaction[]} transactions + * @param {HashgraphProto.proto.ISignedTransaction[]} signedTransactions + * @param {TransactionId[]} transactionIds + * @param {AccountId[]} nodeIds + * @param {HashgraphProto.proto.ITransactionBody[]} bodies + * @returns {TokenAirdropTransaction} + */ + static _fromProtobuf( + transactions, + signedTransactions, + transactionIds, + nodeIds, + bodies, + ) { + const body = bodies[0]; + const tokenAirdrop = + /** @type {HashgraphProto.proto.ITokenAirdropTransactionBody} */ ( + body.tokenAirdrop + ); + + const tokenTransfers = TokenTransfer._fromProtobuf( + tokenAirdrop.tokenTransfers ?? [], + ); + const nftTransfers = NftTransfer._fromProtobuf( + tokenAirdrop.tokenTransfers ?? [], + ); + + return Transaction._fromProtobufTransactions( + new TokenAirdropTransaction({ + nftTransfers: nftTransfers, + tokenTransfers: tokenTransfers, + }), + transactions, + signedTransactions, + transactionIds, + nodeIds, + bodies, + ); + } + + /** + * @override + * @internal + * @param {Channel} channel + * @param {HashgraphProto.proto.ITransaction} request + * @returns {Promise} + */ + _execute(channel, request) { + return channel.token.airdropTokens(request); + } + + /** + * @override + * @protected + * @returns {NonNullable} + */ + _getTransactionDataCase() { + return "tokenAirdrop"; + } + + /** + * @returns {string} + */ + _getLogId() { + const timestamp = /** @type {import("../Timestamp.js").default} */ ( + this._transactionIds.current.validStart + ); + return `TokenAirdropTransaction:${timestamp.toString()}`; + } +} + +TRANSACTION_REGISTRY.set( + "tokenAirdrop", + // eslint-disable-next-line @typescript-eslint/unbound-method + TokenAirdropTransaction._fromProtobuf, +); diff --git a/src/token/TokenCancelAirdropTransaction.js b/src/token/TokenCancelAirdropTransaction.js new file mode 100644 index 000000000..fd1822262 --- /dev/null +++ b/src/token/TokenCancelAirdropTransaction.js @@ -0,0 +1,133 @@ +/*- + * ‌ + * Hedera JavaScript SDK + * ​ + * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +import PendingAirdropId from "../token/PendingAirdropId.js"; +import { TRANSACTION_REGISTRY } from "../transaction/Transaction.js"; +import Transaction from "../transaction/Transaction.js"; +import AirdropPendingTransaction from "./AirdropPendingTransaction.js"; + +/** + * @typedef {import("@hashgraph/proto").proto.ITransaction} HashgraphProto.proto.ITransaction + * @typedef {import("@hashgraph/proto").proto.ITransactionResponse} HashgraphProto.proto.ITransactionResponse + * @typedef {import("@hashgraph/proto").proto.TransactionBody} HashgraphProto.proto.TransactionBody + * @typedef {import("@hashgraph/proto").proto.ISignedTransaction} HashgraphProto.proto.ISignedTransaction + * @typedef {import("@hashgraph/proto").proto.ITransactionBody} HashgraphProto.proto.ITransactionBody + * @typedef {import("@hashgraph/proto").proto.ITokenCancelAirdropTransactionBody} HashgraphProto.proto.ITokenCancelAirdropTransactionBody + */ + +/** + * @typedef {import("../channel/Channel.js").default} Channel + * @typedef {import("../transaction/TransactionId.js").default} TransactionId + * @typedef {import("../account/AccountId.js").default} AccountId + */ +export default class TokenCancelAirdropTransaction extends AirdropPendingTransaction { + /** + * @param {object} props + * @param {PendingAirdropId[]} [props.pendingAirdropIds] + */ + constructor(props = {}) { + super(props); + } + + /** + * @override + * @internal + * @returns {HashgraphProto.proto.ITokenCancelAirdropTransactionBody} + */ + _makeTransactionData() { + return { + pendingAirdrops: this.pendingAirdropIds.map((pendingAirdropId) => + pendingAirdropId.toBytes(), + ), + }; + } + + /** + * @override + * @internal + * @param {Channel} channel + * @param {HashgraphProto.proto.ITransaction} request + * @returns {Promise} + */ + _execute(channel, request) { + return channel.token.cancelAirdrop(request); + } + + /** + * @override + * @protected + * @returns {NonNullable} + */ + _getTransactionDataCase() { + return "tokenCancelAirdrop"; + } + + /** + * @internal + * @param {HashgraphProto.proto.ITransaction[]} transactions + * @param {HashgraphProto.proto.ISignedTransaction[]} signedTransactions + * @param {TransactionId[]} transactionIds + * @param {AccountId[]} nodeIds + * @param {HashgraphProto.proto.ITransactionBody[]} bodies + * @returns {TokenCancelAirdropTransaction} + */ + static _fromProtobuf( + transactions, + signedTransactions, + transactionIds, + nodeIds, + bodies, + ) { + const body = bodies[0]; + const { pendingAirdrops } = + /** @type {HashgraphProto.proto.ITokenCancelAirdropTransactionBody} */ ( + body.tokenCancelAirdrop + ); + + return Transaction._fromProtobufTransactions( + new TokenCancelAirdropTransaction({ + pendingAirdropIds: pendingAirdrops?.map((pendingAirdrop) => { + return PendingAirdropId.fromBytes(pendingAirdrop); + }), + }), + transactions, + signedTransactions, + transactionIds, + nodeIds, + bodies, + ); + } + + /** + * @returns {string} + */ + _getLogId() { + const timestamp = /** @type {import("../Timestamp.js").default} */ ( + this._transactionIds.current.validStart + ); + return `TokenCancelAirdrop:${timestamp.toString()}`; + } +} + +TRANSACTION_REGISTRY.set( + "tokenCancelAirdrop", + // eslint-disable-next-line @typescript-eslint/unbound-method + TokenCancelAirdropTransaction._fromProtobuf, +); diff --git a/src/token/TokenClaimAirdropTransaction.js b/src/token/TokenClaimAirdropTransaction.js new file mode 100644 index 000000000..6e8704f3c --- /dev/null +++ b/src/token/TokenClaimAirdropTransaction.js @@ -0,0 +1,135 @@ +/*- + * ‌ + * Hedera JavaScript SDK + * ​ + * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +import PendingAirdropId from "../token/PendingAirdropId.js"; +import AirdropPendingTransaction from "./AirdropPendingTransaction.js"; +import Transaction, { + TRANSACTION_REGISTRY, +} from "../transaction/Transaction.js"; + +/** + * @typedef {import("@hashgraph/proto").proto.ITransaction} HashgraphProto.proto.ITransaction + * @typedef {import("@hashgraph/proto").proto.ITransactionResponse} HashgraphProto.proto.ITransactionResponse + * @typedef {import("@hashgraph/proto").proto.TransactionBody} HashgraphProto.proto.TransactionBody + * @typedef {import("@hashgraph/proto").proto.ISignedTransaction} HashgraphProto.proto.ISignedTransaction + * @typedef {import("@hashgraph/proto").proto.ITransactionBody} HashgraphProto.proto.ITransactionBody + * @typedef {import("@hashgraph/proto").proto.ITokenClaimAirdropTransactionBody} HashgraphProto.proto.ITokenClaimAirdropTransactionBody + */ + +/** + * @typedef {import("../channel/Channel.js").default} Channel + * @typedef {import("../transaction/TransactionId.js").default} TransactionId + * @typedef {import("../account/AccountId.js").default} AccountId + */ + +export default class TokenClaimAirdropTransaction extends AirdropPendingTransaction { + /** + * @param {object} props + * @param {PendingAirdropId[]} [props.pendingAirdropIds] + */ + constructor(props = {}) { + super(props); + } + + /** + * @override + * @internal + * @param {Channel} channel + * @param {HashgraphProto.proto.ITransaction} request + * @returns {Promise} + */ + _execute(channel, request) { + return channel.token.claimAirdrop(request); + } + + /** + * @override + * @internal + * @returns {HashgraphProto.proto.ITokenClaimAirdropTransactionBody} + */ + _makeTransactionData() { + return { + pendingAirdrops: this.pendingAirdropIds.map((pendingAirdropId) => + pendingAirdropId.toBytes(), + ), + }; + } + + /** + * @internal + * @param {HashgraphProto.proto.ITransaction[]} transactions + * @param {HashgraphProto.proto.ISignedTransaction[]} signedTransactions + * @param {TransactionId[]} transactionIds + * @param {AccountId[]} nodeIds + * @param {HashgraphProto.proto.ITransactionBody[]} bodies + * @returns {TokenClaimAirdropTransaction} + */ + static _fromProtobuf( + transactions, + signedTransactions, + transactionIds, + nodeIds, + bodies, + ) { + const body = bodies[0]; + const { pendingAirdrops } = + /** @type {HashgraphProto.proto.ITokenClaimAirdropTransactionBody} */ ( + body.tokenClaimAirdrop + ); + + return Transaction._fromProtobufTransactions( + new TokenClaimAirdropTransaction({ + pendingAirdropIds: pendingAirdrops?.map((pendingAirdrop) => { + return PendingAirdropId.fromBytes(pendingAirdrop); + }), + }), + transactions, + signedTransactions, + transactionIds, + nodeIds, + bodies, + ); + } + + /** + * @override + * @protected + * @returns {NonNullable} + */ + _getTransactionDataCase() { + return "tokenClaimAirdrop"; + } + + /** + * @returns {string} + */ + _getLogId() { + const timestamp = /** @type {import("../Timestamp.js").default} */ ( + this._transactionIds.current.validStart + ); + return `TokenClaimAirdropTransaction:${timestamp.toString()}`; + } +} + +TRANSACTION_REGISTRY.set( + "tokenClaimAirdrop", + // eslint-disable-next-line @typescript-eslint/unbound-method + TokenClaimAirdropTransaction._fromProtobuf, +); diff --git a/src/token/TokenNftTransfer.js b/src/token/TokenNftTransfer.js index 1861d98e0..bb750c023 100644 --- a/src/token/TokenNftTransfer.js +++ b/src/token/TokenNftTransfer.js @@ -91,7 +91,6 @@ export default class TokenNftTransfer { tokenTransfer.token ), ); - for (const transfer of tokenTransfer.nftTransfers != null ? tokenTransfer.nftTransfers : []) { diff --git a/src/transaction/TransactionRecord.js b/src/transaction/TransactionRecord.js index fd256828d..fe3c78211 100644 --- a/src/transaction/TransactionRecord.js +++ b/src/transaction/TransactionRecord.js @@ -35,6 +35,7 @@ import PublicKey from "../PublicKey.js"; import TokenTransfer from "../token/TokenTransfer.js"; import EvmAddress from "../EvmAddress.js"; import * as hex from "../encoding/hex.js"; +import PendingAirdropRecord from "../token/PendingAirdropRecord.js"; /** * @typedef {import("../token/TokenId.js").default} TokenId @@ -108,6 +109,7 @@ export default class TransactionRecord { * @param {?Uint8Array} props.prngBytes * @param {?number} props.prngNumber * @param {?EvmAddress} props.evmAddress + * @param {PendingAirdropRecord[]} props.newPendingAirdrops */ constructor(props) { /** @@ -303,6 +305,14 @@ export default class TransactionRecord { */ this.evmAddress = props.evmAddress; + /** + * The new default EVM address of the account created by this transaction. + * This field is populated only when the EVM address is not specified in the related transaction body. + * + * @readonly + */ + this.newPendingAirdrops = props.newPendingAirdrops; + Object.freeze(this); } @@ -427,6 +437,9 @@ export default class TransactionRecord { prngNumber: this.prngNumber != null ? this.prngNumber : null, evmAddress: this.evmAddress != null ? this.evmAddress.toBytes() : null, + newPendingAirdrops: this.newPendingAirdrops.map((airdrop) => + airdrop.toBytes(), + ), }, }; } @@ -483,6 +496,13 @@ export default class TransactionRecord { ) : undefined; + const newPendingAirdrops = + record.newPendingAirdrops != null + ? record.newPendingAirdrops.map((airdrop) => + PendingAirdropRecord.fromBytes(airdrop), + ) + : []; + return new TransactionRecord({ receipt: TransactionReceipt._fromProtobuf({ receipt: @@ -568,6 +588,7 @@ export default class TransactionRecord { record.evmAddress != null ? EvmAddress.fromBytes(record.evmAddress) : null, + newPendingAirdrops: newPendingAirdrops, }); } diff --git a/tck/methods/account.ts b/tck/methods/account.ts index ac9f2a0d0..8b1e1491d 100644 --- a/tck/methods/account.ts +++ b/tck/methods/account.ts @@ -3,6 +3,7 @@ import { Hbar, AccountId, AccountUpdateTransaction, + AccountDeleteTransaction, } from "@hashgraph/sdk"; import { sdk } from "../sdk_data"; @@ -11,7 +12,11 @@ import { AccountResponse } from "../response/account"; import { getKeyFromString } from "../utils/key"; import { DEFAULT_GRPC_DEADLINE } from "../utils/constants/config"; -import { CreateAccountParams, UpdateAccountParams } from "../params/account"; +import { + CreateAccountParams, + DeleteAccountParams, + UpdateAccountParams, +} from "../params/account"; import { applyCommonTransactionParams } from "../params/common-tx-params"; export const createAccount = async ({ @@ -165,3 +170,38 @@ export const updateAccount = async ({ status: receipt.status.toString(), }; }; + +export const deleteAccount = async ({ + deleteAccountId, + transferAccountId, + commonTransactionParams, +}: DeleteAccountParams): Promise => { + let transaction = new AccountDeleteTransaction().setGrpcDeadline( + DEFAULT_GRPC_DEADLINE, + ); + + if (deleteAccountId != null) { + transaction.setAccountId(AccountId.fromString(deleteAccountId)); + } + + if (transferAccountId != null) { + transaction.setTransferAccountId( + AccountId.fromString(transferAccountId), + ); + } + + if (commonTransactionParams != null) { + applyCommonTransactionParams( + commonTransactionParams, + transaction, + sdk.getClient(), + ); + } + + const txResponse = await transaction.execute(sdk.getClient()); + const receipt = await txResponse.getReceipt(sdk.getClient()); + + return { + status: receipt.status.toString(), + }; +}; diff --git a/tck/params/account.ts b/tck/params/account.ts index 4f3e9834f..fbc8ef7f2 100644 --- a/tck/params/account.ts +++ b/tck/params/account.ts @@ -25,3 +25,9 @@ export interface UpdateAccountParams { readonly declineStakingReward?: boolean; readonly commonTransactionParams?: Record; } + +export interface DeleteAccountParams { + readonly deleteAccountId?: string; + readonly transferAccountId?: string; + readonly commonTransactionParams?: Record; +} diff --git a/test/integration/TokenAirdropIntegrationTest.js b/test/integration/TokenAirdropIntegrationTest.js new file mode 100644 index 000000000..0a4b179c8 --- /dev/null +++ b/test/integration/TokenAirdropIntegrationTest.js @@ -0,0 +1,533 @@ +import { + AccountCreateTransaction, + TokenAirdropTransaction, + TokenCreateTransaction, + TokenMintTransaction, + TokenType, + PrivateKey, + NftId, + AccountBalanceQuery, + CustomFixedFee, + TokenAssociateTransaction, + TransferTransaction, + Hbar, + AccountId, + TokenId, +} from "../../src/exports.js"; +import IntegrationTestEnv from "./client/NodeIntegrationTestEnv.js"; + +describe("TokenAirdropIntegrationTest", function () { + let env; + const INITIAL_SUPPLY = 1000; + + beforeEach(async function () { + env = await IntegrationTestEnv.new(); + }); + + it("should transfer tokens when the account is associated", async function () { + this.timeout(1200000); + + const ftCreateResponse = await new TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorKey) + .execute(env.client); + + const { tokenId: ftTokenId } = await ftCreateResponse.getReceipt( + env.client, + ); + + const nftCreateResponse = await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client); + + const { tokenId: nftTokenId } = await nftCreateResponse.getReceipt( + env.client, + ); + + const mintResponse = await new TokenMintTransaction() + .setTokenId(nftTokenId) + .addMetadata(Buffer.from("-")) + .execute(env.client); + + const { serials } = await mintResponse.getReceipt(env.client); + + const receiverPrivateKey = PrivateKey.generateED25519(); + const accountCreateResponse = await new AccountCreateTransaction() + .setKey(receiverPrivateKey.publicKey) + .setMaxAutomaticTokenAssociations(-1) + .execute(env.client); + + const { accountId: receiverId } = + await accountCreateResponse.getReceipt(env.client); + + // airdrop the tokens + const transactionResponse = await new TokenAirdropTransaction() + .addNftTransfer( + new NftId(nftTokenId, serials[0]), + env.operatorId, + receiverId, + ) + .addTokenTransfer(ftTokenId, receiverId, INITIAL_SUPPLY) + .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) + .execute(env.client); + + const { newPendingAirdrops } = await transactionResponse.getRecord( + env.client, + ); + expect(newPendingAirdrops.length).to.be.eq(0); + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + const receiverBalance = await new AccountBalanceQuery() + .setAccountId(receiverId) + .execute(env.client); + + expect(operatorBalance.tokens.get(ftTokenId).toInt()).to.be.eq(0); + expect(receiverBalance.tokens.get(ftTokenId).toInt()).to.be.eq( + INITIAL_SUPPLY, + ); + + expect(operatorBalance.tokens.get(nftTokenId).toInt()).to.be.eq(0); + expect(receiverBalance.tokens.get(nftTokenId).toInt()).to.be.eq(1); + }); + + it("tokens should be in pending state when no automatic association", async function () { + this.timeout(1200000); + const ftCreateResponse = await new TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorKey) + .execute(env.client); + + const { tokenId: ftTokenId } = await ftCreateResponse.getReceipt( + env.client, + ); + + const nftCreateResponse = await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client); + + const { tokenId: nftTokenId } = await nftCreateResponse.getReceipt( + env.client, + ); + + const mintResponse = await new TokenMintTransaction() + .setTokenId(nftTokenId) + .addMetadata(Buffer.from("-")) + .execute(env.client); + + const { serials } = await mintResponse.getReceipt(env.client); + + const receiverPrivateKey = PrivateKey.generateED25519(); + const accountCreateResponse = await new AccountCreateTransaction() + .setKey(receiverPrivateKey.publicKey) + .execute(env.client); + + const { accountId: receiverId } = + await accountCreateResponse.getReceipt(env.client); + + const airdropTokenResponse = await new TokenAirdropTransaction() + .addTokenTransfer(ftTokenId, receiverId, INITIAL_SUPPLY) + .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) + .addNftTransfer(nftTokenId, serials[0], env.operatorId, receiverId) + .execute(env.client); + + const airdropTokenRecord = await airdropTokenResponse.getRecord( + env.client, + ); + + const { newPendingAirdrops } = airdropTokenRecord; + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + const receiverBalance = await new AccountBalanceQuery() + .setAccountId(receiverId) + .execute(env.client); + + // FT checks + expect(operatorBalance.tokens.get(ftTokenId).toInt()).to.be.eq( + INITIAL_SUPPLY, + ); + expect(receiverBalance.tokens.get(ftTokenId)).to.be.eq(null); + + // NFT checks + expect(operatorBalance.tokens.get(nftTokenId).toInt()).to.be.eq(1); + expect(receiverBalance.tokens.get(nftTokenId)).to.be.eq(null); + + // record check + expect(newPendingAirdrops.length).to.be.eq(2); + expect(newPendingAirdrops[0].airdropId.senderId).deep.equal( + env.operatorId, + ); + expect(newPendingAirdrops[0].airdropId.receiverId).deep.equal( + receiverId, + ); + expect(newPendingAirdrops[0].airdropId.tokenId).deep.equal(ftTokenId); + expect(newPendingAirdrops[0].airdropId.nftId).to.equal(null); + + expect(newPendingAirdrops[1].airdropId.senderId).deep.equal( + env.operatorId, + ); + expect(newPendingAirdrops[1].airdropId.receiverId).deep.equal( + receiverId, + ); + expect(newPendingAirdrops[1].airdropId.tokenId).deep.equal(null); + expect(newPendingAirdrops[1].airdropId.nftId.tokenId).to.deep.equal( + nftTokenId, + ); + expect(newPendingAirdrops[1].airdropId.nftId.serial).to.deep.equal( + serials[0], + ); + }); + + it("should create hollow account when airdropping tokens and transfers them", async function () { + this.timeout(1200000); + const ftCreateResponse = await new TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorKey) + .execute(env.client); + + const { tokenId: ftTokenId } = await ftCreateResponse.getReceipt( + env.client, + ); + + const nftCreateResponse = await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setTokenType(TokenType.NonFungibleUnique) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorKey) + .execute(env.client); + + const { tokenId: nftTokenId } = await nftCreateResponse.getReceipt( + env.client, + ); + const mintResponse = await new TokenMintTransaction() + .setTokenId(nftTokenId) + .addMetadata(Buffer.from("metadata")) + .execute(env.client); + + const { serials } = await mintResponse.getReceipt(env.client); + + const receiverPrivateKey = PrivateKey.generateED25519(); + const aliasAccountId = receiverPrivateKey.publicKey.toAccountId(0, 0); + + const airdropTokenResponse = await new TokenAirdropTransaction() + .addTokenTransfer(ftTokenId, aliasAccountId, INITIAL_SUPPLY) + .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) + .addNftTransfer( + nftTokenId, + serials[0], + env.operatorId, + aliasAccountId, + ) + .execute(env.client); + + await airdropTokenResponse.getReceipt(env.client); + + const aliasBalance = await new AccountBalanceQuery() + .setAccountId(aliasAccountId) + .execute(env.client); + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(aliasBalance.tokens.get(ftTokenId).toInt()).to.be.eq( + INITIAL_SUPPLY, + ); + expect(operatorBalance.tokens.get(ftTokenId).toInt()).to.be.eq(0); + + expect(aliasBalance.tokens.get(nftTokenId).toInt()).to.be.eq(1); + expect(operatorBalance.tokens.get(nftTokenId).toInt()).to.be.eq(0); + }); + + it("should airdrop with custom fees", async function () { + this.timeout(1200000); + + const FEE_AMOUNT = 1; + const receiverPrivateKey = PrivateKey.generateED25519(); + const createAccountResponse = await new AccountCreateTransaction() + .setKey(receiverPrivateKey.publicKey) + .setMaxAutomaticTokenAssociations(-1) + .execute(env.client); + + const { accountId: receiverId } = + await createAccountResponse.getReceipt(env.client); + + const feeTokenIdResponse = await new TokenCreateTransaction() + .setTokenName("fee") + .setTokenSymbol("FEE") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorKey) + .execute(env.client); + + const { tokenId: feeTokenId } = await feeTokenIdResponse.getReceipt( + env.client, + ); + + let customFixedFee = new CustomFixedFee() + .setFeeCollectorAccountId(env.operatorId) + .setDenominatingTokenId(feeTokenId) + .setAmount(FEE_AMOUNT) + .setAllCollectorsAreExempt(true); + + let tokenWithFee = await new TokenCreateTransaction() + .setTokenName("tokenWithFee") + .setTokenSymbol("TWF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorKey) + .setCustomFees([customFixedFee]) + .execute(env.client); + + const { tokenId: tokenWithFeeId } = await tokenWithFee.getReceipt( + env.client, + ); + + let nftTokenWithFee = await new TokenCreateTransaction() + .setTokenName("tokenWithFee") + .setTokenSymbol("TWF") + .setTokenType(TokenType.NonFungibleUnique) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorKey) + .setCustomFees([customFixedFee]) + .execute(env.client); + + const { tokenId: nftTokenId } = await nftTokenWithFee.getReceipt( + env.client, + ); + + const mintResponse = await new TokenMintTransaction() + .setTokenId(nftTokenId) + .addMetadata(Buffer.from("-")) + .execute(env.client); + + const { serials } = await mintResponse.getReceipt(env.client); + + const senderPrivateKey = PrivateKey.generateED25519(); + const { accountId: senderAccountId } = await ( + await new AccountCreateTransaction() + .setKey(senderPrivateKey.publicKey) + .setMaxAutomaticTokenAssociations(-1) + .setInitialBalance(new Hbar(10)) + .execute(env.client) + ).getReceipt(env.client); + + await ( + await ( + await new TokenAssociateTransaction() + .setAccountId(senderAccountId) + .setTokenIds([tokenWithFeeId, feeTokenId, nftTokenId]) + .freezeWith(env.client) + .sign(senderPrivateKey) + ).execute(env.client) + ).getReceipt(env.client); + + await ( + await new TransferTransaction() + .addTokenTransfer( + tokenWithFeeId, + env.operatorId, + -INITIAL_SUPPLY, + ) + .addTokenTransfer( + tokenWithFeeId, + senderAccountId, + INITIAL_SUPPLY, + ) + .addNftTransfer( + nftTokenId, + serials[0], + env.operatorId, + senderAccountId, + ) + .execute(env.client) + ).getReceipt(env.client); + + await ( + await new TransferTransaction() + .addTokenTransfer(feeTokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(feeTokenId, senderAccountId, INITIAL_SUPPLY) + .execute(env.client) + ).getReceipt(env.client); + + await ( + await ( + await new TokenAirdropTransaction() + .addTokenTransfer( + tokenWithFeeId, + receiverId, + INITIAL_SUPPLY, + ) + .addTokenTransfer( + tokenWithFeeId, + senderAccountId, + -INITIAL_SUPPLY, + ) + .addNftTransfer( + nftTokenId, + serials[0], + senderAccountId, + receiverId, + ) + .freezeWith(env.client) + .sign(senderPrivateKey) + ).execute(env.client) + ).getReceipt(env.client); + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + // check if fees are collected + const DISTINCT_TRANSACTIONS = 2; + expect(operatorBalance.tokens.get(tokenWithFeeId).toInt()).to.be.eq(0); + expect(operatorBalance.tokens.get(feeTokenId).toInt()).to.be.eq( + DISTINCT_TRANSACTIONS, + ); + + const receiverBalance = await new AccountBalanceQuery() + .setAccountId(receiverId) + .execute(env.client); + expect(receiverBalance.tokens.get(tokenWithFeeId).toInt()).to.be.eq( + INITIAL_SUPPLY, + ); + + const senderBalance = await new AccountBalanceQuery() + .setAccountId(senderAccountId) + .execute(env.client); + expect(senderBalance.tokens.get(tokenWithFeeId).toInt()).to.be.eq(0); + expect(senderBalance.tokens.get(feeTokenId).toInt()).to.be.eq( + INITIAL_SUPPLY - DISTINCT_TRANSACTIONS * FEE_AMOUNT, + ); + expect(senderBalance.tokens.get(nftTokenId).toInt()).to.be.eq(0); + expect(receiverBalance.tokens.get(nftTokenId).toInt()).to.be.eq(1); + }); + + it("should airdrop with receiver sig set to true", async function () { + this.timeout(1200000); + const tokenCreateResponse = await new TokenCreateTransaction() + .setTokenName("FFFFFFFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .execute(env.client); + + const { tokenId } = await tokenCreateResponse.getReceipt(env.client); + + const nftTokenResponse = await new TokenCreateTransaction() + .setTokenName("FFFFFFFFFF") + .setTokenSymbol("FFF") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client); + + const { tokenId: nftTokenId } = await nftTokenResponse.getReceipt( + env.client, + ); + + const mintResponse = await new TokenMintTransaction() + .setTokenId(nftTokenId) + .addMetadata(Buffer.from("-")) + .execute(env.client); + + const { serials } = await mintResponse.getReceipt(env.client); + + const receiverPrivateKey = PrivateKey.generateED25519(); + const receiverPublicKey = receiverPrivateKey.publicKey; + const accountCreateResponse = await ( + await new AccountCreateTransaction() + .setKey(receiverPublicKey) + .setReceiverSignatureRequired(true) + .freezeWith(env.client) + .sign(receiverPrivateKey) + ).execute(env.client); + + const { accountId: receiverId } = + await accountCreateResponse.getReceipt(env.client); + + let err = false; + try { + const airdropTokenResponse = await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addNftTransfer( + nftTokenId, + serials[0], + env.operatorId, + receiverId, + ) + .execute(env.client); + + await airdropTokenResponse.getReceipt(env.client); + } catch (error) { + if (error.message.includes("INVALID_SIGNATURE")) { + err = true; + } + } + + expect(err).to.be.eq(false); + }); + + it("should not airdrop with invalid tx body", async function () { + let err = false; + const tokenId = new TokenId(1); + const accountId = new AccountId(1); + + try { + await ( + await new TokenAirdropTransaction().execute(env.client) + ).getReceipt(env.client); + } catch (error) { + if (error.message.includes("EMPTY_TOKEN_TRANSFER_BODY")) { + err = true; + } + } + expect(err).to.be.eq(true); + + err = false; + try { + await ( + await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, accountId, 1) + .addTokenTransfer(tokenId, accountId, 1) + .addTokenTransfer(tokenId, accountId, 1) + .addTokenTransfer(tokenId, accountId, 1) + .addTokenTransfer(tokenId, accountId, 1) + .addTokenTransfer(tokenId, accountId, 1) + .addTokenTransfer(tokenId, accountId, 1) + .addTokenTransfer(tokenId, accountId, 1) + .addTokenTransfer(tokenId, accountId, 1) + .addTokenTransfer(tokenId, accountId, 1) + .addNftTransfer(new NftId(tokenId, 1), accountId, accountId) + .execute(env.client) + ).getReceipt(env.client); + } catch (error) { + if (error.message.includes("INVALID_TRANSACTION_BODY")) { + err = true; + } + } + expect(err).to.be.eq(true); + }); +}); diff --git a/test/integration/TokenCancelAirdropTransaction.js b/test/integration/TokenCancelAirdropTransaction.js new file mode 100644 index 000000000..74b9b686e --- /dev/null +++ b/test/integration/TokenCancelAirdropTransaction.js @@ -0,0 +1,586 @@ +import { expect } from "chai"; +import { + AccountCreateTransaction, + TokenCreateTransaction, + TokenType, + PrivateKey, + TokenAirdropTransaction, + TokenMintTransaction, + TokenCancelAirdropTransaction, + AccountBalanceQuery, + TokenFreezeTransaction, + TokenAssociateTransaction, + TokenPauseTransaction, + TokenDeleteTransaction, + TransactionId, +} from "../../src/exports.js"; +import IntegrationTestEnv from "./client/NodeIntegrationTestEnv.js"; + +describe("TokenCancelAirdropIntegrationTest", function () { + let env; + const INITIAL_SUPPLY = 1000; + + beforeEach(async function () { + env = await IntegrationTestEnv.new(); + }); + + it("should cancel the tokens when they are in pending state", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFFFFF") + .setTokenSymbol("FFF") + .setTreasuryAccountId(env.operatorId) + .setInitialSupply(INITIAL_SUPPLY) + .execute(env.client) + ).getReceipt(env.client); + + const { tokenId: nftId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFFFFF") + .setTokenSymbol("FFF") + .setTokenType(TokenType.NonFungibleUnique) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorKey) + .execute(env.client) + ).getReceipt(env.client); + + const { serials } = await ( + await new TokenMintTransaction() + .setTokenId(nftId) + .addMetadata(Buffer.from("-")) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .execute(env.client) + ).getRecord(env.client); + + const [airdrop] = newPendingAirdrops; + const { airdropId } = airdrop; + await new TokenCancelAirdropTransaction() + .addPendingAirdropId(airdropId) + .execute(env.client); + + const ownerBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(ownerBalance.tokens.get(tokenId).toInt()).to.be.eq( + INITIAL_SUPPLY, + ); + expect(ownerBalance.tokens.get(nftId).toInt()).to.be.eq(1); + }); + + it("should cancel the token when token's frozen", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFFFFF") + .setTokenSymbol("FFF") + .setTreasuryAccountId(env.operatorId) + .setInitialSupply(INITIAL_SUPPLY) + .setFreezeKey(env.operatorKey) + .execute(env.client) + ).getReceipt(env.client); + + const { tokenId: nftId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFFFFF") + .setTokenSymbol("FFF") + .setTokenType(TokenType.NonFungibleUnique) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorKey) + .execute(env.client) + ).getReceipt(env.client); + + const { serials } = await ( + await new TokenMintTransaction() + .setTokenId(nftId) + .addMetadata(Buffer.from("-")) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .execute(env.client) + ).getRecord(env.client); + + await ( + await new TokenAssociateTransaction() + .setAccountId(receiverId) + .setTokenIds([nftId]) + .freezeWith(env.client) + .sign(receiverKey) + ).execute(env.client); + + await ( + await new TokenFreezeTransaction() + .setTokenId(tokenId) + .setAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const [airdrop] = newPendingAirdrops; + const { airdropId } = airdrop; + await new TokenCancelAirdropTransaction() + .addPendingAirdropId(airdropId) + .execute(env.client); + + const ownerBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(ownerBalance.tokens.get(tokenId).toInt()).to.equal( + INITIAL_SUPPLY, + ); + expect(ownerBalance.tokens.get(nftId).toInt()).to.equal(1); + }); + + it("should cancel the token if paused", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFFFFF") + .setTokenSymbol("FFF") + .setTreasuryAccountId(env.operatorId) + .setInitialSupply(INITIAL_SUPPLY) + .setPauseKey(env.operatorKey) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .execute(env.client) + ).getRecord(env.client); + + await ( + await new TokenPauseTransaction() + .setTokenId(tokenId) + .execute(env.client) + ).getReceipt(env.client); + + const [airdrop] = newPendingAirdrops; + const { airdropId } = airdrop; + await new TokenCancelAirdropTransaction() + .addPendingAirdropId(airdropId) + .execute(env.client); + + const ownerBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(ownerBalance.tokens.get(tokenId).toInt()).to.equal( + INITIAL_SUPPLY, + ); + }); + + it("should cancel the token if token is deleted", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFFFFF") + .setTokenSymbol("FFF") + .setTreasuryAccountId(env.operatorId) + .setInitialSupply(INITIAL_SUPPLY) + .setAdminKey(env.operatorKey) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .execute(env.client) + ).getRecord(env.client); + + await ( + await new TokenDeleteTransaction() + .setTokenId(tokenId) + .execute(env.client) + ).getReceipt(env.client); + + const [airdrop] = newPendingAirdrops; + const { airdropId } = airdrop; + await new TokenCancelAirdropTransaction() + .addPendingAirdropId(airdropId) + .execute(env.client); + + const ownerBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(ownerBalance.tokens.get(tokenId).toInt()).to.equal( + INITIAL_SUPPLY, + ); + }); + + it("should cancel the tokens to multiple receivers when they are in pending state", async function () { + this.timeout(120000); + + // create nft and ft tokens + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorPublicKey) + .execute(env.client) + ).getReceipt(env.client); + + const { tokenId: nftId } = await ( + await new TokenCreateTransaction() + .setTokenName("nft") + .setTokenSymbol("NFT") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + // mint nfts + const tokenMintResponse = await new TokenMintTransaction() + .addMetadata(Buffer.from("-")) + .setTokenId(nftId) + .execute(env.client); + + const { serials } = await tokenMintResponse.getReceipt(env.client); + + const tokenMintResponse2 = await new TokenMintTransaction() + .addMetadata(Buffer.from("-")) + .setTokenId(nftId) + .execute(env.client); + + const { serials: serials2 } = await tokenMintResponse2.getReceipt( + env.client, + ); + + // generate accounts + const receiverPrivateKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverPrivateKey) + .execute(env.client) + ).getReceipt(env.client); + + const receiverPrivateKey2 = PrivateKey.generateED25519(); + const { accountId: receiverId2 } = await ( + await new AccountCreateTransaction() + .setKey(receiverPrivateKey2) + .execute(env.client) + ).getReceipt(env.client); + + // airdrop ft and nft + let tx = await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY / 2) + .addTokenTransfer(tokenId, receiverId2, INITIAL_SUPPLY / 2) + .addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addNftTransfer(nftId, serials2[0], env.operatorId, receiverId2) + .execute(env.client); + + // get airdrop ids for both FT and NFTs + const { newPendingAirdrops } = await tx.getRecord(env.client); + const pendingAirdropIds = newPendingAirdrops.map( + (pendingAirdrop) => pendingAirdrop.airdropId, + ); + + await ( + await new TokenCancelAirdropTransaction() + .setPendingAirdropIds(pendingAirdropIds) + .freezeWith(env.client) + .execute(env.client) + ).getReceipt(env.client); + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(operatorBalance.tokens.get(tokenId).toInt()).to.be.equal( + INITIAL_SUPPLY, + ); + expect(operatorBalance.tokens.get(nftId).toInt()).to.be.equal(2); + }); + + it("should cancel the tokens when they are in pending state with multiple airdrop ids", async function () { + this.timeout(120000); + // create nft and ft tokens + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorPublicKey) + .execute(env.client) + ).getReceipt(env.client); + + const { tokenId: nftId } = await ( + await new TokenCreateTransaction() + .setTokenName("nft") + .setTokenSymbol("NFT") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + // mint nfts + const tokenMintResponse = await new TokenMintTransaction() + .addMetadata(Buffer.from("-")) + .setTokenId(nftId) + .execute(env.client); + const { serials } = await tokenMintResponse.getReceipt(env.client); + + const tokenMintResponse2 = await new TokenMintTransaction() + .addMetadata(Buffer.from("-")) + .setTokenId(nftId) + .execute(env.client); + + const { serials: serials2 } = await tokenMintResponse2.getReceipt( + env.client, + ); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .execute(env.client) + ).getRecord(env.client); + + const { newPendingAirdrops: newPendingAirdrops2 } = await ( + await new TokenAirdropTransaction() + .addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addNftTransfer(nftId, serials2[0], env.operatorId, receiverId) + .execute(env.client) + ).getRecord(env.client); + + await ( + await new TokenCancelAirdropTransaction() + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .addPendingAirdropId(newPendingAirdrops2[0].airdropId) + .addPendingAirdropId(newPendingAirdrops2[1].airdropId) + .execute(env.client) + ).getReceipt(env.client); + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(operatorBalance.tokens.get(tokenId).toInt()).to.be.equal( + INITIAL_SUPPLY, + ); + expect(operatorBalance.tokens.get(nftId).toInt()).to.be.equal(2); + }); + + it("should not be able to cancel the tokens when they are not airdropped", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .execute(env.client) + ).getRecord(env.client); + + const randomAccountKey = PrivateKey.generateED25519(); + const { accountId: randomAccountId } = await ( + await new AccountCreateTransaction() + .setKey(randomAccountKey) + .execute(env.client) + ).getReceipt(env.client); + + let err = false; + try { + await ( + await new TokenCancelAirdropTransaction() + .setTransactionId(TransactionId.generate(randomAccountId)) + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .execute(env.client) + ).getReceipt(env.client); + } catch (error) { + err = error.message.includes("INVALID_SIGNATURE"); + } + + expect(err).to.be.true; + }); + + it("should not be able to cancel the tokens when they are already canceled", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const { tokenId: nftId } = await ( + await new TokenCreateTransaction() + .setTokenName("nft") + .setTokenSymbol("NFT") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const { serials } = await ( + await new TokenMintTransaction() + .setTokenId(nftId) + .addMetadata(Buffer.from("-")) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .execute(env.client) + ).getRecord(env.client); + + await ( + await new TokenCancelAirdropTransaction() + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .execute(env.client) + ).getReceipt(env.client); + + // recancel already canceled airdrop + let err = false; + try { + await ( + await new TokenCancelAirdropTransaction() + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .execute(env.client) + ).getReceipt(env.client); + } catch (error) { + err = error.message.includes("INVALID_PENDING_AIRDROP_ID"); + } + expect(err).to.be.true; + }); + + it("should not be able to cancel the tokens with empty list", async function () { + let err = false; + try { + await new TokenCancelAirdropTransaction().execute(env.client); + } catch (error) { + err = error.message.includes("EMPTY_PENDING_AIRDROP_ID_LIST"); + } + expect(err).to.be.true; + }); + + it("cannot cancel the tokens with duplicate entries", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(100) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + //.addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addTokenTransfer(tokenId, env.operatorId, -100) + .addTokenTransfer(tokenId, receiverId, 100) + .execute(env.client) + ).getRecord(env.client); + + let err = false; + try { + await new TokenCancelAirdropTransaction() + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .execute(env.client); + } catch (error) { + err = error.message.includes("PENDING_AIRDROP_ID_REPEATED"); + } + + expect(err).to.be.true; + }); + + after(async function () { + await env.close(); + }); +}); diff --git a/test/integration/TokenClaimAirdropTransaction.js b/test/integration/TokenClaimAirdropTransaction.js new file mode 100644 index 000000000..046789558 --- /dev/null +++ b/test/integration/TokenClaimAirdropTransaction.js @@ -0,0 +1,606 @@ +import { expect } from "chai"; +import { + AccountBalanceQuery, + AccountCreateTransaction, + TokenAirdropTransaction, + PrivateKey, + TokenAssociateTransaction, + TokenCreateTransaction, + TokenDeleteTransaction, + TokenFreezeTransaction, + TokenMintTransaction, + TokenPauseTransaction, + TokenType, + TokenClaimAirdropTransaction, + TransactionId, +} from "../../src/exports.js"; +import IntegrationTestEnv from "./client/NodeIntegrationTestEnv.js"; + +describe("TokenClaimAirdropIntegrationTest", function () { + let env, tx; + const INITIAL_SUPPLY = 1000; + + beforeEach(async function () { + env = await IntegrationTestEnv.new(); + }); + + it("should claim the tokens when they are in pending state", async function () { + this.timeout(120000); + + // create nft and ft tokens + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorPublicKey) + .execute(env.client) + ).getReceipt(env.client); + + const { tokenId: nftId } = await ( + await new TokenCreateTransaction() + .setTokenName("nft") + .setTokenSymbol("NFT") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + // mint nfts + const tokenMintResponse = await new TokenMintTransaction() + .setTokenId(nftId) + .addMetadata(Buffer.from("-")) + .execute(env.client); + + const { serials } = await tokenMintResponse.getReceipt(env.client); + + // generate accounts + const receiverPrivateKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverPrivateKey) + .execute(env.client) + ).getReceipt(env.client); + + // airdrop ft and nft + tx = await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .execute(env.client); + + // get airdrop ids for both FT and NFTs + const { newPendingAirdrops } = await tx.getRecord(env.client); + const [pendingAirdrop, pendingAirdropNft] = newPendingAirdrops; + const { airdropId } = pendingAirdrop; + const { airdropId: airdropNftId } = pendingAirdropNft; + + // claim airdrop + await ( + await ( + await new TokenClaimAirdropTransaction() + .addPendingAirdropId(airdropId) + .addPendingAirdropId(airdropNftId) + .freezeWith(env.client) + .sign(receiverPrivateKey) + ).execute(env.client) + ).getReceipt(env.client); + + // check balances + const receiverBalance = await new AccountBalanceQuery() + .setAccountId(receiverId) + .execute(env.client); + + expect(receiverBalance.tokens.get(tokenId).toInt()).to.be.equal( + INITIAL_SUPPLY, + ); + expect(receiverBalance.tokens.get(nftId).toInt()).to.be.equal(1); + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(operatorBalance.tokens.get(tokenId).toInt()).to.be.equal(0); + expect(operatorBalance.tokens.get(nftId).toInt()).to.be.equal(0); + }); + + it("should claim the tokens to multiple receivers when they are in pending state", async function () { + this.timeout(120000); + + // create nft and ft tokens + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorPublicKey) + .execute(env.client) + ).getReceipt(env.client); + + const { tokenId: nftId } = await ( + await new TokenCreateTransaction() + .setTokenName("nft") + .setTokenSymbol("NFT") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + // mint nfts + const tokenMintResponse = await new TokenMintTransaction() + .addMetadata(Buffer.from("-")) + .setTokenId(nftId) + .execute(env.client); + + const { serials } = await tokenMintResponse.getReceipt(env.client); + + const tokenMintResponse2 = await new TokenMintTransaction() + .addMetadata(Buffer.from("-")) + .setTokenId(nftId) + .execute(env.client); + + const { serials: serials2 } = await tokenMintResponse2.getReceipt( + env.client, + ); + + // generate accounts + const receiverPrivateKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverPrivateKey) + .execute(env.client) + ).getReceipt(env.client); + + const receiverPrivateKey2 = PrivateKey.generateED25519(); + const { accountId: receiverId2 } = await ( + await new AccountCreateTransaction() + .setKey(receiverPrivateKey2) + .execute(env.client) + ).getReceipt(env.client); + + // airdrop ft and nft + tx = await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY / 2) + .addTokenTransfer(tokenId, receiverId2, INITIAL_SUPPLY / 2) + .addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addNftTransfer(nftId, serials2[0], env.operatorId, receiverId2) + .execute(env.client); + + // get airdrop ids for both FT and NFTs + const { newPendingAirdrops } = await tx.getRecord(env.client); + const pendingAirdropIds = newPendingAirdrops.map( + (pendingAirdrop) => pendingAirdrop.airdropId, + ); + + await ( + await ( + await ( + await new TokenClaimAirdropTransaction() + .setPendingAirdropIds(pendingAirdropIds) + .freezeWith(env.client) + .sign(receiverPrivateKey) + ).sign(receiverPrivateKey2) + ).execute(env.client) + ).getReceipt(env.client); + + const receiverBalance = await new AccountBalanceQuery() + .setAccountId(receiverId) + .execute(env.client); + + const receiverBalance2 = await new AccountBalanceQuery() + .setAccountId(receiverId2) + .execute(env.client); + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(receiverBalance.tokens.get(tokenId).toInt()).to.be.equal( + INITIAL_SUPPLY / 2, + ); + + expect(receiverBalance.tokens.get(nftId).toInt()).to.be.equal(1); + + expect(receiverBalance2.tokens.get(tokenId).toInt()).to.be.equal( + INITIAL_SUPPLY / 2, + ); + expect(receiverBalance2.tokens.get(nftId).toInt()).to.be.equal(1); + + expect(operatorBalance.tokens.get(tokenId).toInt()).to.be.equal(0); + expect(operatorBalance.tokens.get(nftId).toInt()).to.be.equal(0); + }); + + it("should claim the tokens when they are in pending state with multiple airdrop ids", async function () { + this.timeout(120000); + // create nft and ft tokens + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .setSupplyKey(env.operatorPublicKey) + .execute(env.client) + ).getReceipt(env.client); + + const { tokenId: nftId } = await ( + await new TokenCreateTransaction() + .setTokenName("nft") + .setTokenSymbol("NFT") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + // mint nfts + const tokenMintResponse = await new TokenMintTransaction() + .addMetadata(Buffer.from("-")) + .setTokenId(nftId) + .execute(env.client); + const { serials } = await tokenMintResponse.getReceipt(env.client); + + const tokenMintResponse2 = await new TokenMintTransaction() + .addMetadata(Buffer.from("-")) + .setTokenId(nftId) + .execute(env.client); + + const { serials: serials2 } = await tokenMintResponse2.getReceipt( + env.client, + ); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .execute(env.client) + ).getRecord(env.client); + + const { newPendingAirdrops: newPendingAirdrops2 } = await ( + await new TokenAirdropTransaction() + .addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addNftTransfer(nftId, serials2[0], env.operatorId, receiverId) + .execute(env.client) + ).getRecord(env.client); + + await ( + await ( + await new TokenClaimAirdropTransaction() + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .addPendingAirdropId(newPendingAirdrops2[0].airdropId) + .addPendingAirdropId(newPendingAirdrops2[1].airdropId) + .freezeWith(env.client) + .sign(receiverKey) + ).execute(env.client) + ).getReceipt(env.client); + + const receiverBalance = await new AccountBalanceQuery() + .setAccountId(receiverId) + .execute(env.client); + + expect(receiverBalance.tokens.get(tokenId).toInt()).to.be.equal( + INITIAL_SUPPLY, + ); + expect(receiverBalance.tokens.get(nftId).toInt()).to.be.equal(2); + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(operatorBalance.tokens.get(tokenId).toInt()).to.be.equal(0); + expect(operatorBalance.tokens.get(nftId).toInt()).to.be.equal(0); + }); + + it("should not be able to claim the tokens when they are not airdropped", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFFFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .execute(env.client) + ).getRecord(env.client); + + const randomAccountKey = PrivateKey.generateED25519(); + const { accountId: randomAccountId } = await ( + await new AccountCreateTransaction() + .setKey(randomAccountKey) + .execute(env.client) + ).getReceipt(env.client); + + let err = false; + try { + await ( + await new TokenClaimAirdropTransaction() + .setTransactionId(TransactionId.generate(randomAccountId)) + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .execute(env.client) + ).getReceipt(env.client); + } catch (error) { + err = error.message.includes("INVALID_SIGNATURE"); + } + expect(err).to.be.true; + }); + + it("should not be able to claim the tokens when they are already claimed", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(INITIAL_SUPPLY) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const { tokenId: nftId } = await ( + await new TokenCreateTransaction() + .setTokenName("nft") + .setTokenSymbol("NFT") + .setTokenType(TokenType.NonFungibleUnique) + .setSupplyKey(env.operatorKey) + .setSupplyKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const { serials } = await ( + await new TokenMintTransaction() + .setTokenId(nftId) + .addMetadata(Buffer.from("-")) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + .addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .execute(env.client) + ).getRecord(env.client); + + await ( + await ( + await new TokenClaimAirdropTransaction() + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .freezeWith(env.client) + .sign(receiverKey) + ).execute(env.client) + ).getReceipt(env.client); + + // reclaim already claimed airdrop + let err = false; + try { + await ( + await ( + await new TokenClaimAirdropTransaction() + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .freezeWith(env.client) + .sign(receiverKey) + ).execute(env.client) + ).getReceipt(env.client); + } catch (error) { + err = error.message.includes("INVALID_PENDING_AIRDROP_ID"); + } + expect(err).to.be.true; + }); + + it("should not be able to claim the tokens with empty list", async function () { + let err = false; + try { + await new TokenClaimAirdropTransaction().execute(env.client); + } catch (error) { + err = error.message.includes("EMPTY_PENDING_AIRDROP_ID_LIST"); + } + expect(err).to.be.true; + }); + + it("should not claim the tokens with duplicate entries", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(100) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + const { newPendingAirdrops } = await ( + await new TokenAirdropTransaction() + //.addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addTokenTransfer(tokenId, env.operatorId, -100) + .addTokenTransfer(tokenId, receiverId, 100) + .execute(env.client) + ).getRecord(env.client); + + let err = false; + try { + await new TokenClaimAirdropTransaction() + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .addPendingAirdropId(newPendingAirdrops[0].airdropId) + .execute(env.client); + } catch (error) { + err = error.message.includes("PENDING_AIRDROP_ID_REPEATED"); + } + + expect(err).to.be.true; + }); + + it("should not be able to claim tokens when token is paused", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(100) + .setPauseKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + await ( + await new TokenPauseTransaction() + .setTokenId(tokenId) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + let err = false; + try { + await ( + await new TokenAirdropTransaction() + //.addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addTokenTransfer(tokenId, env.operatorId, -100) + .addTokenTransfer(tokenId, receiverId, 100) + .execute(env.client) + ).getRecord(env.client); + } catch (error) { + err = error.message.includes("TOKEN_IS_PAUSED"); + } + expect(err).to.be.true; + }); + + it("should not be able to claim tokens when token is deleted", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(100) + .setAdminKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + await ( + await new TokenDeleteTransaction() + .setTokenId(tokenId) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + let err = false; + try { + await ( + await new TokenAirdropTransaction() + //.addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addTokenTransfer(tokenId, env.operatorId, -100) + .addTokenTransfer(tokenId, receiverId, 100) + .execute(env.client) + ).getRecord(env.client); + } catch (error) { + err = error.message.includes("TOKEN_WAS_DELETED"); + } + expect(err).to.be.true; + }); + + it("should not be able to claim tokens when token is frozen", async function () { + this.timeout(120000); + const { tokenId } = await ( + await new TokenCreateTransaction() + .setTokenName("FFFFF") + .setTokenSymbol("FFF") + .setInitialSupply(100) + .setFreezeKey(env.operatorKey) + .setTreasuryAccountId(env.operatorId) + .execute(env.client) + ).getReceipt(env.client); + + const receiverKey = PrivateKey.generateED25519(); + const { accountId: receiverId } = await ( + await new AccountCreateTransaction() + .setKey(receiverKey) + .execute(env.client) + ).getReceipt(env.client); + + await ( + await new TokenAssociateTransaction() + .setAccountId(receiverId) + .setTokenIds([tokenId]) + .freezeWith(env.client) + .sign(receiverKey) + ).execute(env.client); + + await ( + await new TokenFreezeTransaction() + .setAccountId(receiverId) + .setTokenId(tokenId) + .execute(env.client) + ).getReceipt(env.client); + + let err = false; + try { + await ( + await new TokenAirdropTransaction() + //.addNftTransfer(nftId, serials[0], env.operatorId, receiverId) + .addTokenTransfer(tokenId, env.operatorId, -100) + .addTokenTransfer(tokenId, receiverId, 100) + .execute(env.client) + ).getRecord(env.client); + } catch (error) { + err = error.message.includes("ACCOUNT_FROZEN_FOR_TOKEN"); + } + expect(err).to.be.true; + }); + + after(async function () { + await env.close(); + }); +}); diff --git a/test/unit/AirdropCancelTransaction.js b/test/unit/AirdropCancelTransaction.js new file mode 100644 index 000000000..25e9e229d --- /dev/null +++ b/test/unit/AirdropCancelTransaction.js @@ -0,0 +1,23 @@ +import { AccountId, TokenId } from "../../src/exports.js"; +import TokenCancelAirdropTransaction from "../../src/token/TokenCancelAirdropTransaction.js"; +import PendingAirdropId from "../../src/token/PendingAirdropId.js"; + +describe("TokenAirdropCancelTransaction", function () { + it("from | to bytes", async function () { + const pendingAirdropId = new PendingAirdropId({ + tokenId: new TokenId(0, 0, 123), + serial: 456, + senderId: new AccountId(0, 0, 789), + receiverId: new AccountId(0, 0, 987), + }); + + const tx = new TokenCancelAirdropTransaction({ + pendingAirdropIds: [pendingAirdropId], + }); + console.log(tx.toBytes()); + + const tx2 = TokenCancelAirdropTransaction.fromBytes(tx.toBytes()); + + expect(tx2.pendingAirdropIds[0]).to.deep.equal(pendingAirdropId); + }); +}); diff --git a/test/unit/AirdropClaimTransaction.js b/test/unit/AirdropClaimTransaction.js new file mode 100644 index 000000000..9f47960f8 --- /dev/null +++ b/test/unit/AirdropClaimTransaction.js @@ -0,0 +1,21 @@ +import { AccountId, TokenId } from "../../src/exports.js"; +import TokenClaimAirdropTransaction from "../../src/token/TokenClaimAirdropTransaction.js"; +import PendingAirdropId from "../../src/token/PendingAirdropId.js"; + +describe("TokenClaimAirdropTransaction", function () { + it("from | to bytes", async function () { + const pendingAirdropId = new PendingAirdropId({ + tokenId: new TokenId(0, 0, 123), + serial: 456, + senderId: new AccountId(0, 0, 789), + receiverId: new AccountId(0, 0, 987), + }); + const tx = new TokenClaimAirdropTransaction({ + pendingAirdropIds: [pendingAirdropId], + }); + + const tx2 = TokenClaimAirdropTransaction.fromBytes(tx.toBytes()); + + expect(tx2.pendingAirdropIds[0]).to.deep.equal(pendingAirdropId); + }); +}); diff --git a/test/unit/TokenAirdropTransaction.js b/test/unit/TokenAirdropTransaction.js new file mode 100644 index 000000000..70d1f944b --- /dev/null +++ b/test/unit/TokenAirdropTransaction.js @@ -0,0 +1,103 @@ +import { expect } from "chai"; +import { + AccountId, + TokenAirdropTransaction, + NftId, + TokenId, +} from "../../src/index.js"; + +describe("TokenAirdropTransaction", function () { + it("from | toBytes", async function () { + const SENDER = new AccountId(0, 0, 100); + const RECEIVER = new AccountId(0, 0, 101); + const TOKEN_IDS = [ + new TokenId(0, 0, 1), + new TokenId(0, 0, 2), + new TokenId(0, 0, 3), + new TokenId(0, 0, 4), + ]; + const NFT_IDS = [ + new NftId(TOKEN_IDS[0], 1), + new NftId(TOKEN_IDS[0], 2), + ]; + const AMOUNT = 1000; + const EXPECTED_DECIMALS = 1; + + const transaction = new TokenAirdropTransaction() + .addTokenTransfer(TOKEN_IDS[0], SENDER, AMOUNT) + .addTokenTransferWithDecimals( + TOKEN_IDS[1], + SENDER, + AMOUNT, + EXPECTED_DECIMALS, + ) + .addApprovedTokenTransfer(TOKEN_IDS[2], SENDER, AMOUNT) + .addApprovedTokenTransferWithDecimals( + TOKEN_IDS[3], + SENDER, + AMOUNT, + EXPECTED_DECIMALS, + ) + .addNftTransfer(NFT_IDS[0], SENDER, RECEIVER) + .addApprovedNftTransfer(NFT_IDS[1], SENDER, RECEIVER); + + const txBytes = transaction.toBytes(); + const tx = TokenAirdropTransaction.fromBytes(txBytes); + + // normal token transfer + const tokenNormalTransfer = tx._tokenTransfers[0]; + expect(tokenNormalTransfer.tokenId).to.deep.equal(TOKEN_IDS[0]); + expect(tokenNormalTransfer.accountId).to.deep.equal(SENDER); + expect(tokenNormalTransfer.amount.toInt()).to.equal(AMOUNT); + + // token transfer with decimals + const tokenTransferWithDecimals = tx._tokenTransfers[1]; + expect(tokenTransferWithDecimals.tokenId).to.deep.equal(TOKEN_IDS[1]); + expect(tokenTransferWithDecimals.accountId).to.deep.equal(SENDER); + expect(tokenTransferWithDecimals.amount.toInt()).to.equal(AMOUNT); + + expect(tokenTransferWithDecimals.expectedDecimals).to.equal( + EXPECTED_DECIMALS, + ); + + // approved token transfer + const approvedTokenTransfer = tx._tokenTransfers[2]; + expect(approvedTokenTransfer.tokenId).to.deep.equal(TOKEN_IDS[2]); + expect(approvedTokenTransfer.accountId).to.deep.equal(SENDER); + expect(approvedTokenTransfer.amount.toInt()).to.equal(AMOUNT); + expect(approvedTokenTransfer.isApproved).to.equal(true); + + // approved token transfer with decimals + const approvedTokenTransferWithDecimals = tx._tokenTransfers[3]; + expect(approvedTokenTransferWithDecimals.tokenId).to.deep.equal( + TOKEN_IDS[3], + ); + expect(approvedTokenTransferWithDecimals.accountId).to.deep.equal( + SENDER, + ); + expect(approvedTokenTransferWithDecimals.amount.toInt()).to.equal( + AMOUNT, + ); + expect(approvedTokenTransferWithDecimals.isApproved).to.equal(true); + expect(approvedTokenTransferWithDecimals.expectedDecimals).to.equal( + EXPECTED_DECIMALS, + ); + + // nft transfer + const nftTransfer = tx._nftTransfers[0]; + expect(nftTransfer.tokenId).to.deep.equal(NFT_IDS[0].tokenId); + expect(nftTransfer.serialNumber).to.deep.equal(NFT_IDS[0].serial); + expect(nftTransfer.senderAccountId).to.deep.equal(SENDER); + expect(nftTransfer.receiverAccountId).to.deep.equal(RECEIVER); + + // approved nft transfer + const approvedNftTransfer = tx._nftTransfers[1]; + expect(approvedNftTransfer.tokenId).to.deep.equal(NFT_IDS[1].tokenId); + expect(approvedNftTransfer.serialNumber).to.deep.equal( + NFT_IDS[1].serial, + ); + expect(approvedNftTransfer.senderAccountId).to.deep.equal(SENDER); + expect(approvedNftTransfer.receiverAccountId).to.deep.equal(RECEIVER); + expect(approvedNftTransfer.isApproved).to.equal(true); + }); +}); From 0b264e357eda7f6a07c0ac0382874d75f9534748 Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Tue, 17 Sep 2024 09:02:16 +0300 Subject: [PATCH 09/13] refactor: Cleared some uneccessary comments & changed a bit the flow some methods conditions Signed-off-by: ivaylogarnev-limechain --- src/transaction/Transaction.js | 73 ++++++++++++++++------------------ 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/src/transaction/Transaction.js b/src/transaction/Transaction.js index df98a5bc9..5d746fcf3 100644 --- a/src/transaction/Transaction.js +++ b/src/transaction/Transaction.js @@ -858,7 +858,6 @@ export default class Transaction extends Executable { * @returns {Uint8Array[]} The removed signatures. */ removeSignature(publicKey) { - // If the transaction isn't frozen, freeze it. if (!this.isFrozen()) { this.freeze(); } @@ -902,15 +901,13 @@ export default class Transaction extends Executable { * It will call collectSignatures to get the removed signatures, then clear all signatures * from the internal tracking. * - * @returns {{ [transactionId: string]: { [nodeAccountId: string]: Uint8Array[] } }} The removed signatures in the specified format. + * @returns {{ [userPublicKey: string]: Uint8Array[] | Uint8Array }} The removed signatures in the specified format. */ removeAllSignatures() { - // If the transaction isn't frozen, freeze it. if (!this.isFrozen()) { this.freeze(); } - // Collect all signatures const removedSignatures = this._collectSignaturesByPublicKey(); // Iterate over the signed transactions and clear all signatures @@ -1835,26 +1832,25 @@ export default class Transaction extends Executable { /** @type {Uint8Array[]} */ const removedSignatures = []; - if (transaction.sigMap && transaction.sigMap.sigPair) { - transaction.sigMap.sigPair = transaction.sigMap.sigPair.filter( - (sigPair) => { - const shouldRemove = this._shouldRemoveSignature( - sigPair, - publicKeyHex, - ); + if (!transaction.sigMap || !transaction.sigMap.sigPair) { + return []; + } - if (shouldRemove) { - if (sigPair.ed25519) { - removedSignatures.push(sigPair.ed25519); - } else if (sigPair.ECDSASecp256k1) { - removedSignatures.push(sigPair.ECDSASecp256k1); - } - } + transaction.sigMap.sigPair = transaction.sigMap.sigPair.filter( + (sigPair) => { + const shouldRemove = this._shouldRemoveSignature( + sigPair, + publicKeyHex, + ); + const signature = sigPair.ed25519 ?? sigPair.ECDSASecp256k1; - return !shouldRemove; - }, - ); - } + if (shouldRemove && signature) { + removedSignatures.push(signature); + } + + return !shouldRemove; + }, + ); return removedSignatures; } @@ -1889,28 +1885,27 @@ export default class Transaction extends Executable { // Iterate over the signed transactions and collect signatures for (const transaction of this._signedTransactions.list) { + if (!(transaction.sigMap && transaction.sigMap.sigPair)) { + return []; + } + // Collect the signatures - if (transaction.sigMap && transaction.sigMap.sigPair) { - for (const sigPair of transaction.sigMap.sigPair) { - let signature; - if (sigPair.ed25519) { - signature = sigPair.ed25519; - } else if (sigPair.ECDSASecp256k1) { - signature = sigPair.ECDSASecp256k1; - } + for (const sigPair of transaction.sigMap.sigPair) { + const signature = sigPair.ed25519 ?? sigPair.ECDSASecp256k1; - if (signature && sigPair.pubKeyPrefix) { - const publicKeyHex = hex.encode(sigPair.pubKeyPrefix); + if (!signature || !sigPair.pubKeyPrefix) { + return []; + } - // Initialize the structure for this publicKey if it doesn't exist - if (!collectedSignatures[publicKeyHex]) { - collectedSignatures[publicKeyHex] = []; - } + const publicKeyHex = hex.encode(sigPair.pubKeyPrefix); - // Add the signature to the corresponding public key - collectedSignatures[publicKeyHex].push(signature); - } + // Initialize the structure for this publicKey if it doesn't exist + if (!collectedSignatures[publicKeyHex]) { + collectedSignatures[publicKeyHex] = []; } + + // Add the signature to the corresponding public key + collectedSignatures[publicKeyHex].push(signature); } } From f8a31524344ba6ee3238caffdb5fda2f0f1761fc Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Wed, 18 Sep 2024 14:04:50 +0300 Subject: [PATCH 10/13] Merge branch main into feat/2461-remove-clear-signatures Signed-off-by: ivaylogarnev-limechain --- src/PrivateKey.js | 20 +- src/transaction/Transaction.js | 56 ++-- test/integration/PrivateKey.js | 118 ++++++++ test/unit/PrivateKey.js | 121 ++++++++ test/unit/Transaction.js | 521 +++++++++++++++++++++------------ 5 files changed, 622 insertions(+), 214 deletions(-) create mode 100644 test/integration/PrivateKey.js create mode 100644 test/unit/PrivateKey.js diff --git a/src/PrivateKey.js b/src/PrivateKey.js index 418d3bbb2..41b35816e 100644 --- a/src/PrivateKey.js +++ b/src/PrivateKey.js @@ -325,16 +325,24 @@ export default class PrivateKey extends Key { /** * @param {Transaction} transaction - * @returns {Uint8Array} + * @returns {Uint8Array | Uint8Array[]} */ signTransaction(transaction) { - const tx = transaction._signedTransactions.get(0); - const signature = - tx.bodyBytes != null ? this.sign(tx.bodyBytes) : new Uint8Array(); + const signatures = transaction._signedTransactions.list.map( + (signedTransaction) => { + const bodyBytes = signedTransaction.bodyBytes; + + if (!bodyBytes) { + return new Uint8Array(); + } + + return this._key.sign(bodyBytes); + }, + ); - transaction.addSignature(this.publicKey, signature); + transaction.addSignature(this.publicKey, signatures); - return signature; + return signatures; } /** diff --git a/src/transaction/Transaction.js b/src/transaction/Transaction.js index 5d746fcf3..07e249bc7 100644 --- a/src/transaction/Transaction.js +++ b/src/transaction/Transaction.js @@ -789,20 +789,33 @@ export default class Transaction extends Executable { /** * Add a signature explicitly * - * This method requires the transaction to have exactly 1 node account ID set - * since different node account IDs have different byte representations and - * hence the same signature would not work for all transactions that are the same - * except for node account ID being different. + * This method supports both single and multiple signatures. A single signature will be applied to all transactions, + * While an array of signatures must correspond to each transaction individually. * * @param {PublicKey} publicKey - * @param {Uint8Array} signature + * @param {Uint8Array | Uint8Array[]} signature * @returns {this} */ addSignature(publicKey, signature) { - // Require that only one node is set on this transaction - // FIXME: This doesn't consider if we have one node account ID set, but we're - // also a chunked transaction. We should also check transaction IDs is of length 1 - this._requireOneNodeAccountId(); + const isSingleSignature = signature instanceof Uint8Array; + const isArraySignature = Array.isArray(signature); + + // Check if it is a single signature with NOT exactly one transaction + if (isSingleSignature && this._signedTransactions.length !== 1) { + throw new Error( + "Signature array must match the number of transactions", + ); + } + + // Check if it's an array but the array length doesn't match the number of transactions + if ( + isArraySignature && + signature.length !== this._signedTransactions.length + ) { + throw new Error( + "Signature array must match the number of transactions", + ); + } // If the transaction isn't frozen, freeze it. if (!this.isFrozen()) { @@ -817,7 +830,7 @@ export default class Transaction extends Executable { return this; } - // Transactions will have to be regenerated + // If we add a new signer, then we need to re-create all transactions this._transactions.clear(); // Locking the transaction IDs and node account IDs is necessary for consistency @@ -826,21 +839,22 @@ export default class Transaction extends Executable { this._nodeAccountIds.setLocked(); this._signedTransactions.setLocked(); - // Add the signature to the signed transaction list. This is a copy paste - // of `.signWith()`, but it really shouldn't be if `_signedTransactions.list` - // must be a length of one. - // FIXME: Remove unnecessary for loop. - for (const transaction of this._signedTransactions.list) { - if (transaction.sigMap == null) { - transaction.sigMap = {}; + const signatureArray = isSingleSignature ? [signature] : signature; + + // Add the signature to the signed transaction list + for (let index = 0; index < this._signedTransactions.length; index++) { + const signedTransaction = this._signedTransactions.get(index); + + if (signedTransaction.sigMap == null) { + signedTransaction.sigMap = {}; } - if (transaction.sigMap.sigPair == null) { - transaction.sigMap.sigPair = []; + if (signedTransaction.sigMap.sigPair == null) { + signedTransaction.sigMap.sigPair = []; } - transaction.sigMap.sigPair.push( - publicKey._toProtobufSignature(signature), + signedTransaction.sigMap.sigPair.push( + publicKey._toProtobufSignature(signatureArray[index]), ); } diff --git a/test/integration/PrivateKey.js b/test/integration/PrivateKey.js new file mode 100644 index 000000000..dc6a2ee8b --- /dev/null +++ b/test/integration/PrivateKey.js @@ -0,0 +1,118 @@ +import { + PrivateKey, + AccountCreateTransaction, + Hbar, + AccountId, + KeyList, + TransferTransaction, + Transaction, + Status, + FileAppendTransaction, + FileCreateTransaction, +} from "../../src/exports.js"; +import dotenv from "dotenv"; +import IntegrationTestEnv from "./client/NodeIntegrationTestEnv.js"; + +import { expect } from "chai"; + +dotenv.config(); + +describe("PrivateKey signTransaction", function () { + let env, user1Key, user2Key, createdAccountId, keyList; + + // Setting up the environment and creating a new account with a key list + before(async () => { + env = await IntegrationTestEnv.new(); + + user1Key = PrivateKey.generate(); + user2Key = PrivateKey.generate(); + keyList = new KeyList([user1Key.publicKey, user2Key.publicKey]); + + // Create account + const createAccountTransaction = new AccountCreateTransaction() + .setInitialBalance(new Hbar(2)) + .setKey(keyList); + + const createResponse = await createAccountTransaction.execute( + env.client, + ); + const createReceipt = await createResponse.getReceipt(env.client); + + createdAccountId = createReceipt.accountId; + + expect(createdAccountId).to.exist; + }); + + it("Transfer Transaction Execution with Multiple Nodes", async () => { + // Create and sign transfer transaction + const transferTransaction = new TransferTransaction() + .addHbarTransfer(createdAccountId, new Hbar(-1)) + .addHbarTransfer("0.0.3", new Hbar(1)) + .setNodeAccountIds([ + new AccountId(3), + new AccountId(4), + new AccountId(5), + ]) + .freezeWith(env.client); + + // Serialize and sign the transaction + const transferTransactionBytes = transferTransaction.toBytes(); + const user1Signatures = user1Key.signTransaction(transferTransaction); + const user2Signatures = user2Key.signTransaction(transferTransaction); + + // Deserialize the transaction and add signatures + const signedTransaction = Transaction.fromBytes( + transferTransactionBytes, + ); + signedTransaction.addSignature(user1Key.publicKey, user1Signatures); + signedTransaction.addSignature(user2Key.publicKey, user2Signatures); + + // Execute the signed transaction + const result = await signedTransaction.execute(env.client); + const receipt = await result.getReceipt(env.client); + + expect(receipt.status).to.be.equal(Status.Success); + }); + + it("File Append Transaction Execution with Multiple Nodes", async () => { + const operatorKey = env.operatorKey.publicKey; + + // Create file + let response = await new FileCreateTransaction() + .setKeys([operatorKey]) + .setContents("[e2e::FileCreateTransaction]") + .execute(env.client); + + let createTxReceipt = await response.getReceipt(env.client); + const file = createTxReceipt.fileId; + + // Append content to the file + const fileAppendTx = new FileAppendTransaction() + .setFileId(file) + .setContents("[e2e::FileAppendTransaction]") + .setNodeAccountIds([ + new AccountId(3), + new AccountId(4), + new AccountId(5), + ]) + .freezeWith(env.client); + + // Serialize and sign the transaction + const fileAppendTransactionBytes = fileAppendTx.toBytes(); + const user1Signatures = user1Key.signTransaction(fileAppendTx); + const user2Signatures = user2Key.signTransaction(fileAppendTx); + + // Deserialize the transaction and add signatures + const signedTransaction = Transaction.fromBytes( + fileAppendTransactionBytes, + ); + signedTransaction.addSignature(user1Key.publicKey, user1Signatures); + signedTransaction.addSignature(user2Key.publicKey, user2Signatures); + + // Execute the signed transaction + const result = await signedTransaction.execute(env.client); + const receipt = await result.getReceipt(env.client); + + expect(receipt.status).to.be.equal(Status.Success); + }); +}); diff --git a/test/unit/PrivateKey.js b/test/unit/PrivateKey.js new file mode 100644 index 000000000..31f2a7859 --- /dev/null +++ b/test/unit/PrivateKey.js @@ -0,0 +1,121 @@ +import { expect } from "chai"; +import sinon from "sinon"; + +import { PrivateKey } from "../../src/index.js"; +import Transaction from "../../src/transaction/Transaction.js"; + +describe("PrivateKey signTransaction", function () { + let privateKey, mockedTransaction, mockedSignature; + + beforeEach(() => { + privateKey = PrivateKey.generate(); + + mockedTransaction = sinon.createStubInstance(Transaction); + mockedSignature = new Uint8Array([4, 5, 6]); + + // Mock addSignature method on the transaction + mockedTransaction.addSignature = sinon.spy(); + }); + + it("should sign transaction and add signature", function () { + // Mock _signedTransactions.list to return an array with one signed transaction + mockedTransaction._signedTransactions = { + list: [{ bodyBytes: new Uint8Array([1, 2, 3]) }], + }; + + // Stub the _key.sign method to return a mock signature + privateKey._key = { + sign: sinon.stub().returns(mockedSignature), + }; + + // Call the real signTransaction method + const signatures = privateKey.signTransaction(mockedTransaction); + + // Validate that the signatures are correct + expect(signatures).to.deep.equal([mockedSignature]); + + sinon.assert.calledWith( + mockedTransaction.addSignature, + privateKey.publicKey, + [mockedSignature], + ); + + // Ensure that sign method of the privateKey._key was called + sinon.assert.calledOnce(privateKey._key.sign); + }); + + it("should return empty signature if bodyBytes are missing", function () { + // Set bodyBytes to null to simulate missing bodyBytes + mockedTransaction._signedTransactions = { + list: [{ bodyBytes: null }], + }; + + // Stub the _key.sign method to return a mock signature + privateKey._key = { + sign: sinon.stub().returns(mockedSignature), + }; + + // Call signTransaction method + const signatures = privateKey.signTransaction(mockedTransaction); + + // Validate that an empty Uint8Array was returned + expect(signatures).to.deep.equal([new Uint8Array()]); + + // Ensure that the transaction's addSignature method was called with the empty signature + sinon.assert.calledWith( + mockedTransaction.addSignature, + privateKey.publicKey, + [new Uint8Array()], + ); + + // Ensure that sign method of the privateKey._key was not called + sinon.assert.notCalled(privateKey._key.sign); + }); + + it("should sign transaction and add multiple signature", function () { + const mockedSignatures = [ + new Uint8Array([10, 11, 12]), + new Uint8Array([13, 14, 15]), + new Uint8Array([16, 17, 18]), + ]; + + const signedTransactions = [ + { bodyBytes: new Uint8Array([1, 2, 3]) }, + { bodyBytes: new Uint8Array([4, 5, 6]) }, + { bodyBytes: new Uint8Array([7, 8, 9]) }, + ]; + + // Mock _signedTransactions.list to return an array of transaction + mockedTransaction._signedTransactions = { + list: signedTransactions, + }; + + // Stub the _key.sign method to return a list of mock signature + privateKey._key = { + sign: sinon + .stub() + .onCall(0) + .returns(mockedSignatures[0]) + .onCall(1) + .returns(mockedSignatures[1]) + .onCall(2) + .returns(mockedSignatures[2]), + }; + + // Call the real signTransaction method + const signatures = privateKey.signTransaction(mockedTransaction); + + // Validate that the signatures are correct + expect(signatures).to.deep.equal(mockedSignatures); + + // Ensure that the transaction's addSignature method was called with the correct arguments + sinon.assert.calledWith( + mockedTransaction.addSignature, + privateKey.publicKey, + mockedSignatures, + ); + + // Ensure that sign method of the privateKey._key was called the correct number of times + sinon.assert.callCount(privateKey._key.sign, 3); + }); +}); diff --git a/test/unit/Transaction.js b/test/unit/Transaction.js index ceece7c91..4c698463b 100644 --- a/test/unit/Transaction.js +++ b/test/unit/Transaction.js @@ -10,12 +10,14 @@ import { Timestamp, Transaction, TransactionId, + PublicKey, } from "../../src/index.js"; import * as hex from "../../src/encoding/hex.js"; import Client from "../../src/client/NodeClient.js"; import * as HashgraphProto from "@hashgraph/proto"; import Long from "long"; import BigNumber from "bignumber.js"; +import sinon from "sinon"; describe("Transaction", function () { it("toBytes", async function () { @@ -313,235 +315,380 @@ describe("Transaction", function () { }); }); }); +}); + +describe("Transaction removeSignature/removeAllSignatures methods", () => { + let key1, key2, key3; + let transaction; + + beforeEach(async function () { + const nodeAccountId = new AccountId(3); + const account = AccountId.fromString("0.0.1004"); + const validStart = new Timestamp(1451, 590); + const transactionId = new TransactionId(account, validStart); + + key1 = PrivateKey.generateED25519(); + key2 = PrivateKey.generateED25519(); + key3 = PrivateKey.generateED25519(); + + transaction = new AccountCreateTransaction() + .setInitialBalance(new Hbar(2)) + .setTransactionId(transactionId) + .setNodeAccountIds([nodeAccountId]) + .freeze(); + }); - describe("Transaction removeSignature/removeAllSignatures methods", function () { - let key1, key2, key3; - let transaction; - - beforeEach(async function () { - const nodeAccountId = new AccountId(3); - const account = AccountId.fromString("0.0.1004"); - const validStart = new Timestamp(1451, 590); - const transactionId = new TransactionId(account, validStart); - - key1 = PrivateKey.generateED25519(); - key2 = PrivateKey.generateED25519(); - key3 = PrivateKey.generateED25519(); - - transaction = new AccountCreateTransaction() - .setInitialBalance(new Hbar(2)) - .setTransactionId(transactionId) - .setNodeAccountIds([nodeAccountId]) - .freeze(); + const signAndAddSignatures = (transaction, ...keys) => { + // Map through the keys to sign the transaction and add signatures + const signatures = keys.map((key) => { + const signature = key.signTransaction(transaction); + transaction.addSignature(key.publicKey, signature); + return signature; }); - const signAndAddSignatures = (transaction, ...keys) => { - // Map through the keys to sign the transaction and add signatures - const signatures = keys.map((key) => { - const signature = key.signTransaction(transaction); - transaction.addSignature(key.publicKey, signature); - return signature; - }); + return signatures; + }; - return signatures; - }; + it("should remove a specific signature", function () { + // Sign the transaction with multiple keys + const [signature1] = signAndAddSignatures( + transaction, + key1, + key2, + key3, + ); - it("should remove a specific signature", function () { - // Sign the transaction with multiple keys - const [signature1] = signAndAddSignatures( - transaction, - key1, - key2, - key3, - ); + //Check if the transaction internal tracking of signer public keys is correct + expect(transaction._signerPublicKeys.size).to.equal(3); + expect(transaction._publicKeys.length).to.equal(3); + expect(transaction._transactionSigners.length).to.equal(3); - //Check if the transaction internal tracking of signer public keys is correct - expect(transaction._signerPublicKeys.size).to.equal(3); - expect(transaction._publicKeys.length).to.equal(3); - expect(transaction._transactionSigners.length).to.equal(3); + // Ensure all signatures are present before removal + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); - // Ensure all signatures are present before removal - const signaturesBefore = transaction.getSignatures(); - expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); + // Remove one signature + transaction.removeSignature(key1.publicKey, signature1); - // Remove one signature - transaction.removeSignature(key1.publicKey, signature1); + //Check if the transaction is frozen + expect(transaction.isFrozen()).to.be.true; - //Check if the transaction is frozen - expect(transaction.isFrozen()).to.be.true; + //Check if the transaction internal tracking of signer public keys is correct + expect(transaction._signerPublicKeys.size).to.equal(2); + expect(transaction._publicKeys.length).to.equal(2); + expect(transaction._transactionSigners.length).to.equal(2); - //Check if the transaction internal tracking of signer public keys is correct - expect(transaction._signerPublicKeys.size).to.equal(2); - expect(transaction._publicKeys.length).to.equal(2); - expect(transaction._transactionSigners.length).to.equal(2); + // Ensure the specific signature has been removed + const signaturesAfter = transaction.getSignatures(); + expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); + }); - // Ensure the specific signature has been removed - const signaturesAfter = transaction.getSignatures(); - expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); - }); + it("should clear all signatures", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2, key3); - it("should clear all signatures", function () { - // Sign the transaction with multiple keys - signAndAddSignatures(transaction, key1, key2, key3); + // Ensure all signatures are present before clearing + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); - // Ensure all signatures are present before clearing - const signaturesBefore = transaction.getSignatures(); - expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); + // Clear all signatures + transaction.removeAllSignatures(); - // Clear all signatures - transaction.removeAllSignatures(); + //Check if the transaction is frozen + expect(transaction.isFrozen()).to.be.true; - //Check if the transaction is frozen - expect(transaction.isFrozen()).to.be.true; + //Check if the transaction internal tracking of signer public keys is cleared + expect(transaction._signerPublicKeys.size).to.equal(0); + expect(transaction._publicKeys.length).to.equal(0); + expect(transaction._transactionSigners.length).to.equal(0); - //Check if the transaction internal tracking of signer public keys is cleared - expect(transaction._signerPublicKeys.size).to.equal(0); - expect(transaction._publicKeys.length).to.equal(0); - expect(transaction._transactionSigners.length).to.equal(0); + // Ensure all signatures have been cleared + const signaturesAfter = transaction.getSignatures(); + expect(signaturesAfter.get(new AccountId(3)).size).to.equal(0); + }); - // Ensure all signatures have been cleared - const signaturesAfter = transaction.getSignatures(); - expect(signaturesAfter.get(new AccountId(3)).size).to.equal(0); - }); + it("should not remove a non-existing signature", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2); - it("should not remove a non-existing signature", function () { - // Sign the transaction with multiple keys - signAndAddSignatures(transaction, key1, key2); + // Attempt to remove a non-existing signature + expect(() => { + transaction.removeSignature(key3.publicKey); + }).to.throw("The public key has not signed this transaction"); - // Attempt to remove a non-existing signature - expect(() => { - transaction.removeSignature(key3.publicKey); - }).to.throw("The public key has not signed this transaction"); + // Ensure signatures are not affected + const signaturesAfter = transaction.getSignatures(); + expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); + }); - // Ensure signatures are not affected - const signaturesAfter = transaction.getSignatures(); - expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); - }); + it("should clear and re-sign after all signatures are cleared", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2); - it("should clear and re-sign after all signatures are cleared", function () { - // Sign the transaction with multiple keys - signAndAddSignatures(transaction, key1, key2); + // Ensure all signatures are present before clearing + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(2); - // Ensure all signatures are present before clearing - const signaturesBefore = transaction.getSignatures(); - expect(signaturesBefore.get(new AccountId(3)).size).to.equal(2); + // Clear all signatures + transaction.removeAllSignatures(); - // Clear all signatures - transaction.removeAllSignatures(); + // Ensure all signatures have been cleared + const signaturesAfterClear = transaction.getSignatures(); + expect(signaturesAfterClear.get(new AccountId(3)).size).to.equal(0); - // Ensure all signatures have been cleared - const signaturesAfterClear = transaction.getSignatures(); - expect(signaturesAfterClear.get(new AccountId(3)).size).to.equal(0); + // Re-sign the transaction with a different key + const signature3 = key3.signTransaction(transaction); + transaction.addSignature(key3.publicKey, signature3); - // Re-sign the transaction with a different key - const signature3 = key3.signTransaction(transaction); - transaction.addSignature(key3.publicKey, signature3); + // Ensure only one signature exists after re-signing + const signaturesAfterResign = transaction.getSignatures(); + expect(signaturesAfterResign.get(new AccountId(3)).size).to.equal(1); + }); - // Ensure only one signature exists after re-signing - const signaturesAfterResign = transaction.getSignatures(); - expect(signaturesAfterResign.get(new AccountId(3)).size).to.equal( - 1, - ); - }); + it("should return the removed signature in Uint8Array format when removing a specific signature", function () { + // Sign the transaction with multiple keys + const [signature1] = signAndAddSignatures( + transaction, + key1, + key2, + key3, + ); - it("should return the removed signature in Uint8Array format when removing a specific signature", function () { - // Sign the transaction with multiple keys - const [signature1] = signAndAddSignatures( - transaction, - key1, - key2, - key3, - ); + // Remove one signature and capture the returned value + const removedSignatures = transaction.removeSignature( + key1.publicKey, + signature1, + ); - // Remove one signature and capture the returned value - const removedSignatures = transaction.removeSignature( - key1.publicKey, - signature1, - ); + // Check the format of the returned value + expect(removedSignatures).to.be.an("array"); + expect(removedSignatures.length).to.equal(1); + expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); - // Check the format of the returned value - expect(removedSignatures).to.be.an("array"); - expect(removedSignatures.length).to.equal(1); - expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); + // Ensure the returned signature is the one that was removed + expect(removedSignatures[0]).to.deep.equal(signature1); + }); - // Ensure the returned signature is the one that was removed - expect(removedSignatures[0]).to.deep.equal(signature1); - }); + it("should return the signatures for a public key in Uint8Array format when only the public key is provided", function () { + // Sign the transaction with multiple keys + const [signature1] = signAndAddSignatures(transaction, key1); - it("should return the signatures for a public key in Uint8Array format when only the public key is provided", function () { - // Sign the transaction with multiple keys - const [signature1] = signAndAddSignatures(transaction, key1); + // Remove all signatures for key1 and capture the returned value + const removedSignatures = transaction.removeSignature(key1.publicKey); - // Remove all signatures for key1 and capture the returned value - const removedSignatures = transaction.removeSignature( - key1.publicKey, - ); + // Check the format of the returned value + expect(removedSignatures).to.be.an("array"); + expect(removedSignatures.length).to.equal(1); + expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); - // Check the format of the returned value - expect(removedSignatures).to.be.an("array"); - expect(removedSignatures.length).to.equal(1); - expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); + // Ensure the returned signatures are the ones that were removed + expect(removedSignatures).to.include.members([signature1]); + }); - // Ensure the returned signatures are the ones that were removed - expect(removedSignatures).to.include.members([signature1]); - }); + it("should return all removed signatures in the expected object format when clearing all signatures", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2, key3); - it("should return all removed signatures in the expected object format when clearing all signatures", function () { - // Sign the transaction with multiple keys - signAndAddSignatures(transaction, key1, key2, key3); + // Clear all signatures and capture the returned value + const removedSignatures = transaction.removeAllSignatures(); - // Clear all signatures and capture the returned value - const removedSignatures = transaction.removeAllSignatures(); + // Check the format of the returned value + expect(removedSignatures).to.be.an("object"); - // Check the format of the returned value - expect(removedSignatures).to.be.an("object"); + // Ensure the object has the correct structure + expect(removedSignatures).to.have.property( + key1.publicKey.toStringRaw(), + ); + expect(removedSignatures[key1.publicKey.toStringRaw()]).to.be.an( + "array", + ); + expect(removedSignatures).to.have.property( + key2.publicKey.toStringRaw(), + ); + expect(removedSignatures[key2.publicKey.toStringRaw()]).to.be.an( + "array", + ); + expect(removedSignatures).to.have.property( + key3.publicKey.toStringRaw(), + ); + expect(removedSignatures[key3.publicKey.toStringRaw()]).to.be.an( + "array", + ); - // Ensure the object has the correct structure - expect(removedSignatures).to.have.property( - key1.publicKey.toStringRaw(), - ); - expect(removedSignatures[key1.publicKey.toStringRaw()]).to.be.an( - "array", - ); - expect(removedSignatures).to.have.property( - key2.publicKey.toStringRaw(), - ); - expect(removedSignatures[key2.publicKey.toStringRaw()]).to.be.an( - "array", - ); - expect(removedSignatures).to.have.property( - key3.publicKey.toStringRaw(), - ); - expect(removedSignatures[key3.publicKey.toStringRaw()]).to.be.an( - "array", - ); + // Ensure the removed signatures are in the expected format + const signaturesArray1 = + removedSignatures[key1.publicKey.toStringRaw()]; + const signaturesArray2 = + removedSignatures[key2.publicKey.toStringRaw()]; + const signaturesArray3 = + removedSignatures[key3.publicKey.toStringRaw()]; + + [signaturesArray1, signaturesArray2, signaturesArray3].forEach( + (signaturesArray) => { + signaturesArray.forEach((sig) => { + expect(sig).to.be.instanceOf(Uint8Array); + }); + }, + ); + }); + + it("should return an empty object when no signatures are present", function () { + expect(transaction._signerPublicKeys.size).to.equal(0); + + // Clear all signatures and capture the returned value + const removedSignatures = transaction.removeAllSignatures(); - // Ensure the removed signatures are in the expected format - const signaturesArray1 = - removedSignatures[key1.publicKey.toStringRaw()]; - const signaturesArray2 = - removedSignatures[key2.publicKey.toStringRaw()]; - const signaturesArray3 = - removedSignatures[key3.publicKey.toStringRaw()]; - - [signaturesArray1, signaturesArray2, signaturesArray3].forEach( - (signaturesArray) => { - signaturesArray.forEach((sig) => { - expect(sig).to.be.instanceOf(Uint8Array); - }); + // Check the format of the returned value + expect(removedSignatures).to.be.an("object"); + expect(Object.keys(removedSignatures)).to.have.lengthOf(0); + }); +}); + +describe("addSignature tests", () => { + let transaction, mockedPublicKey, mockedSignature; + + beforeEach(() => { + transaction = new Transaction(); + mockedPublicKey = sinon.createStubInstance(PublicKey); + mockedSignature = new Uint8Array([4, 5, 6]); + + // Mock methods of PublicKey + mockedPublicKey.toBytesRaw.returns(new Uint8Array([1, 2, 3])); + mockedPublicKey._toProtobufSignature = sinon + .stub() + .returns({ ed25519: mockedSignature }); + + // Mock the necessary internals of the Transaction object + transaction._signedTransactions = { + length: 1, + list: [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, }, - ); - }); + ], + get(index) { + return this.list[index]; + }, + }; - it("should return an empty object when no signatures are present", function () { - expect(transaction._signerPublicKeys.size).to.equal(0); + transaction._transactionIds = { setLocked: sinon.spy() }; + transaction._nodeAccountIds = { setLocked: sinon.spy() }; + transaction._signedTransactions.setLocked = sinon.spy(); - // Clear all signatures and capture the returned value - const removedSignatures = transaction.removeAllSignatures(); + // Assume that the transaction is not frozen initially + transaction.isFrozen = sinon.stub().returns(false); + transaction.freeze = sinon.spy(); + }); - // Check the format of the returned value - expect(removedSignatures).to.be.an("object"); - expect(Object.keys(removedSignatures)).to.have.lengthOf(0); - }); + it("should add a single signature when one transaction is present", () => { + transaction.addSignature(mockedPublicKey, mockedSignature); + + // Verify the signature was added correctly to sigMap.sigPair + expect( + transaction._signedTransactions.get(0).sigMap.sigPair, + ).to.deep.equal([{ ed25519: mockedSignature }]); + + // Verify freeze was called since the transaction was not frozen + sinon.assert.calledOnce(transaction.freeze); + }); + + it("should throw an error when adding a single signature to multiple transactions", () => { + transaction._signedTransactions.length = 2; + transaction._signedTransactions.list = [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, + }, + { + bodyBytes: new Uint8Array([4, 5, 6]), + sigMap: { sigPair: [] }, + }, + ]; + + expect(() => { + transaction.addSignature(mockedPublicKey, mockedSignature); + }).to.throw("Signature array must match the number of transactions"); + }); + + it("should add multiple signatures corresponding to each transaction", () => { + const mockedSignatures = [ + new Uint8Array([10, 11, 12]), + new Uint8Array([13, 14, 15]), + ]; + + transaction._signedTransactions.length = 2; + transaction._signedTransactions.list = [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, + }, + { + bodyBytes: new Uint8Array([4, 5, 6]), + sigMap: { sigPair: [] }, + }, + ]; + + // Update stub to return correct signatures for each call + mockedPublicKey._toProtobufSignature + .onCall(0) + .returns({ ed25519: mockedSignatures[0] }) + .onCall(1) + .returns({ ed25519: mockedSignatures[1] }); + + transaction.addSignature(mockedPublicKey, mockedSignatures); + + // Verify the signatures were added correctly + expect( + transaction._signedTransactions.get(0).sigMap.sigPair, + ).to.deep.equal([{ ed25519: mockedSignatures[0] }]); + expect( + transaction._signedTransactions.get(1).sigMap.sigPair, + ).to.deep.equal([{ ed25519: mockedSignatures[1] }]); + + // Validate the calls to _toProtobufSignature + sinon.assert.calledWith( + mockedPublicKey._toProtobufSignature, + mockedSignatures[0], + ); + sinon.assert.calledWith( + mockedPublicKey._toProtobufSignature, + mockedSignatures[1], + ); + }); + + it("should throw an error when signature array length doesn't match the number of transactions", () => { + const mismatchedSignatures = [new Uint8Array([10, 11, 12])]; + transaction._signedTransactions.length = 2; + transaction._signedTransactions.list = [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, + }, + { + bodyBytes: new Uint8Array([4, 5, 6]), + sigMap: { sigPair: [] }, + }, + ]; + + expect(() => { + transaction.addSignature(mockedPublicKey, mismatchedSignatures); + }).to.throw("Signature array must match the number of transactions"); + }); + + it("should freeze the transaction if it is not frozen", () => { + transaction.isFrozen.returns(false); + + transaction._signedTransactions.length = 1; + transaction._signedTransactions.list = [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, + }, + ]; + + transaction.addSignature(mockedPublicKey, mockedSignature); + + sinon.assert.calledOnce(transaction.freeze); }); }); From e3cc17ae9ac2708e19c7d9ef14560adf29487aab Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Wed, 18 Sep 2024 16:18:01 +0300 Subject: [PATCH 11/13] refactor: Refactored signTransaction method a bit, added integration tests and examples for the multi-node flow Signed-off-by: ivaylogarnev-limechain --- examples/multi-node-multi-signature-remove.js | 199 ++++++ .../multi-node-multi-signature-removeAll.js | 206 ++++++ examples/multi-node-multi-signature.js | 165 +++++ src/PrivateKey.js | 3 +- test/integration/PrivateKey.js | 6 +- .../integration/TransactionIntegrationTest.js | 137 ++++ test/unit/PrivateKey.js | 6 +- test/unit/Transaction.js | 640 +++++++++--------- 8 files changed, 1039 insertions(+), 323 deletions(-) create mode 100644 examples/multi-node-multi-signature-remove.js create mode 100644 examples/multi-node-multi-signature-removeAll.js create mode 100644 examples/multi-node-multi-signature.js diff --git a/examples/multi-node-multi-signature-remove.js b/examples/multi-node-multi-signature-remove.js new file mode 100644 index 000000000..b87272dce --- /dev/null +++ b/examples/multi-node-multi-signature-remove.js @@ -0,0 +1,199 @@ +import { + Client, + PrivateKey, + AccountCreateTransaction, + Hbar, + AccountId, + KeyList, + TransferTransaction, + Transaction, +} from "@hashgraph/sdk"; + +import dotenv from "dotenv"; + +dotenv.config(); + +let aliceKey; +let bobKey; + +async function main() { + /** + * + * Step 1: Create Client + * + **/ + if ( + process.env.OPERATOR_ID == null || + process.env.OPERATOR_KEY == null || + process.env.HEDERA_NETWORK == null + ) { + throw new Error( + "Environment variables OPERATOR_ID, HEDERA_NETWORK, and OPERATOR_KEY are required.", + ); + } + + const client = Client.forName(process.env.HEDERA_NETWORK).setOperator( + AccountId.fromString(process.env.OPERATOR_ID), + PrivateKey.fromStringED25519(process.env.OPERATOR_KEY), + ); + + /** + * + * Step 2: Create keys for two users + * + **/ + aliceKey = PrivateKey.generate(); + bobKey = PrivateKey.generate(); + + const keyList = new KeyList([aliceKey.publicKey, bobKey.publicKey]); + + /** + * + * Step 3: Create an account with the keyList + * + **/ + const createAccountTransaction = new AccountCreateTransaction() + .setInitialBalance(new Hbar(2)) + .setKey(keyList); + + const createResponse = await createAccountTransaction.execute(client); + const createReceipt = await createResponse.getReceipt(client); + + /** + * + * Step 4: Create a transfer transaction with multiple nodes + * + **/ + const transferTransaction = new TransferTransaction() + .addHbarTransfer(createReceipt.accountId, new Hbar(-1)) + .addHbarTransfer("0.0.3", new Hbar(1)) + // Set multiple nodes + .setNodeAccountIds([ + new AccountId(3), + new AccountId(4), + new AccountId(5), + ]) + .freezeWith(client); + + /** + * + * Step 5: Serialize the transaction + * & Collect multiple signatures (Uint8Array[]) from one key + * + **/ + + const transferTransactionBytes = transferTransaction.toBytes(); + + const aliceSignatures = aliceKey.signTransaction(transferTransaction); + const bobSignatures = bobKey.signTransaction(transferTransaction); + + /** + * + * Step 6: Deserialize the transaction + * & Add the previously collected signatures + * + **/ + const signedTransaction = Transaction.fromBytes(transferTransactionBytes); + + signedTransaction.addSignature(aliceKey.publicKey, aliceSignatures); + signedTransaction.addSignature(bobKey.publicKey, bobSignatures); + + console.log("ADDED users signatures below: \n"); + + if (Array.isArray(aliceSignatures) && Array.isArray(bobSignatures)) { + console.log( + "Alice Signatures =>", + aliceSignatures.map((aliceSig) => + PrivateKey.fromBytes(aliceSig).toStringDer(), + ), + ); + + console.log( + "Bob Signatures =>", + bobSignatures.map((bobSig) => + PrivateKey.fromBytes(bobSig).toStringDer(), + ), + ); + } + + const signaturesInTheTransactionBefore = + getAllSignaturesFromTransaction(signedTransaction); + + console.log("\n\nSignatures in the transaction: "); + console.log(signaturesInTheTransactionBefore); + + /** + * + * Step 7: Remove the signatures for Alice from the transaction + * + **/ + + const removedAliceSignatures = signedTransaction.removeSignature( + aliceKey.publicKey, + ); + + console.log("\nREMOVED Alice signatures below: \n"); + + if (Array.isArray(aliceSignatures) && Array.isArray(bobSignatures)) { + console.log( + "Alice removed signatures =>", + removedAliceSignatures.map((aliceSig) => + PrivateKey.fromBytes(aliceSig).toStringDer(), + ), + ); + } + + const signaturesInTheTransactionAfter = + getAllSignaturesFromTransaction(signedTransaction); + + console.log("\n\nSignatures left in the transaction: "); + console.log(signaturesInTheTransactionAfter); + + /** + * + * Step 8: Add the removed signature back to the transaction + * + **/ + signedTransaction.addSignature(aliceKey.publicKey, removedAliceSignatures); + + /** + * + * Step 9: Execute and take the receipt + * + **/ + const result = await signedTransaction.execute(client); + + const receipt = await result.getReceipt(client); + + console.log(`\n \nTransaction status: ${receipt.status.toString()}`); + + client.close(); +} + +void main(); + +const getAllSignaturesFromTransaction = (signedTransaction) => { + const signatures = []; + + signedTransaction._signedTransactions.list.forEach((transaction) => { + if (transaction.sigMap?.sigPair) { + transaction.sigMap.sigPair.forEach((sigPair) => { + if (sigPair.ed25519) { + signatures.push( + PrivateKey.fromBytesED25519( + sigPair.ed25519, + ).toStringDer(), + ); + } else if (sigPair.ECDSASecp256k1) { + signatures.push( + PrivateKey.fromBytesECDSA( + sigPair.ECDSASecp256k1, + ).toStringDer(), + ); + } + }); + } + }); + + return signatures; +}; diff --git a/examples/multi-node-multi-signature-removeAll.js b/examples/multi-node-multi-signature-removeAll.js new file mode 100644 index 000000000..1241d056f --- /dev/null +++ b/examples/multi-node-multi-signature-removeAll.js @@ -0,0 +1,206 @@ +import { + Client, + PrivateKey, + AccountCreateTransaction, + Hbar, + AccountId, + KeyList, + TransferTransaction, + Transaction, + PublicKey, +} from "@hashgraph/sdk"; + +import dotenv from "dotenv"; + +dotenv.config(); + +let aliceKey; +let bobKey; + +async function main() { + /** + * + * Step 1: Create Client + * + **/ + if ( + process.env.OPERATOR_ID == null || + process.env.OPERATOR_KEY == null || + process.env.HEDERA_NETWORK == null + ) { + throw new Error( + "Environment variables OPERATOR_ID, HEDERA_NETWORK, and OPERATOR_KEY are required.", + ); + } + + const client = Client.forName(process.env.HEDERA_NETWORK).setOperator( + AccountId.fromString(process.env.OPERATOR_ID), + PrivateKey.fromStringED25519(process.env.OPERATOR_KEY), + ); + + /** + * + * Step 2: Create keys for two users + * + **/ + aliceKey = PrivateKey.generate(); + bobKey = PrivateKey.generate(); + + console.log("Alice public key:", aliceKey.publicKey.toStringDer()); + console.log("Bob public key:", bobKey.publicKey.toStringDer()); + + const keyList = new KeyList([aliceKey.publicKey, bobKey.publicKey]); + + /** + * + * Step 3: Create an account with the keyList + * + **/ + const createAccountTransaction = new AccountCreateTransaction() + .setInitialBalance(new Hbar(2)) + .setKey(keyList); + + const createResponse = await createAccountTransaction.execute(client); + const createReceipt = await createResponse.getReceipt(client); + + /** + * + * Step 4: Create a transfer transaction with multiple nodes + * + **/ + const transferTransaction = new TransferTransaction() + .addHbarTransfer(createReceipt.accountId, new Hbar(-1)) + .addHbarTransfer("0.0.3", new Hbar(1)) + // Set multiple nodes + .setNodeAccountIds([ + new AccountId(3), + new AccountId(4), + new AccountId(5), + ]) + .freezeWith(client); + + /** + * + * Step 5: Serialize the transaction + * & Collect multiple signatures (Uint8Array[]) from one key + * + **/ + + const transferTransactionBytes = transferTransaction.toBytes(); + + const aliceSignatures = aliceKey.signTransaction(transferTransaction); + const bobSignatures = bobKey.signTransaction(transferTransaction); + + /** + * + * Step 6: Deserialize the transaction + * & Add the previously collected signatures + * + **/ + const signedTransaction = Transaction.fromBytes(transferTransactionBytes); + + signedTransaction.addSignature(aliceKey.publicKey, aliceSignatures); + signedTransaction.addSignature(bobKey.publicKey, bobSignatures); + + console.log("\nADDED users signatures below: \n"); + + if (Array.isArray(aliceSignatures) && Array.isArray(bobSignatures)) { + console.log( + "Alice Signatures =>", + aliceSignatures.map((aliceSig) => + PrivateKey.fromBytes(aliceSig).toStringDer(), + ), + ); + + console.log( + "Bob Signatures =>", + bobSignatures.map((bobSig) => + PrivateKey.fromBytes(bobSig).toStringDer(), + ), + ); + } + + const signaturesInTheTransactionBefore = + getAllSignaturesFromTransaction(signedTransaction); + + console.log("\n\nSignatures in the transaction: "); + console.log(signaturesInTheTransactionBefore); + + /** + * + * Step 7: Remove all signatures from the transaction and add them back + * + **/ + + const allSignaturesRemoved = signedTransaction.removeAllSignatures(); + + const signaturesInTheTransactionAfter = + getAllSignaturesFromTransaction(signedTransaction); + + console.log( + "\nSignatures left in the transaction:", + signaturesInTheTransactionAfter, + ); + + for (const [publicKey, signatures] of Object.entries( + allSignaturesRemoved, + )) { + // Show the signatures for Alice & Bob + console.log(`\nRemoved signatures for ${publicKey}:`); + + if (Array.isArray(signatures)) { + console.log( + signatures.map((sig) => + PrivateKey.fromBytes(sig).toStringDer(), + ), + ); + } + + // Add the removed signatures back + signedTransaction.addSignature( + PublicKey.fromString(publicKey), + signatures, + ); + } + + /** + * + * Step 8: Execute and take the receipt + * + **/ + const result = await signedTransaction.execute(client); + + const receipt = await result.getReceipt(client); + + console.log(`\n \nTransaction status: ${receipt.status.toString()}`); + + client.close(); +} + +void main(); + +const getAllSignaturesFromTransaction = (signedTransaction) => { + const signatures = []; + + signedTransaction._signedTransactions.list.forEach((transaction) => { + if (transaction.sigMap?.sigPair) { + transaction.sigMap.sigPair.forEach((sigPair) => { + if (sigPair.ed25519) { + signatures.push( + PrivateKey.fromBytesED25519( + sigPair.ed25519, + ).toStringDer(), + ); + } else if (sigPair.ECDSASecp256k1) { + signatures.push( + PrivateKey.fromBytesECDSA( + sigPair.ECDSASecp256k1, + ).toStringDer(), + ); + } + }); + } + }); + + return signatures; +}; diff --git a/examples/multi-node-multi-signature.js b/examples/multi-node-multi-signature.js new file mode 100644 index 000000000..14fdca622 --- /dev/null +++ b/examples/multi-node-multi-signature.js @@ -0,0 +1,165 @@ +import { + Client, + PrivateKey, + AccountCreateTransaction, + Hbar, + AccountId, + KeyList, + TransferTransaction, + Transaction, +} from "@hashgraph/sdk"; + +import dotenv from "dotenv"; + +dotenv.config(); + +let aliceKey; +let bobKey; + +async function main() { + /** + * + * Step 1: Create Client + * + **/ + if ( + process.env.OPERATOR_ID == null || + process.env.OPERATOR_KEY == null || + process.env.HEDERA_NETWORK == null + ) { + throw new Error( + "Environment variables OPERATOR_ID, HEDERA_NETWORK, and OPERATOR_KEY are required.", + ); + } + + const client = Client.forName(process.env.HEDERA_NETWORK).setOperator( + AccountId.fromString(process.env.OPERATOR_ID), + PrivateKey.fromStringED25519(process.env.OPERATOR_KEY), + ); + + /** + * + * Step 2: Create keys for two users + * + **/ + aliceKey = PrivateKey.generate(); + bobKey = PrivateKey.generate(); + + const keyList = new KeyList([aliceKey.publicKey, bobKey.publicKey]); + + /** + * + * Step 3: Create an account with the keyList + * + **/ + const createAccountTransaction = new AccountCreateTransaction() + .setInitialBalance(new Hbar(2)) + .setKey(keyList); + + const createResponse = await createAccountTransaction.execute(client); + const createReceipt = await createResponse.getReceipt(client); + + /** + * + * Step 4: Create a transfer transaction with multiple nodes + * + **/ + const transferTransaction = new TransferTransaction() + .addHbarTransfer(createReceipt.accountId, new Hbar(-1)) + .addHbarTransfer("0.0.3", new Hbar(1)) + // Set multiple nodes + .setNodeAccountIds([ + new AccountId(3), + new AccountId(4), + new AccountId(5), + ]) + .freezeWith(client); + + /** + * + * Step 5: Serialize the transaction + * & Collect multiple signatures (Uint8Array[]) from one key + * + **/ + + const transferTransactionBytes = transferTransaction.toBytes(); + + const aliceSignatures = aliceKey.signTransaction(transferTransaction); + const bobSignatures = bobKey.signTransaction(transferTransaction); + + /** + * + * Step 6: Deserialize the transaction + * & Add the previously collected signatures + * + **/ + const signedTransaction = Transaction.fromBytes(transferTransactionBytes); + + signedTransaction.addSignature(aliceKey.publicKey, aliceSignatures); + signedTransaction.addSignature(bobKey.publicKey, bobSignatures); + + console.log("ADDED users signatures below: \n"); + + if (Array.isArray(aliceSignatures) && Array.isArray(bobSignatures)) { + console.log( + "Alice Signatures =>", + aliceSignatures.map((aliceSig) => + PrivateKey.fromBytes(aliceSig).toStringDer(), + ), + ); + + console.log( + "Bob Signatures =>", + bobSignatures.map((bobSig) => + PrivateKey.fromBytes(bobSig).toStringDer(), + ), + ); + } + + const signaturesInTheTransactionBefore = + getAllSignaturesFromTransaction(signedTransaction); + + console.log("\n\nSignatures in the transaction: "); + console.log(signaturesInTheTransactionBefore); + + /** + * + * Step 7: Execute and take the receipt + * + **/ + const result = await signedTransaction.execute(client); + + const receipt = await result.getReceipt(client); + + console.log(`\n \nTransaction status: ${receipt.status.toString()}`); + + client.close(); +} + +void main(); + +const getAllSignaturesFromTransaction = (signedTransaction) => { + const signatures = []; + + signedTransaction._signedTransactions.list.forEach((transaction) => { + if (transaction.sigMap?.sigPair) { + transaction.sigMap.sigPair.forEach((sigPair) => { + if (sigPair.ed25519) { + signatures.push( + PrivateKey.fromBytesED25519( + sigPair.ed25519, + ).toStringDer(), + ); + } else if (sigPair.ECDSASecp256k1) { + signatures.push( + PrivateKey.fromBytesECDSA( + sigPair.ECDSASecp256k1, + ).toStringDer(), + ); + } + }); + } + }); + + return signatures; +}; diff --git a/src/PrivateKey.js b/src/PrivateKey.js index 41b35816e..a802053d2 100644 --- a/src/PrivateKey.js +++ b/src/PrivateKey.js @@ -342,7 +342,8 @@ export default class PrivateKey extends Key { transaction.addSignature(this.publicKey, signatures); - return signatures; + // Return directly Uint8Array if there is only one signature + return signatures.length === 1 ? signatures[0] : signatures; } /** diff --git a/test/integration/PrivateKey.js b/test/integration/PrivateKey.js index dc6a2ee8b..262a9486e 100644 --- a/test/integration/PrivateKey.js +++ b/test/integration/PrivateKey.js @@ -21,7 +21,7 @@ describe("PrivateKey signTransaction", function () { let env, user1Key, user2Key, createdAccountId, keyList; // Setting up the environment and creating a new account with a key list - before(async () => { + before(async function () { env = await IntegrationTestEnv.new(); user1Key = PrivateKey.generate(); @@ -43,7 +43,7 @@ describe("PrivateKey signTransaction", function () { expect(createdAccountId).to.exist; }); - it("Transfer Transaction Execution with Multiple Nodes", async () => { + it("Transfer Transaction Execution with Multiple Nodes", async function () { // Create and sign transfer transaction const transferTransaction = new TransferTransaction() .addHbarTransfer(createdAccountId, new Hbar(-1)) @@ -74,7 +74,7 @@ describe("PrivateKey signTransaction", function () { expect(receipt.status).to.be.equal(Status.Success); }); - it("File Append Transaction Execution with Multiple Nodes", async () => { + it("File Append Transaction Execution with Multiple Nodes", async function () { const operatorKey = env.operatorKey.publicKey; // Create file diff --git a/test/integration/TransactionIntegrationTest.js b/test/integration/TransactionIntegrationTest.js index a92f05902..73a2b7c20 100644 --- a/test/integration/TransactionIntegrationTest.js +++ b/test/integration/TransactionIntegrationTest.js @@ -13,9 +13,12 @@ import { TransactionId, Timestamp, AccountUpdateTransaction, + KeyList, + Status, } from "../../src/exports.js"; import * as hex from "../../src/encoding/hex.js"; import IntegrationTestEnv from "./client/NodeIntegrationTestEnv.js"; +import { expect } from "chai"; describe("TransactionIntegration", function () { it("should be executable", async function () { @@ -765,4 +768,138 @@ describe("TransactionIntegration", function () { } }); }); + + describe("Transaction Signature Manipulation Flow", function () { + let env, user1Key, user2Key, createdAccountId, keyList; + + // Setting up the environment and creating a new account with a key list + before(async function () { + env = await IntegrationTestEnv.new(); + + user1Key = PrivateKey.generate(); + user2Key = PrivateKey.generate(); + keyList = new KeyList([user1Key.publicKey, user2Key.publicKey]); + + // Create account + const createAccountTransaction = new AccountCreateTransaction() + .setInitialBalance(new Hbar(2)) + .setKey(keyList); + + const createResponse = await createAccountTransaction.execute( + env.client, + ); + const createReceipt = await createResponse.getReceipt(env.client); + + createdAccountId = createReceipt.accountId; + + expect(createdAccountId).to.exist; + }); + + it("Transaction with Signature Removal and Re-addition", async function () { + // Step 1: Create and sign transfer transaction + const transferTransaction = new TransferTransaction() + .addHbarTransfer(createdAccountId, new Hbar(-1)) + .addHbarTransfer("0.0.3", new Hbar(1)) + .setNodeAccountIds([ + new AccountId(3), + new AccountId(4), + new AccountId(5), + ]) + .freezeWith(env.client); + + // Step 2: Serialize and sign the transaction + const transferTransactionBytes = transferTransaction.toBytes(); + const user1Signatures = + user1Key.signTransaction(transferTransaction); + const user2Signatures = + user2Key.signTransaction(transferTransaction); + + // Step 3: Deserialize the transaction and add signatures + const signedTransaction = Transaction.fromBytes( + transferTransactionBytes, + ); + signedTransaction.addSignature(user1Key.publicKey, user1Signatures); + signedTransaction.addSignature(user2Key.publicKey, user2Signatures); + + const getSignaturesNumberPerNode = () => + signedTransaction._signedTransactions.list[0].sigMap.sigPair + .length; + + // Test if the transaction for a node has 2 signatures + expect(getSignaturesNumberPerNode()).to.be.equal(2); + + // Step 4: Remove the signature for user1 from the transaction + signedTransaction.removeSignature(user1Key.publicKey); + + // Test if the transaction for a node has 1 signature after removal + expect(getSignaturesNumberPerNode()).to.be.equal(1); + + // Step 5: Re-add the removed signature + signedTransaction.addSignature(user1Key.publicKey, user1Signatures); + + // Test if the transaction for a node has 2 signatures after adding back the signature + expect(getSignaturesNumberPerNode()).to.be.equal(2); + + // Step 6: Execute the signed transaction + const result = await signedTransaction.execute(env.client); + const receipt = await result.getReceipt(env.client); + + // Step 7: Verify the transaction status + expect(receipt.status).to.be.equal(Status.Success); + }); + + it("Transaction with All Signature Removal and Re-addition", async function () { + // Step 1: Create and sign transfer transaction + const transferTransaction = new TransferTransaction() + .addHbarTransfer(createdAccountId, new Hbar(-1)) + .addHbarTransfer("0.0.3", new Hbar(1)) + .setNodeAccountIds([ + new AccountId(3), + new AccountId(4), + new AccountId(5), + ]) + .freezeWith(env.client); + + // Step 2: Serialize and sign the transaction + const transferTransactionBytes = transferTransaction.toBytes(); + const user1Signatures = + user1Key.signTransaction(transferTransaction); + const user2Signatures = + user2Key.signTransaction(transferTransaction); + + // Step 3: Deserialize the transaction and add signatures + const signedTransaction = Transaction.fromBytes( + transferTransactionBytes, + ); + signedTransaction.addSignature(user1Key.publicKey, user1Signatures); + signedTransaction.addSignature(user2Key.publicKey, user2Signatures); + + const getSignaturesNumberPerNode = () => + signedTransaction._signedTransactions.list[0].sigMap.sigPair + .length; + + // Test if the transaction for a node has 2 signatures + expect(getSignaturesNumberPerNode()).to.be.equal(2); + + // Step 4: Remove the signature for user1 from the transaction + signedTransaction.removeAllSignatures(); + + // Test if the transaction for a node has 0 signatures after removal + expect(getSignaturesNumberPerNode()).to.be.equal(0); + + // Step 5: Try to execute the transaction without any signatures and expect it to fail + try { + const result = await signedTransaction.execute(env.client); + await result.getReceipt(env.client); + + // If we get here, the transaction did not fail as expected + throw new Error( + "Transaction should have failed due to missing signatures", + ); + } catch (error) { + // Expect the error to be due to an invalid signature + expect(error.message).to.include("INVALID_SIGNATURE"); + } + }); + }); }); diff --git a/test/unit/PrivateKey.js b/test/unit/PrivateKey.js index 31f2a7859..b44949fad 100644 --- a/test/unit/PrivateKey.js +++ b/test/unit/PrivateKey.js @@ -7,7 +7,7 @@ import Transaction from "../../src/transaction/Transaction.js"; describe("PrivateKey signTransaction", function () { let privateKey, mockedTransaction, mockedSignature; - beforeEach(() => { + beforeEach(function () { privateKey = PrivateKey.generate(); mockedTransaction = sinon.createStubInstance(Transaction); @@ -32,7 +32,7 @@ describe("PrivateKey signTransaction", function () { const signatures = privateKey.signTransaction(mockedTransaction); // Validate that the signatures are correct - expect(signatures).to.deep.equal([mockedSignature]); + expect(signatures).to.deep.equal(mockedSignature); sinon.assert.calledWith( mockedTransaction.addSignature, @@ -59,7 +59,7 @@ describe("PrivateKey signTransaction", function () { const signatures = privateKey.signTransaction(mockedTransaction); // Validate that an empty Uint8Array was returned - expect(signatures).to.deep.equal([new Uint8Array()]); + expect(signatures).to.deep.equal(new Uint8Array()); // Ensure that the transaction's addSignature method was called with the empty signature sinon.assert.calledWith( diff --git a/test/unit/Transaction.js b/test/unit/Transaction.js index 4c698463b..67aa7fdee 100644 --- a/test/unit/Transaction.js +++ b/test/unit/Transaction.js @@ -315,380 +315,388 @@ describe("Transaction", function () { }); }); }); -}); - -describe("Transaction removeSignature/removeAllSignatures methods", () => { - let key1, key2, key3; - let transaction; - - beforeEach(async function () { - const nodeAccountId = new AccountId(3); - const account = AccountId.fromString("0.0.1004"); - const validStart = new Timestamp(1451, 590); - const transactionId = new TransactionId(account, validStart); - key1 = PrivateKey.generateED25519(); - key2 = PrivateKey.generateED25519(); - key3 = PrivateKey.generateED25519(); + describe("addSignature tests", function () { + let transaction, mockedPublicKey, mockedSignature; + + beforeEach(function () { + transaction = new Transaction(); + mockedPublicKey = sinon.createStubInstance(PublicKey); + mockedSignature = new Uint8Array([4, 5, 6]); + + // Mock methods of PublicKey + mockedPublicKey.toBytesRaw.returns(new Uint8Array([1, 2, 3])); + mockedPublicKey._toProtobufSignature = sinon + .stub() + .returns({ ed25519: mockedSignature }); + + // Mock the necessary internals of the Transaction object + transaction._signedTransactions = { + length: 1, + list: [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, + }, + ], + get(index) { + return this.list[index]; + }, + }; - transaction = new AccountCreateTransaction() - .setInitialBalance(new Hbar(2)) - .setTransactionId(transactionId) - .setNodeAccountIds([nodeAccountId]) - .freeze(); - }); + transaction._transactionIds = { setLocked: sinon.spy() }; + transaction._nodeAccountIds = { setLocked: sinon.spy() }; + transaction._signedTransactions.setLocked = sinon.spy(); - const signAndAddSignatures = (transaction, ...keys) => { - // Map through the keys to sign the transaction and add signatures - const signatures = keys.map((key) => { - const signature = key.signTransaction(transaction); - transaction.addSignature(key.publicKey, signature); - return signature; + // Assume that the transaction is not frozen initially + transaction.isFrozen = sinon.stub().returns(false); + transaction.freeze = sinon.spy(); }); - return signatures; - }; - - it("should remove a specific signature", function () { - // Sign the transaction with multiple keys - const [signature1] = signAndAddSignatures( - transaction, - key1, - key2, - key3, - ); - - //Check if the transaction internal tracking of signer public keys is correct - expect(transaction._signerPublicKeys.size).to.equal(3); - expect(transaction._publicKeys.length).to.equal(3); - expect(transaction._transactionSigners.length).to.equal(3); - - // Ensure all signatures are present before removal - const signaturesBefore = transaction.getSignatures(); - expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); - - // Remove one signature - transaction.removeSignature(key1.publicKey, signature1); + it("should add a single signature when one transaction is present", function () { + transaction.addSignature(mockedPublicKey, mockedSignature); - //Check if the transaction is frozen - expect(transaction.isFrozen()).to.be.true; + // Verify the signature was added correctly to sigMap.sigPair + expect( + transaction._signedTransactions.get(0).sigMap.sigPair, + ).to.deep.equal([{ ed25519: mockedSignature }]); - //Check if the transaction internal tracking of signer public keys is correct - expect(transaction._signerPublicKeys.size).to.equal(2); - expect(transaction._publicKeys.length).to.equal(2); - expect(transaction._transactionSigners.length).to.equal(2); + // Verify freeze was called since the transaction was not frozen + sinon.assert.calledOnce(transaction.freeze); + }); - // Ensure the specific signature has been removed - const signaturesAfter = transaction.getSignatures(); - expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); - }); + it("should throw an error when adding a single signature to multiple transactions", function () { + transaction._signedTransactions.length = 2; + transaction._signedTransactions.list = [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, + }, + { + bodyBytes: new Uint8Array([4, 5, 6]), + sigMap: { sigPair: [] }, + }, + ]; - it("should clear all signatures", function () { - // Sign the transaction with multiple keys - signAndAddSignatures(transaction, key1, key2, key3); + expect(() => { + transaction.addSignature(mockedPublicKey, mockedSignature); + }).to.throw( + "Signature array must match the number of transactions", + ); + }); - // Ensure all signatures are present before clearing - const signaturesBefore = transaction.getSignatures(); - expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); + it("should add multiple signatures corresponding to each transaction", function () { + const mockedSignatures = [ + new Uint8Array([10, 11, 12]), + new Uint8Array([13, 14, 15]), + ]; - // Clear all signatures - transaction.removeAllSignatures(); + transaction._signedTransactions.length = 2; + transaction._signedTransactions.list = [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, + }, + { + bodyBytes: new Uint8Array([4, 5, 6]), + sigMap: { sigPair: [] }, + }, + ]; + + // Update stub to return correct signatures for each call + mockedPublicKey._toProtobufSignature + .onCall(0) + .returns({ ed25519: mockedSignatures[0] }) + .onCall(1) + .returns({ ed25519: mockedSignatures[1] }); + + transaction.addSignature(mockedPublicKey, mockedSignatures); + + // Verify the signatures were added correctly + expect( + transaction._signedTransactions.get(0).sigMap.sigPair, + ).to.deep.equal([{ ed25519: mockedSignatures[0] }]); + expect( + transaction._signedTransactions.get(1).sigMap.sigPair, + ).to.deep.equal([{ ed25519: mockedSignatures[1] }]); + + // Validate the calls to _toProtobufSignature + sinon.assert.calledWith( + mockedPublicKey._toProtobufSignature, + mockedSignatures[0], + ); + sinon.assert.calledWith( + mockedPublicKey._toProtobufSignature, + mockedSignatures[1], + ); + }); - //Check if the transaction is frozen - expect(transaction.isFrozen()).to.be.true; + it("should throw an error when signature array length doesn't match the number of transactions", function () { + const mismatchedSignatures = [new Uint8Array([10, 11, 12])]; + transaction._signedTransactions.length = 2; + transaction._signedTransactions.list = [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, + }, + { + bodyBytes: new Uint8Array([4, 5, 6]), + sigMap: { sigPair: [] }, + }, + ]; - //Check if the transaction internal tracking of signer public keys is cleared - expect(transaction._signerPublicKeys.size).to.equal(0); - expect(transaction._publicKeys.length).to.equal(0); - expect(transaction._transactionSigners.length).to.equal(0); + expect(() => { + transaction.addSignature(mockedPublicKey, mismatchedSignatures); + }).to.throw( + "Signature array must match the number of transactions", + ); + }); - // Ensure all signatures have been cleared - const signaturesAfter = transaction.getSignatures(); - expect(signaturesAfter.get(new AccountId(3)).size).to.equal(0); - }); + it("should freeze the transaction if it is not frozen", function () { + transaction.isFrozen.returns(false); - it("should not remove a non-existing signature", function () { - // Sign the transaction with multiple keys - signAndAddSignatures(transaction, key1, key2); + transaction._signedTransactions.length = 1; + transaction._signedTransactions.list = [ + { + bodyBytes: new Uint8Array([1, 2, 3]), + sigMap: { sigPair: [] }, + }, + ]; - // Attempt to remove a non-existing signature - expect(() => { - transaction.removeSignature(key3.publicKey); - }).to.throw("The public key has not signed this transaction"); + transaction.addSignature(mockedPublicKey, mockedSignature); - // Ensure signatures are not affected - const signaturesAfter = transaction.getSignatures(); - expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); + sinon.assert.calledOnce(transaction.freeze); + }); }); - it("should clear and re-sign after all signatures are cleared", function () { - // Sign the transaction with multiple keys - signAndAddSignatures(transaction, key1, key2); - - // Ensure all signatures are present before clearing - const signaturesBefore = transaction.getSignatures(); - expect(signaturesBefore.get(new AccountId(3)).size).to.equal(2); - - // Clear all signatures - transaction.removeAllSignatures(); - - // Ensure all signatures have been cleared - const signaturesAfterClear = transaction.getSignatures(); - expect(signaturesAfterClear.get(new AccountId(3)).size).to.equal(0); + describe("Transaction removeSignature/removeAllSignatures methods", function () { + let key1, key2, key3; + let transaction; + + beforeEach(async function () { + const nodeAccountId = new AccountId(3); + const account = AccountId.fromString("0.0.1004"); + const validStart = new Timestamp(1451, 590); + const transactionId = new TransactionId(account, validStart); + + key1 = PrivateKey.generateED25519(); + key2 = PrivateKey.generateED25519(); + key3 = PrivateKey.generateED25519(); + + transaction = new AccountCreateTransaction() + .setInitialBalance(new Hbar(2)) + .setTransactionId(transactionId) + .setNodeAccountIds([nodeAccountId]) + .freeze(); + }); - // Re-sign the transaction with a different key - const signature3 = key3.signTransaction(transaction); - transaction.addSignature(key3.publicKey, signature3); + const signAndAddSignatures = (transaction, ...keys) => { + // Map through the keys to sign the transaction and add signatures + const signatures = keys.map((key) => { + const signature = key.signTransaction(transaction); + transaction.addSignature(key.publicKey, signature); + return signature; + }); - // Ensure only one signature exists after re-signing - const signaturesAfterResign = transaction.getSignatures(); - expect(signaturesAfterResign.get(new AccountId(3)).size).to.equal(1); - }); + return signatures; + }; - it("should return the removed signature in Uint8Array format when removing a specific signature", function () { - // Sign the transaction with multiple keys - const [signature1] = signAndAddSignatures( - transaction, - key1, - key2, - key3, - ); + it("should remove a specific signature", function () { + // Sign the transaction with multiple keys + const [signature1] = signAndAddSignatures( + transaction, + key1, + key2, + key3, + ); - // Remove one signature and capture the returned value - const removedSignatures = transaction.removeSignature( - key1.publicKey, - signature1, - ); + //Check if the transaction internal tracking of signer public keys is correct + expect(transaction._signerPublicKeys.size).to.equal(3); + expect(transaction._publicKeys.length).to.equal(3); + expect(transaction._transactionSigners.length).to.equal(3); - // Check the format of the returned value - expect(removedSignatures).to.be.an("array"); - expect(removedSignatures.length).to.equal(1); - expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); + // Ensure all signatures are present before removal + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); - // Ensure the returned signature is the one that was removed - expect(removedSignatures[0]).to.deep.equal(signature1); - }); + // Remove one signature + transaction.removeSignature(key1.publicKey, signature1); - it("should return the signatures for a public key in Uint8Array format when only the public key is provided", function () { - // Sign the transaction with multiple keys - const [signature1] = signAndAddSignatures(transaction, key1); + //Check if the transaction is frozen + expect(transaction.isFrozen()).to.be.true; - // Remove all signatures for key1 and capture the returned value - const removedSignatures = transaction.removeSignature(key1.publicKey); + //Check if the transaction internal tracking of signer public keys is correct + expect(transaction._signerPublicKeys.size).to.equal(2); + expect(transaction._publicKeys.length).to.equal(2); + expect(transaction._transactionSigners.length).to.equal(2); - // Check the format of the returned value - expect(removedSignatures).to.be.an("array"); - expect(removedSignatures.length).to.equal(1); - expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); + // Ensure the specific signature has been removed + const signaturesAfter = transaction.getSignatures(); + expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); + }); - // Ensure the returned signatures are the ones that were removed - expect(removedSignatures).to.include.members([signature1]); - }); + it("should clear all signatures", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2, key3); - it("should return all removed signatures in the expected object format when clearing all signatures", function () { - // Sign the transaction with multiple keys - signAndAddSignatures(transaction, key1, key2, key3); + // Ensure all signatures are present before clearing + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(3); - // Clear all signatures and capture the returned value - const removedSignatures = transaction.removeAllSignatures(); + // Clear all signatures + transaction.removeAllSignatures(); - // Check the format of the returned value - expect(removedSignatures).to.be.an("object"); + //Check if the transaction is frozen + expect(transaction.isFrozen()).to.be.true; - // Ensure the object has the correct structure - expect(removedSignatures).to.have.property( - key1.publicKey.toStringRaw(), - ); - expect(removedSignatures[key1.publicKey.toStringRaw()]).to.be.an( - "array", - ); - expect(removedSignatures).to.have.property( - key2.publicKey.toStringRaw(), - ); - expect(removedSignatures[key2.publicKey.toStringRaw()]).to.be.an( - "array", - ); - expect(removedSignatures).to.have.property( - key3.publicKey.toStringRaw(), - ); - expect(removedSignatures[key3.publicKey.toStringRaw()]).to.be.an( - "array", - ); + //Check if the transaction internal tracking of signer public keys is cleared + expect(transaction._signerPublicKeys.size).to.equal(0); + expect(transaction._publicKeys.length).to.equal(0); + expect(transaction._transactionSigners.length).to.equal(0); - // Ensure the removed signatures are in the expected format - const signaturesArray1 = - removedSignatures[key1.publicKey.toStringRaw()]; - const signaturesArray2 = - removedSignatures[key2.publicKey.toStringRaw()]; - const signaturesArray3 = - removedSignatures[key3.publicKey.toStringRaw()]; - - [signaturesArray1, signaturesArray2, signaturesArray3].forEach( - (signaturesArray) => { - signaturesArray.forEach((sig) => { - expect(sig).to.be.instanceOf(Uint8Array); - }); - }, - ); - }); + // Ensure all signatures have been cleared + const signaturesAfter = transaction.getSignatures(); + expect(signaturesAfter.get(new AccountId(3)).size).to.equal(0); + }); - it("should return an empty object when no signatures are present", function () { - expect(transaction._signerPublicKeys.size).to.equal(0); + it("should not remove a non-existing signature", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2); - // Clear all signatures and capture the returned value - const removedSignatures = transaction.removeAllSignatures(); + // Attempt to remove a non-existing signature + expect(() => { + transaction.removeSignature(key3.publicKey); + }).to.throw("The public key has not signed this transaction"); - // Check the format of the returned value - expect(removedSignatures).to.be.an("object"); - expect(Object.keys(removedSignatures)).to.have.lengthOf(0); - }); -}); + // Ensure signatures are not affected + const signaturesAfter = transaction.getSignatures(); + expect(signaturesAfter.get(new AccountId(3)).size).to.equal(2); + }); -describe("addSignature tests", () => { - let transaction, mockedPublicKey, mockedSignature; + it("should clear and re-sign after all signatures are cleared", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2); - beforeEach(() => { - transaction = new Transaction(); - mockedPublicKey = sinon.createStubInstance(PublicKey); - mockedSignature = new Uint8Array([4, 5, 6]); + // Ensure all signatures are present before clearing + const signaturesBefore = transaction.getSignatures(); + expect(signaturesBefore.get(new AccountId(3)).size).to.equal(2); - // Mock methods of PublicKey - mockedPublicKey.toBytesRaw.returns(new Uint8Array([1, 2, 3])); - mockedPublicKey._toProtobufSignature = sinon - .stub() - .returns({ ed25519: mockedSignature }); + // Clear all signatures + transaction.removeAllSignatures(); - // Mock the necessary internals of the Transaction object - transaction._signedTransactions = { - length: 1, - list: [ - { - bodyBytes: new Uint8Array([1, 2, 3]), - sigMap: { sigPair: [] }, - }, - ], - get(index) { - return this.list[index]; - }, - }; + // Ensure all signatures have been cleared + const signaturesAfterClear = transaction.getSignatures(); + expect(signaturesAfterClear.get(new AccountId(3)).size).to.equal(0); - transaction._transactionIds = { setLocked: sinon.spy() }; - transaction._nodeAccountIds = { setLocked: sinon.spy() }; - transaction._signedTransactions.setLocked = sinon.spy(); + // Re-sign the transaction with a different key + const signature3 = key3.signTransaction(transaction); + transaction.addSignature(key3.publicKey, signature3); - // Assume that the transaction is not frozen initially - transaction.isFrozen = sinon.stub().returns(false); - transaction.freeze = sinon.spy(); - }); + // Ensure only one signature exists after re-signing + const signaturesAfterResign = transaction.getSignatures(); + expect(signaturesAfterResign.get(new AccountId(3)).size).to.equal( + 1, + ); + }); - it("should add a single signature when one transaction is present", () => { - transaction.addSignature(mockedPublicKey, mockedSignature); + it("should return the removed signature in Uint8Array format when removing a specific signature", function () { + // Sign the transaction with multiple keys + const [signature1] = signAndAddSignatures( + transaction, + key1, + key2, + key3, + ); - // Verify the signature was added correctly to sigMap.sigPair - expect( - transaction._signedTransactions.get(0).sigMap.sigPair, - ).to.deep.equal([{ ed25519: mockedSignature }]); + // Remove one signature and capture the returned value + const removedSignatures = transaction.removeSignature( + key1.publicKey, + signature1, + ); - // Verify freeze was called since the transaction was not frozen - sinon.assert.calledOnce(transaction.freeze); - }); + // Check the format of the returned value + expect(removedSignatures).to.be.an("array"); + expect(removedSignatures.length).to.equal(1); + expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); - it("should throw an error when adding a single signature to multiple transactions", () => { - transaction._signedTransactions.length = 2; - transaction._signedTransactions.list = [ - { - bodyBytes: new Uint8Array([1, 2, 3]), - sigMap: { sigPair: [] }, - }, - { - bodyBytes: new Uint8Array([4, 5, 6]), - sigMap: { sigPair: [] }, - }, - ]; + // Ensure the returned signature is the one that was removed + expect(removedSignatures[0]).to.deep.equal(signature1); + }); - expect(() => { - transaction.addSignature(mockedPublicKey, mockedSignature); - }).to.throw("Signature array must match the number of transactions"); - }); + it("should return the signatures for a public key in Uint8Array format when only the public key is provided", function () { + // Sign the transaction with multiple keys + const [signature1] = signAndAddSignatures(transaction, key1); - it("should add multiple signatures corresponding to each transaction", () => { - const mockedSignatures = [ - new Uint8Array([10, 11, 12]), - new Uint8Array([13, 14, 15]), - ]; - - transaction._signedTransactions.length = 2; - transaction._signedTransactions.list = [ - { - bodyBytes: new Uint8Array([1, 2, 3]), - sigMap: { sigPair: [] }, - }, - { - bodyBytes: new Uint8Array([4, 5, 6]), - sigMap: { sigPair: [] }, - }, - ]; + // Remove all signatures for key1 and capture the returned value + const removedSignatures = transaction.removeSignature( + key1.publicKey, + ); - // Update stub to return correct signatures for each call - mockedPublicKey._toProtobufSignature - .onCall(0) - .returns({ ed25519: mockedSignatures[0] }) - .onCall(1) - .returns({ ed25519: mockedSignatures[1] }); + // Check the format of the returned value + expect(removedSignatures).to.be.an("array"); + expect(removedSignatures.length).to.equal(1); + expect(removedSignatures[0]).to.be.instanceOf(Uint8Array); - transaction.addSignature(mockedPublicKey, mockedSignatures); + // Ensure the returned signatures are the ones that were removed + expect(removedSignatures).to.include.members([signature1]); + }); - // Verify the signatures were added correctly - expect( - transaction._signedTransactions.get(0).sigMap.sigPair, - ).to.deep.equal([{ ed25519: mockedSignatures[0] }]); - expect( - transaction._signedTransactions.get(1).sigMap.sigPair, - ).to.deep.equal([{ ed25519: mockedSignatures[1] }]); + it("should return all removed signatures in the expected object format when clearing all signatures", function () { + // Sign the transaction with multiple keys + signAndAddSignatures(transaction, key1, key2, key3); - // Validate the calls to _toProtobufSignature - sinon.assert.calledWith( - mockedPublicKey._toProtobufSignature, - mockedSignatures[0], - ); - sinon.assert.calledWith( - mockedPublicKey._toProtobufSignature, - mockedSignatures[1], - ); - }); + // Clear all signatures and capture the returned value + const removedSignatures = transaction.removeAllSignatures(); - it("should throw an error when signature array length doesn't match the number of transactions", () => { - const mismatchedSignatures = [new Uint8Array([10, 11, 12])]; - transaction._signedTransactions.length = 2; - transaction._signedTransactions.list = [ - { - bodyBytes: new Uint8Array([1, 2, 3]), - sigMap: { sigPair: [] }, - }, - { - bodyBytes: new Uint8Array([4, 5, 6]), - sigMap: { sigPair: [] }, - }, - ]; + // Check the format of the returned value + expect(removedSignatures).to.be.an("object"); - expect(() => { - transaction.addSignature(mockedPublicKey, mismatchedSignatures); - }).to.throw("Signature array must match the number of transactions"); - }); + // Ensure the object has the correct structure + expect(removedSignatures).to.have.property( + key1.publicKey.toStringRaw(), + ); + expect(removedSignatures[key1.publicKey.toStringRaw()]).to.be.an( + "array", + ); + expect(removedSignatures).to.have.property( + key2.publicKey.toStringRaw(), + ); + expect(removedSignatures[key2.publicKey.toStringRaw()]).to.be.an( + "array", + ); + expect(removedSignatures).to.have.property( + key3.publicKey.toStringRaw(), + ); + expect(removedSignatures[key3.publicKey.toStringRaw()]).to.be.an( + "array", + ); - it("should freeze the transaction if it is not frozen", () => { - transaction.isFrozen.returns(false); + // Ensure the removed signatures are in the expected format + const signaturesArray1 = + removedSignatures[key1.publicKey.toStringRaw()]; + const signaturesArray2 = + removedSignatures[key2.publicKey.toStringRaw()]; + const signaturesArray3 = + removedSignatures[key3.publicKey.toStringRaw()]; + + [signaturesArray1, signaturesArray2, signaturesArray3].forEach( + (signaturesArray) => { + signaturesArray.forEach((sig) => { + expect(sig).to.be.instanceOf(Uint8Array); + }); + }, + ); + }); - transaction._signedTransactions.length = 1; - transaction._signedTransactions.list = [ - { - bodyBytes: new Uint8Array([1, 2, 3]), - sigMap: { sigPair: [] }, - }, - ]; + it("should return an empty object when no signatures are present", function () { + expect(transaction._signerPublicKeys.size).to.equal(0); - transaction.addSignature(mockedPublicKey, mockedSignature); + // Clear all signatures and capture the returned value + const removedSignatures = transaction.removeAllSignatures(); - sinon.assert.calledOnce(transaction.freeze); + // Check the format of the returned value + expect(removedSignatures).to.be.an("object"); + expect(Object.keys(removedSignatures)).to.have.lengthOf(0); + }); }); }); From 52487c6f222d42cbe7ad27863758b575f8d00b4d Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Wed, 18 Sep 2024 16:38:05 +0300 Subject: [PATCH 12/13] refactor: Removed duplicate integration tests and added description to examples Signed-off-by: ivaylogarnev-limechain --- examples/multi-node-multi-signature-remove.js | 4 +++ .../multi-node-multi-signature-removeAll.js | 4 +++ examples/multi-node-multi-signature.js | 3 ++ test/integration/PrivateKey.js | 32 ------------------- .../integration/TransactionIntegrationTest.js | 4 ++- 5 files changed, 14 insertions(+), 33 deletions(-) diff --git a/examples/multi-node-multi-signature-remove.js b/examples/multi-node-multi-signature-remove.js index b87272dce..907bf9dec 100644 --- a/examples/multi-node-multi-signature-remove.js +++ b/examples/multi-node-multi-signature-remove.js @@ -16,6 +16,10 @@ dotenv.config(); let aliceKey; let bobKey; +/** + * @description Create a transaction with multiple nodes and multiple signatures + * and remove one of the signatures from the transaction then add it back + */ async function main() { /** * diff --git a/examples/multi-node-multi-signature-removeAll.js b/examples/multi-node-multi-signature-removeAll.js index 1241d056f..f7eb7ade3 100644 --- a/examples/multi-node-multi-signature-removeAll.js +++ b/examples/multi-node-multi-signature-removeAll.js @@ -17,6 +17,10 @@ dotenv.config(); let aliceKey; let bobKey; +/** + * @description Create a transaction with multiple nodes and multiple signatures + * and remove all of the signatures from the transaction + */ async function main() { /** * diff --git a/examples/multi-node-multi-signature.js b/examples/multi-node-multi-signature.js index 14fdca622..2a340b941 100644 --- a/examples/multi-node-multi-signature.js +++ b/examples/multi-node-multi-signature.js @@ -16,6 +16,9 @@ dotenv.config(); let aliceKey; let bobKey; +/** + * @description Create a transaction with multiple nodes and multiple signatures + */ async function main() { /** * diff --git a/test/integration/PrivateKey.js b/test/integration/PrivateKey.js index 262a9486e..9a2d98c55 100644 --- a/test/integration/PrivateKey.js +++ b/test/integration/PrivateKey.js @@ -4,7 +4,6 @@ import { Hbar, AccountId, KeyList, - TransferTransaction, Transaction, Status, FileAppendTransaction, @@ -43,37 +42,6 @@ describe("PrivateKey signTransaction", function () { expect(createdAccountId).to.exist; }); - it("Transfer Transaction Execution with Multiple Nodes", async function () { - // Create and sign transfer transaction - const transferTransaction = new TransferTransaction() - .addHbarTransfer(createdAccountId, new Hbar(-1)) - .addHbarTransfer("0.0.3", new Hbar(1)) - .setNodeAccountIds([ - new AccountId(3), - new AccountId(4), - new AccountId(5), - ]) - .freezeWith(env.client); - - // Serialize and sign the transaction - const transferTransactionBytes = transferTransaction.toBytes(); - const user1Signatures = user1Key.signTransaction(transferTransaction); - const user2Signatures = user2Key.signTransaction(transferTransaction); - - // Deserialize the transaction and add signatures - const signedTransaction = Transaction.fromBytes( - transferTransactionBytes, - ); - signedTransaction.addSignature(user1Key.publicKey, user1Signatures); - signedTransaction.addSignature(user2Key.publicKey, user2Signatures); - - // Execute the signed transaction - const result = await signedTransaction.execute(env.client); - const receipt = await result.getReceipt(env.client); - - expect(receipt.status).to.be.equal(Status.Success); - }); - it("File Append Transaction Execution with Multiple Nodes", async function () { const operatorKey = env.operatorKey.publicKey; diff --git a/test/integration/TransactionIntegrationTest.js b/test/integration/TransactionIntegrationTest.js index 73a2b7c20..50c6b433c 100644 --- a/test/integration/TransactionIntegrationTest.js +++ b/test/integration/TransactionIntegrationTest.js @@ -795,6 +795,7 @@ describe("TransactionIntegration", function () { expect(createdAccountId).to.exist; }); + /** @description: example multi-node-multi-signature-remove.js */ it("Transaction with Signature Removal and Re-addition", async function () { // Step 1: Create and sign transfer transaction const transferTransaction = new TransferTransaction() @@ -848,7 +849,8 @@ describe("TransactionIntegration", function () { expect(receipt.status).to.be.equal(Status.Success); }); - it("Transaction with All Signature Removal and Re-addition", async function () { + /** @description: example multi-node-multi-signature-removeAll.js */ + it("Transaction with All Signature Removal", async function () { // Step 1: Create and sign transfer transaction const transferTransaction = new TransferTransaction() .addHbarTransfer(createdAccountId, new Hbar(-1)) From 01d9e0abd83a0731d23efe6028322614adf4dd11 Mon Sep 17 00:00:00 2001 From: ivaylogarnev-limechain Date: Fri, 20 Sep 2024 13:55:43 +0300 Subject: [PATCH 13/13] refactor: Examples pre-release warnings fixes Signed-off-by: ivaylogarnev-limechain --- examples/multi-node-multi-signature-remove.js | 24 +++++++----- .../multi-node-multi-signature-removeAll.js | 39 ++++++++++++------- examples/multi-node-multi-signature.js | 20 ++++++---- examples/multi-sig-offline.js | 4 +- 4 files changed, 56 insertions(+), 31 deletions(-) diff --git a/examples/multi-node-multi-signature-remove.js b/examples/multi-node-multi-signature-remove.js index 907bf9dec..12f1db1ae 100644 --- a/examples/multi-node-multi-signature-remove.js +++ b/examples/multi-node-multi-signature-remove.js @@ -25,7 +25,7 @@ async function main() { * * Step 1: Create Client * - **/ + */ if ( process.env.OPERATOR_ID == null || process.env.OPERATOR_KEY == null || @@ -45,7 +45,7 @@ async function main() { * * Step 2: Create keys for two users * - **/ + */ aliceKey = PrivateKey.generate(); bobKey = PrivateKey.generate(); @@ -55,7 +55,7 @@ async function main() { * * Step 3: Create an account with the keyList * - **/ + */ const createAccountTransaction = new AccountCreateTransaction() .setInitialBalance(new Hbar(2)) .setKey(keyList); @@ -67,7 +67,7 @@ async function main() { * * Step 4: Create a transfer transaction with multiple nodes * - **/ + */ const transferTransaction = new TransferTransaction() .addHbarTransfer(createReceipt.accountId, new Hbar(-1)) .addHbarTransfer("0.0.3", new Hbar(1)) @@ -84,7 +84,7 @@ async function main() { * Step 5: Serialize the transaction * & Collect multiple signatures (Uint8Array[]) from one key * - **/ + */ const transferTransactionBytes = transferTransaction.toBytes(); @@ -96,7 +96,7 @@ async function main() { * Step 6: Deserialize the transaction * & Add the previously collected signatures * - **/ + */ const signedTransaction = Transaction.fromBytes(transferTransactionBytes); signedTransaction.addSignature(aliceKey.publicKey, aliceSignatures); @@ -130,7 +130,7 @@ async function main() { * * Step 7: Remove the signatures for Alice from the transaction * - **/ + */ const removedAliceSignatures = signedTransaction.removeSignature( aliceKey.publicKey, @@ -157,14 +157,14 @@ async function main() { * * Step 8: Add the removed signature back to the transaction * - **/ + */ signedTransaction.addSignature(aliceKey.publicKey, removedAliceSignatures); /** * * Step 9: Execute and take the receipt * - **/ + */ const result = await signedTransaction.execute(client); const receipt = await result.getReceipt(client); @@ -176,7 +176,13 @@ async function main() { void main(); +/** + * Extracts all signatures from a signed transaction. + * @param {Transaction} signedTransaction - The signed transaction object containing the list of signed transactions. + * @returns {string[]} An array of signatures in DER format. + */ const getAllSignaturesFromTransaction = (signedTransaction) => { + /** @type {string[]} */ const signatures = []; signedTransaction._signedTransactions.list.forEach((transaction) => { diff --git a/examples/multi-node-multi-signature-removeAll.js b/examples/multi-node-multi-signature-removeAll.js index f7eb7ade3..2e8bc6079 100644 --- a/examples/multi-node-multi-signature-removeAll.js +++ b/examples/multi-node-multi-signature-removeAll.js @@ -26,7 +26,7 @@ async function main() { * * Step 1: Create Client * - **/ + */ if ( process.env.OPERATOR_ID == null || process.env.OPERATOR_KEY == null || @@ -46,7 +46,7 @@ async function main() { * * Step 2: Create keys for two users * - **/ + */ aliceKey = PrivateKey.generate(); bobKey = PrivateKey.generate(); @@ -59,7 +59,7 @@ async function main() { * * Step 3: Create an account with the keyList * - **/ + */ const createAccountTransaction = new AccountCreateTransaction() .setInitialBalance(new Hbar(2)) .setKey(keyList); @@ -71,7 +71,7 @@ async function main() { * * Step 4: Create a transfer transaction with multiple nodes * - **/ + */ const transferTransaction = new TransferTransaction() .addHbarTransfer(createReceipt.accountId, new Hbar(-1)) .addHbarTransfer("0.0.3", new Hbar(1)) @@ -88,7 +88,7 @@ async function main() { * Step 5: Serialize the transaction * & Collect multiple signatures (Uint8Array[]) from one key * - **/ + */ const transferTransactionBytes = transferTransaction.toBytes(); @@ -100,7 +100,7 @@ async function main() { * Step 6: Deserialize the transaction * & Add the previously collected signatures * - **/ + */ const signedTransaction = Transaction.fromBytes(transferTransactionBytes); signedTransaction.addSignature(aliceKey.publicKey, aliceSignatures); @@ -134,7 +134,7 @@ async function main() { * * Step 7: Remove all signatures from the transaction and add them back * - **/ + */ const allSignaturesRemoved = signedTransaction.removeAllSignatures(); @@ -146,6 +146,17 @@ async function main() { signaturesInTheTransactionAfter, ); + /** + * Print the signatures in DER format + * @param {Uint8Array[]} signatures - The signatures to print. + * @returns {void} An array of signatures in DER format. + */ + const printSignatures = (signatures) => { + return signatures.forEach((sig) => { + console.log(PrivateKey.fromBytes(sig).toStringDer()); + }); + }; + for (const [publicKey, signatures] of Object.entries( allSignaturesRemoved, )) { @@ -153,11 +164,7 @@ async function main() { console.log(`\nRemoved signatures for ${publicKey}:`); if (Array.isArray(signatures)) { - console.log( - signatures.map((sig) => - PrivateKey.fromBytes(sig).toStringDer(), - ), - ); + printSignatures(signatures); } // Add the removed signatures back @@ -171,7 +178,7 @@ async function main() { * * Step 8: Execute and take the receipt * - **/ + */ const result = await signedTransaction.execute(client); const receipt = await result.getReceipt(client); @@ -183,7 +190,13 @@ async function main() { void main(); +/** + * Extracts all signatures from a signed transaction. + * @param {Transaction} signedTransaction - The signed transaction object containing the list of signed transactions. + * @returns {string[]} An array of signatures in DER format. + */ const getAllSignaturesFromTransaction = (signedTransaction) => { + /** @type {string[]} */ const signatures = []; signedTransaction._signedTransactions.list.forEach((transaction) => { diff --git a/examples/multi-node-multi-signature.js b/examples/multi-node-multi-signature.js index 2a340b941..558ba7396 100644 --- a/examples/multi-node-multi-signature.js +++ b/examples/multi-node-multi-signature.js @@ -24,7 +24,7 @@ async function main() { * * Step 1: Create Client * - **/ + */ if ( process.env.OPERATOR_ID == null || process.env.OPERATOR_KEY == null || @@ -44,7 +44,7 @@ async function main() { * * Step 2: Create keys for two users * - **/ + */ aliceKey = PrivateKey.generate(); bobKey = PrivateKey.generate(); @@ -54,7 +54,7 @@ async function main() { * * Step 3: Create an account with the keyList * - **/ + */ const createAccountTransaction = new AccountCreateTransaction() .setInitialBalance(new Hbar(2)) .setKey(keyList); @@ -66,7 +66,7 @@ async function main() { * * Step 4: Create a transfer transaction with multiple nodes * - **/ + */ const transferTransaction = new TransferTransaction() .addHbarTransfer(createReceipt.accountId, new Hbar(-1)) .addHbarTransfer("0.0.3", new Hbar(1)) @@ -83,7 +83,7 @@ async function main() { * Step 5: Serialize the transaction * & Collect multiple signatures (Uint8Array[]) from one key * - **/ + */ const transferTransactionBytes = transferTransaction.toBytes(); @@ -95,7 +95,7 @@ async function main() { * Step 6: Deserialize the transaction * & Add the previously collected signatures * - **/ + */ const signedTransaction = Transaction.fromBytes(transferTransactionBytes); signedTransaction.addSignature(aliceKey.publicKey, aliceSignatures); @@ -129,7 +129,7 @@ async function main() { * * Step 7: Execute and take the receipt * - **/ + */ const result = await signedTransaction.execute(client); const receipt = await result.getReceipt(client); @@ -141,7 +141,13 @@ async function main() { void main(); +/** + * Extracts all signatures from a signed transaction. + * @param {Transaction} signedTransaction - The signed transaction object containing the list of signed transactions. + * @returns {string[]} An array of signatures in DER format. + */ const getAllSignaturesFromTransaction = (signedTransaction) => { + /** @type {string[]} */ const signatures = []; signedTransaction._signedTransactions.list.forEach((transaction) => { diff --git a/examples/multi-sig-offline.js b/examples/multi-sig-offline.js index 50eb16274..a574d07d1 100644 --- a/examples/multi-sig-offline.js +++ b/examples/multi-sig-offline.js @@ -89,7 +89,7 @@ async function main() { /** * @param {Uint8Array} transactionBytes - * @returns {Uint8Array} + * @returns {Uint8Array | Uint8Array[]} */ function user1Signs(transactionBytes) { const transaction = Transaction.fromBytes(transactionBytes); @@ -98,7 +98,7 @@ function user1Signs(transactionBytes) { /** * @param {Uint8Array} transactionBytes - * @returns {Uint8Array} + * @returns {Uint8Array | Uint8Array[]} */ function user2Signs(transactionBytes) { const transaction = Transaction.fromBytes(transactionBytes);