From 674f8240d447b048fbf75986a38f57852194c433 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Mon, 26 Aug 2024 01:43:43 +0300 Subject: [PATCH 01/33] feat: add Airdrop Transaction Signed-off-by: Ivaylo Nikolov --- src/exports.js | 1 + src/token/AccountAmount.js | 106 ++++++++++++++++++ src/token/AirdropTokenTransaction.js | 121 ++++++++++++++++++++ src/token/TokenTransferList.js | 159 +++++++++++++++++++++++++++ 4 files changed, 387 insertions(+) create mode 100644 src/token/AccountAmount.js create mode 100644 src/token/AirdropTokenTransaction.js create mode 100644 src/token/TokenTransferList.js diff --git a/src/exports.js b/src/exports.js index c82db744c..27beab2bb 100644 --- a/src/exports.js +++ b/src/exports.js @@ -33,6 +33,7 @@ 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 AirdropTokenTransaction } from "./token/AirdropTokenTransaction.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/AccountAmount.js b/src/token/AccountAmount.js new file mode 100644 index 000000000..61b633a97 --- /dev/null +++ b/src/token/AccountAmount.js @@ -0,0 +1,106 @@ +/** + * @namespace proto + * @typedef {import("@hashgraph/proto").proto.AccountAmount} Hashgraph.proto.AccountAmount + */ + +import Long from "long"; +import { AccountId } from "../exports.js"; + +export default class AccountAmount { + /** + * @param {object} props + * @param {AccountId} [props.accountId] + * @param {Long} [props.amount] + * @param {boolean} [props.isApproval] + */ + constructor(props = {}) { + this._accountId = null; + this._amount = null; + this._isApproval = null; + + if (props.accountId != null) { + this.setAccountId(props.accountId); + } + if (props.amount != null) { + this.setAmount(props.amount); + } + if (props.isApproval != null) { + this.setIsApproval(props.isApproval); + } + } + + /** + * @returns {?AccountId} + */ + get accountId() { + return this._accountId; + } + + /** + * @param {AccountId} accountId + * @returns {this} + */ + setAccountId(accountId) { + this._accountId = accountId; + return this; + } + + /** + * @returns {Long?} + */ + get amount() { + return this._amount; + } + + /** + * @param {Long} amount + * @returns {this} + */ + setAmount(amount) { + this._amount = amount; + return this; + } + + /** + * @returns {boolean?} + */ + get isApproval() { + return this._isApproval; + } + + /** + * @param {boolean} isApproval + * @returns {this} + */ + setIsApproval(isApproval) { + this._isApproval = isApproval; + return this; + } + + /** + * @returns {Hashgraph.proto.AccountAmount} + */ + _toProtobuf() { + return { + accountID: + this._accountId != null ? this._accountId._toProtobuf() : null, + amount: this._amount != null ? this._amount : Long.ZERO, + isApproval: this._isApproval != null ? this._isApproval : false, + }; + } + + /** + * @param {Hashgraph.proto.AccountAmount} pb + * @returns {AccountAmount} + */ + static _fromProtobuf(pb) { + return new AccountAmount({ + accountId: + pb.accountID != null + ? AccountId._fromProtobuf(pb.accountID) + : undefined, + amount: pb.amount, + isApproval: pb.isApproval, + }); + } +} diff --git a/src/token/AirdropTokenTransaction.js b/src/token/AirdropTokenTransaction.js new file mode 100644 index 000000000..b764cb6f3 --- /dev/null +++ b/src/token/AirdropTokenTransaction.js @@ -0,0 +1,121 @@ +import Transaction, { + TRANSACTION_REGISTRY, +} from "../transaction/Transaction.js"; +import TokenTransferList from "./TokenTransferList.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.ITransactionResponse} HashgraphProto.proto.ITransactionResponse + * @typedef {import("@hashgraph/proto").proto.TransactionBody} HashgraphProto.proto.TransactionBody + * @typedef {import("@hashgraph/proto").proto.ITokenRejectTransactionBody} HashgraphProto.proto.ITokenRejectTransactionBody + * @typedef {import("@hashgraph/proto").proto.TokenReference} HashgraphProto.proto.TokenReference + */ + +/** + * @typedef {import("../transaction/TransactionId.js").default} TransactionId + * @typedef {import("../account/AccountId.js").default} AccountId + */ +export default class AirdropTokenTransaction extends Transaction { + /** + * @param {object} props + * @param {TokenTransferList[]} [props.tokenTransfer] + */ + constructor(props = {}) { + super(); + /** + * @private + * @type {TokenTransferList[]} + */ + this._tokenTransfers = []; + + if (props.tokenTransfer != null) { + this.setTokenTransfers(props.tokenTransfer); + } + } + + /** + * @returns {TokenTransferList[]} + */ + get tokenTransfers() { + return this._tokenTransfers; + } + + /** + * @param {TokenTransferList[]} tokenTransfers + * @returns {this} + */ + setTokenTransfers(tokenTransfers) { + console.log(tokenTransfers.length); + this._tokenTransfers = tokenTransfers; + return this; + } + + /** + * @returns {HashgraphProto.proto.ITokenAirdropTransactionBody} + */ + _makeTransactionData() { + return { + tokenTransfers: this._tokenTransfers.map((tokenTransfer) => + tokenTransfer._toProtobuf(), + ), + }; + } + + /** + * @internal + * @param {HashgraphProto.proto.ITransaction[]} transactions + * @param {HashgraphProto.proto.ISignedTransaction[]} signedTransactions + * @param {TransactionId[]} transactionIds + * @param {AccountId[]} nodeIds + * @param {HashgraphProto.proto.ITransactionBody[]} bodies + * @returns {AirdropTokenTransaction} + */ + static _fromProtobuf( + transactions, + signedTransactions, + transactionIds, + nodeIds, + bodies, + ) { + const body = bodies[0]; + const tokenAirdrop = + /** @type {HashgraphProto.proto.ITokenAirdropTransactionBody} */ ( + body.tokenAirdrop + ); + + return Transaction._fromProtobufTransactions( + new AirdropTokenTransaction({ + tokenTransfer: tokenAirdrop.tokenTransfers?.map( + (tokenTransfer) => + TokenTransferList._fromProtobuf(tokenTransfer), + ), + }), + transactions, + signedTransactions, + transactionIds, + nodeIds, + bodies, + ); + } + + /** + * @override + * @protected + * @returns {NonNullable} + */ + _getTransactionDataCase() { + return "tokenAirdrop"; + } +} + +TRANSACTION_REGISTRY.set( + "tokenAirdrop", + // eslint-disable-next-line @typescript-eslint/unbound-method + AirdropTokenTransaction._fromProtobuf, +); diff --git a/src/token/TokenTransferList.js b/src/token/TokenTransferList.js new file mode 100644 index 000000000..934f24a56 --- /dev/null +++ b/src/token/TokenTransferList.js @@ -0,0 +1,159 @@ +/** + * @namespace proto + * @typedef {import("@hashgraph/proto").proto.ITokenTransferList} HashgraphProto.proto.ITokenTransferList + * @typedef {import("@hashgraph/proto").google.protobuf.IUInt32Value} Google.proto.Uint32Value + */ + +import TokenId from "./TokenId.js"; + +/** + * @typedef {import("./AccountAmount").default} AccountAmount + * @typedef {import("./TokenNftTransfer").default} TokenNftTransfer + */ +export default class TokenTransferList { + /** + * @param {object} props + * @param {TokenId} [props.tokenId] + * @param {AccountAmount[]} [props.accountAmounts] + * @param {TokenNftTransfer[]} [props.tokenNftTransfers] + * @param {number} [props.expectedDecimals] + */ + constructor(props = {}) { + /** + * @private + * @type {?TokenId} + */ + this._tokenId = null; + + /** + * @private + * @type {AccountAmount[]} + */ + this._accountAmounts = []; + + /** + * @private + * @type {TokenNftTransfer[]} + */ + this._tokenNftTransfers = []; + + /** + * @private + * @type {?number} + */ + this._expectedDecimals = null; + + if (props.tokenId != null) { + this.setTokenId(props.tokenId); + } + + if (props.accountAmounts != null) { + this.setAccountAmounts(props.accountAmounts); + } + + if (props.tokenNftTransfers != null) { + this.setTokenNftTransfers(props.tokenNftTransfers); + } + + if (props.expectedDecimals != null) { + this.setExpectedDecimals(props.expectedDecimals); + } + } + + /** + * @returns {?TokenId} + */ + get tokenId() { + return this._tokenId; + } + + /** + * @param {TokenId} tokenId + * @returns {this} + */ + setTokenId(tokenId) { + this._tokenId = tokenId; + return this; + } + + /** + * @returns {AccountAmount[]} + */ + get accountAmounts() { + return this._accountAmounts; + } + + /** + * @param {AccountAmount[]} accountAmounts + * @returns + */ + setAccountAmounts(accountAmounts) { + this._accountAmounts = accountAmounts; + return this; + } + + /** + * @returns {TokenNftTransfer[]} + */ + get tokenNftTransfers() { + return this._tokenNftTransfers; + } + + /** + * + * @param {TokenNftTransfer[]} tokenNftTransfers + * @returns {this} + */ + setTokenNftTransfers(tokenNftTransfers) { + this._tokenNftTransfers = tokenNftTransfers; + return this; + } + + /** + * @returns {?number} + */ + get expectedDecimals() { + return this._expectedDecimals; + } + + /** + * + * @param {number} expectedDecimals + * @returns {this} + */ + setExpectedDecimals(expectedDecimals) { + this._expectedDecimals = expectedDecimals; + return this; + } + + /** + * @returns {HashgraphProto.proto.ITokenTransferList} + */ + _toProtobuf() { + return { + token: this._tokenId != null ? this._tokenId._toProtobuf() : null, + transfers: this._accountAmounts.map((accountAmount) => + accountAmount._toProtobuf(), + ), + nftTransfers: this._tokenNftTransfers.map((tokenNftTransfer) => + tokenNftTransfer._toProtobuf(), + ), + /** + * TODO: uint32 value doesn't exist in js + */ + }; + } + + /** + * @param {HashgraphProto.proto.ITokenTransferList} tokenTransferList + * @returns {TokenTransferList} + */ + static _fromProtobuf(tokenTransferList) { + return new TokenTransferList({ + tokenId: + tokenTransferList.token != null + ? TokenId._fromProtobuf(tokenTransferList.token) + : undefined, + }); + } +} From bd5d5e392acf5457bfccec0995812f2c5752ea76 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Mon, 26 Aug 2024 01:44:31 +0300 Subject: [PATCH 02/33] test(wip): unit tests Signed-off-by: Ivaylo Nikolov --- test/unit/AirdropTokenTransaction.js | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/unit/AirdropTokenTransaction.js diff --git a/test/unit/AirdropTokenTransaction.js b/test/unit/AirdropTokenTransaction.js new file mode 100644 index 000000000..9d012f42f --- /dev/null +++ b/test/unit/AirdropTokenTransaction.js @@ -0,0 +1,34 @@ +/* eslint-disable mocha/no-setup-in-describe */ + +import { + AccountId, + AirdropTokenTransaction, + TokenId, +} from "../../src/index.js"; +import TokenTransferList from "../../src/token/TokenTransferList.js"; +import AccountAmount from "../../src/token/AccountAmount.js"; + +describe("Transaction", function () { + it("toBytes", async function () { + const user = new AccountId(0, 0, 1); + const tokenId = new TokenId(0, 0, 1); + let accountAmounts = new AccountAmount() + .setAccountId(user) + .setIsApproval(true) + .setAmount(1000); + + let transferList = new TokenTransferList() + .setAccountAmounts([accountAmounts]) + .setTokenId(tokenId) + .setExpectedDecimals(1); + + const transaction = new AirdropTokenTransaction().setTokenTransfers([ + transferList, + ]); + + console.log(AirdropTokenTransaction.fromBytes(transaction.toBytes())); + /*console.log( + AirdropTokenTransaction._fromProtobuf(transaction._toProtobuf()), + );*/ + }); +}); From 46f81587c00fa6f91e99d918652bc9064225554e Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 27 Aug 2024 23:37:10 +0300 Subject: [PATCH 03/33] fix: use interface for AccountAmount protobuf Signed-off-by: Ivaylo Nikolov --- src/token/AccountAmount.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/token/AccountAmount.js b/src/token/AccountAmount.js index 61b633a97..2b8e2ffd2 100644 --- a/src/token/AccountAmount.js +++ b/src/token/AccountAmount.js @@ -1,6 +1,6 @@ /** * @namespace proto - * @typedef {import("@hashgraph/proto").proto.AccountAmount} Hashgraph.proto.AccountAmount + * @typedef {import("@hashgraph/proto").proto.IAccountAmount} Hashgraph.proto.IAccountAmount */ import Long from "long"; @@ -15,7 +15,7 @@ export default class AccountAmount { */ constructor(props = {}) { this._accountId = null; - this._amount = null; + this._amount = Long.ZERO; this._isApproval = null; if (props.accountId != null) { @@ -78,19 +78,19 @@ export default class AccountAmount { } /** - * @returns {Hashgraph.proto.AccountAmount} + * @returns {Hashgraph.proto.IAccountAmount} */ _toProtobuf() { return { accountID: this._accountId != null ? this._accountId._toProtobuf() : null, - amount: this._amount != null ? this._amount : Long.ZERO, + amount: this._amount, isApproval: this._isApproval != null ? this._isApproval : false, }; } /** - * @param {Hashgraph.proto.AccountAmount} pb + * @param {Hashgraph.proto.IAccountAmount} pb * @returns {AccountAmount} */ static _fromProtobuf(pb) { @@ -99,7 +99,7 @@ export default class AccountAmount { pb.accountID != null ? AccountId._fromProtobuf(pb.accountID) : undefined, - amount: pb.amount, + amount: pb.amount != null ? pb.amount : Long.ZERO, isApproval: pb.isApproval, }); } From fb4d428a4c39f7478b798ddce9e2a3883a54e9c0 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 27 Aug 2024 23:37:46 +0300 Subject: [PATCH 04/33] fix: remove circular dependancy Signed-off-by: Ivaylo Nikolov --- src/token/AccountAmount.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/token/AccountAmount.js b/src/token/AccountAmount.js index 2b8e2ffd2..2ebca8ac9 100644 --- a/src/token/AccountAmount.js +++ b/src/token/AccountAmount.js @@ -4,7 +4,7 @@ */ import Long from "long"; -import { AccountId } from "../exports.js"; +import AccountId from "../account/AccountId.js"; export default class AccountAmount { /** From 42b74d50713e2da4fe3e2e0cfeaddf5abaa472ca Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 27 Aug 2024 23:39:34 +0300 Subject: [PATCH 05/33] refactor: RenameTokenTransfer list to TokenTransfer Signed-off-by: Ivaylo Nikolov --- src/token/AirdropTokenTransaction.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/token/AirdropTokenTransaction.js b/src/token/AirdropTokenTransaction.js index b764cb6f3..2596ae6f7 100644 --- a/src/token/AirdropTokenTransaction.js +++ b/src/token/AirdropTokenTransaction.js @@ -1,7 +1,7 @@ import Transaction, { TRANSACTION_REGISTRY, } from "../transaction/Transaction.js"; -import TokenTransferList from "./TokenTransferList.js"; +import TokenTransfer from "./AirdropTokenTransfer.js"; /** * @namespace proto @@ -24,13 +24,13 @@ import TokenTransferList from "./TokenTransferList.js"; export default class AirdropTokenTransaction extends Transaction { /** * @param {object} props - * @param {TokenTransferList[]} [props.tokenTransfer] + * @param {TokenTransfer[]} [props.tokenTransfer] */ constructor(props = {}) { super(); /** * @private - * @type {TokenTransferList[]} + * @type {TokenTransfer[]} */ this._tokenTransfers = []; @@ -40,14 +40,14 @@ export default class AirdropTokenTransaction extends Transaction { } /** - * @returns {TokenTransferList[]} + * @returns {TokenTransfer[]} */ get tokenTransfers() { return this._tokenTransfers; } /** - * @param {TokenTransferList[]} tokenTransfers + * @param {TokenTransfer[]} tokenTransfers * @returns {this} */ setTokenTransfers(tokenTransfers) { @@ -93,7 +93,7 @@ export default class AirdropTokenTransaction extends Transaction { new AirdropTokenTransaction({ tokenTransfer: tokenAirdrop.tokenTransfers?.map( (tokenTransfer) => - TokenTransferList._fromProtobuf(tokenTransfer), + TokenTransfer._fromProtobuf(tokenTransfer), ), }), transactions, From d3febec7770914406f9a6daac218d8a984063098 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 27 Aug 2024 23:41:07 +0300 Subject: [PATCH 06/33] feat: rename token transfer and add expected decimals Signed-off-by: Ivaylo Nikolov --- ...ransferList.js => AirdropTokenTransfer.js} | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) rename src/token/{TokenTransferList.js => AirdropTokenTransfer.js} (78%) diff --git a/src/token/TokenTransferList.js b/src/token/AirdropTokenTransfer.js similarity index 78% rename from src/token/TokenTransferList.js rename to src/token/AirdropTokenTransfer.js index 934f24a56..7678e37db 100644 --- a/src/token/TokenTransferList.js +++ b/src/token/AirdropTokenTransfer.js @@ -1,16 +1,12 @@ /** * @namespace proto * @typedef {import("@hashgraph/proto").proto.ITokenTransferList} HashgraphProto.proto.ITokenTransferList - * @typedef {import("@hashgraph/proto").google.protobuf.IUInt32Value} Google.proto.Uint32Value */ - +import AccountAmount from "./AccountAmount.js"; import TokenId from "./TokenId.js"; +import TokenNftTransfer from "./TokenNftTransfer.js"; -/** - * @typedef {import("./AccountAmount").default} AccountAmount - * @typedef {import("./TokenNftTransfer").default} TokenNftTransfer - */ -export default class TokenTransferList { +export default class TokenTransfer { /** * @param {object} props * @param {TokenId} [props.tokenId] @@ -85,7 +81,7 @@ export default class TokenTransferList { /** * @param {AccountAmount[]} accountAmounts - * @returns + * @returns {this} */ setAccountAmounts(accountAmounts) { this._accountAmounts = accountAmounts; @@ -138,22 +134,29 @@ export default class TokenTransferList { nftTransfers: this._tokenNftTransfers.map((tokenNftTransfer) => tokenNftTransfer._toProtobuf(), ), - /** - * TODO: uint32 value doesn't exist in js - */ + expectedDecimals: + this._expectedDecimals != null + ? { value: this._expectedDecimals } + : null, }; } /** - * @param {HashgraphProto.proto.ITokenTransferList} tokenTransferList - * @returns {TokenTransferList} + * @param {HashgraphProto.proto.ITokenTransferList} tokenTransfer + * @returns {TokenTransfer} */ - static _fromProtobuf(tokenTransferList) { - return new TokenTransferList({ + static _fromProtobuf(tokenTransfer) { + return new TokenTransfer({ tokenId: - tokenTransferList.token != null - ? TokenId._fromProtobuf(tokenTransferList.token) + tokenTransfer.token != null + ? TokenId._fromProtobuf(tokenTransfer.token) : undefined, + accountAmounts: tokenTransfer.transfers?.map((transfer) => + AccountAmount._fromProtobuf(transfer), + ), + tokenNftTransfers: TokenNftTransfer._fromProtobuf([tokenTransfer]), + expectedDecimals: + tokenTransfer.expectedDecimals?.value || undefined, }); } } From ae01f4b8f9dc98c345b2daec8698d66be011b120 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 27 Aug 2024 23:42:01 +0300 Subject: [PATCH 07/33] refactor: remove redundant code Signed-off-by: Ivaylo Nikolov --- src/token/AirdropTokenTransaction.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/token/AirdropTokenTransaction.js b/src/token/AirdropTokenTransaction.js index 2596ae6f7..b185f5953 100644 --- a/src/token/AirdropTokenTransaction.js +++ b/src/token/AirdropTokenTransaction.js @@ -11,10 +11,7 @@ import TokenTransfer from "./AirdropTokenTransfer.js"; * @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.ITransactionResponse} HashgraphProto.proto.ITransactionResponse * @typedef {import("@hashgraph/proto").proto.TransactionBody} HashgraphProto.proto.TransactionBody - * @typedef {import("@hashgraph/proto").proto.ITokenRejectTransactionBody} HashgraphProto.proto.ITokenRejectTransactionBody - * @typedef {import("@hashgraph/proto").proto.TokenReference} HashgraphProto.proto.TokenReference */ /** @@ -51,7 +48,6 @@ export default class AirdropTokenTransaction extends Transaction { * @returns {this} */ setTokenTransfers(tokenTransfers) { - console.log(tokenTransfers.length); this._tokenTransfers = tokenTransfers; return this; } From f68acd2deebb73b243c0831854bc34238d3580da Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 27 Aug 2024 23:42:34 +0300 Subject: [PATCH 08/33] test: finished unit tests Signed-off-by: Ivaylo Nikolov --- test/unit/AirdropTokenTransaction.js | 151 +++++++++++++++++++++++---- 1 file changed, 132 insertions(+), 19 deletions(-) diff --git a/test/unit/AirdropTokenTransaction.js b/test/unit/AirdropTokenTransaction.js index 9d012f42f..26e50524b 100644 --- a/test/unit/AirdropTokenTransaction.js +++ b/test/unit/AirdropTokenTransaction.js @@ -1,34 +1,147 @@ -/* eslint-disable mocha/no-setup-in-describe */ - +import { expect } from "chai"; import { AccountId, AirdropTokenTransaction, TokenId, } from "../../src/index.js"; -import TokenTransferList from "../../src/token/TokenTransferList.js"; +import TokenTransfer from "../../src/token/TokenTransfer.js"; import AccountAmount from "../../src/token/AccountAmount.js"; +import TokenNftTransfer from "../../src/token/TokenNftTransfer.js"; + describe("Transaction", function () { - it("toBytes", async function () { - const user = new AccountId(0, 0, 1); - const tokenId = new TokenId(0, 0, 1); - let accountAmounts = new AccountAmount() - .setAccountId(user) - .setIsApproval(true) - .setAmount(1000); - - let transferList = new TokenTransferList() - .setAccountAmounts([accountAmounts]) - .setTokenId(tokenId) + it("from | toBytes", async function () { + const USER = new AccountId(0, 0, 100); + const TOKEN_ID = new TokenId(0, 0, 1); + const NFT_ID = 1; + const IS_APPROVAL = true; + const AMOUNT = 1000; + + let accountAmount = new AccountAmount() + .setAccountId(USER) + .setIsApproval(IS_APPROVAL) + .setAmount(AMOUNT); + + let nftTransfer = new TokenNftTransfer({ + isApproved: true, + receiverAccountId: USER, + senderAccountId: USER, + serialNumber: NFT_ID, + tokenId: TOKEN_ID, + }); + + let tokenTransfer = new TokenTransfer() + .setAccountAmounts([accountAmount]) + .setTokenNftTransfers([nftTransfer]) + .setTokenId(TOKEN_ID) .setExpectedDecimals(1); const transaction = new AirdropTokenTransaction().setTokenTransfers([ - transferList, + tokenTransfer, ]); - console.log(AirdropTokenTransaction.fromBytes(transaction.toBytes())); - /*console.log( - AirdropTokenTransaction._fromProtobuf(transaction._toProtobuf()), - );*/ + const txBytes = transaction.toBytes(); + const tx = AirdropTokenTransaction.fromBytes(txBytes); + + expect(tx.tokenTransfers[0].tokenId).to.deep.equal(TOKEN_ID); + + // token transfer tests + expect(tx.tokenTransfers[0].expectedDecimals).to.equal(1); + expect(tx.tokenTransfers[0].tokenId).to.deep.equal(TOKEN_ID); + expect(tx.tokenTransfers.length).to.equal(1); + + // account amount tests + expect(tx.tokenTransfers[0].accountAmounts[0].accountId).to.deep.equal( + USER, + ); + expect( + tx.tokenTransfers[0].accountAmounts[0].amount.toInt(), + ).to.deep.equal(accountAmount.amount); + expect(tx.tokenTransfers[0].accountAmounts[0].isApproval).to.deep.equal( + accountAmount.isApproval, + ); + expect(tx.tokenTransfers[0].accountAmounts.length).to.equal(1); + + // nft transfer tests + expect(tx.tokenTransfers[0].tokenNftTransfers[0].tokenId).to.deep.equal( + TOKEN_ID, + ); + expect( + tx.tokenTransfers[0].tokenNftTransfers[0].senderAccountId, + ).to.deep.equal(USER); + expect( + tx.tokenTransfers[0].tokenNftTransfers[0].receiverAccountId, + ).to.deep.equal(USER); + expect( + tx.tokenTransfers[0].tokenNftTransfers[0].serialNumber.toInt(), + ).to.deep.equal(NFT_ID); + expect( + tx.tokenTransfers[0].tokenNftTransfers[0].isApproved, + ).to.deep.equal(IS_APPROVAL); + expect(tx.tokenTransfers[0].tokenNftTransfers.length).to.equal(1); + }); + + describe("Token Transfer", function () { + let tokenTransfer; + + beforeEach(function () { + tokenTransfer = new TokenTransfer(); + }); + + it("should set token transfer", function () { + const ACCOUNT_AMOUNT = new AccountAmount(); + tokenTransfer.setAccountAmounts(ACCOUNT_AMOUNT); + expect(tokenTransfer.accountAmounts).to.equal(ACCOUNT_AMOUNT); + }); + + it("should set expected decimals", function () { + const EXPECTED_DECIMALS = 1; + tokenTransfer.setExpectedDecimals(EXPECTED_DECIMALS); + expect(tokenTransfer.expectedDecimals).to.equal(EXPECTED_DECIMALS); + }); + + it("should set token id", function () { + const TOKEN_ID = new TokenId(0, 0, 1); + tokenTransfer.setTokenId(TOKEN_ID); + }); + + it("should set token nft transfers", function () { + const TOKEN_NFT_TRANSFER = new TokenNftTransfer({ + isApproved: true, + receiverAccountId: new AccountId(0, 0, 100), + senderAccountId: new AccountId(0, 0, 100), + serialNumber: 1, + tokenId: new TokenId(0, 0, 1), + }); + tokenTransfer.setTokenNftTransfers([TOKEN_NFT_TRANSFER]); + expect(tokenTransfer.tokenNftTransfers).to.deep.equal([ + TOKEN_NFT_TRANSFER, + ]); + }); + }); + + describe("Account Amount", function () { + let accountAmount; + beforeEach(function () { + accountAmount = new AccountAmount(); + }); + + it("should set account id", function () { + const ACCOUNT_ID = new AccountId(0, 0, 100); + accountAmount.setAccountId(ACCOUNT_ID); + expect(accountAmount.accountId).to.deep.equal(ACCOUNT_ID); + }); + + it("should set amount", function () { + const AMOUNT = 1000; + accountAmount.setAmount(AMOUNT); + expect(accountAmount.amount).to.equal(AMOUNT); + }); + + it("should set is approval", function () { + const IS_APPROVAL = true; + accountAmount.setIsApproval(IS_APPROVAL); + expect(accountAmount.isApproval).to.equal(IS_APPROVAL); + }); }); }); From 34673c986cb5bce9c07b2ac90451eeec741fae2b Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 27 Aug 2024 23:43:47 +0300 Subject: [PATCH 09/33] fix: correct return type for amount Signed-off-by: Ivaylo Nikolov --- src/token/AccountAmount.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/token/AccountAmount.js b/src/token/AccountAmount.js index 2ebca8ac9..8ccc5ce8b 100644 --- a/src/token/AccountAmount.js +++ b/src/token/AccountAmount.js @@ -46,7 +46,7 @@ export default class AccountAmount { } /** - * @returns {Long?} + * @returns {Long} */ get amount() { return this._amount; From 75cf548a0c03ab02098ff996741f15ae533491b3 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 27 Aug 2024 23:44:37 +0300 Subject: [PATCH 10/33] fix: correct return for fromProtobuf Signed-off-by: Ivaylo Nikolov --- src/token/AccountAmount.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/token/AccountAmount.js b/src/token/AccountAmount.js index 8ccc5ce8b..d2698c3da 100644 --- a/src/token/AccountAmount.js +++ b/src/token/AccountAmount.js @@ -100,7 +100,7 @@ export default class AccountAmount { ? AccountId._fromProtobuf(pb.accountID) : undefined, amount: pb.amount != null ? pb.amount : Long.ZERO, - isApproval: pb.isApproval, + isApproval: pb.isApproval != null ? pb.isApproval : false, }); } } From 4c66eecba15b69cfffe45ebc3978e97f2ae0607d Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Wed, 28 Aug 2024 23:52:18 +0300 Subject: [PATCH 11/33] feat: add missing methods Signed-off-by: Ivaylo Nikolov --- src/token/AirdropNftTransfer.js | 119 ++++++++++++++++ src/token/AirdropTokenTransaction.js | 197 ++++++++++++++++++++++++++- src/token/AirdropTokenTransfer.js | 2 +- 3 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 src/token/AirdropNftTransfer.js diff --git a/src/token/AirdropNftTransfer.js b/src/token/AirdropNftTransfer.js new file mode 100644 index 000000000..8604d9424 --- /dev/null +++ b/src/token/AirdropNftTransfer.js @@ -0,0 +1,119 @@ +/*- + * ‌ + * Hedera JavaScript SDK + * ​ + * Copyright (C) 2020 - 2023 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 Long from "long"; +import AccountId from "../account/AccountId.js"; + +/** + * @namespace proto + * @typedef {import("@hashgraph/proto").proto.ITokenTransferList} HashgraphProto.proto.ITokenTransferList + * @typedef {import("@hashgraph/proto").proto.IAccountAmount} HashgraphProto.proto.IAccountAmount + * @typedef {import("@hashgraph/proto").proto.INftTransfer} HashgraphProto.proto.INftTransfer + * @typedef {import("@hashgraph/proto").proto.IAccountID} HashgraphProto.proto.IAccountID + * @typedef {import("@hashgraph/proto").proto.ITokenID} HashgraphProto.proto.ITokenID + */ + +/** + * @typedef {import("bignumber.js").default} BigNumber + */ + +/** + * An account, and the amount that it sends or receives during a cryptocurrency tokentransfer. + */ +export default class NftTransfer { + /** + * @internal + * @param {object} props + * @param {AccountId | string} props.senderAccountId + * @param {AccountId | string} props.receiverAccountId + * @param {Long | number} props.serialNumber + * @param {boolean} props.isApproved + */ + constructor(props) { + /** + * The Account ID that sends or receives cryptocurrency. + */ + this.senderAccountId = + props.senderAccountId instanceof AccountId + ? props.senderAccountId + : AccountId.fromString(props.senderAccountId); + + /** + * The Account ID that sends or receives cryptocurrency. + */ + this.receiverAccountId = + props.receiverAccountId instanceof AccountId + ? props.receiverAccountId + : AccountId.fromString(props.receiverAccountId); + + this.serialNumber = Long.fromValue(props.serialNumber); + this.isApproved = props.isApproved; + } + + /** + * @internal + * @param {HashgraphProto.proto.ITokenTransferList[]} tokenTransfers + * @returns {NftTransfer[]} + */ + static _fromProtobuf(tokenTransfers) { + const transfers = []; + + for (const tokenTransfer of tokenTransfers) { + for (const transfer of tokenTransfer.nftTransfers != null + ? tokenTransfer.nftTransfers + : []) { + transfers.push( + new NftTransfer({ + senderAccountId: AccountId._fromProtobuf( + /** @type {HashgraphProto.proto.IAccountID} */ ( + transfer.senderAccountID + ), + ), + receiverAccountId: AccountId._fromProtobuf( + /** @type {HashgraphProto.proto.IAccountID} */ ( + transfer.receiverAccountID + ), + ), + serialNumber: + transfer.serialNumber != null + ? transfer.serialNumber + : Long.ZERO, + isApproved: transfer.isApproval == true, + }), + ); + } + } + + return transfers; + } + + /** + * @internal + * @returns {HashgraphProto.proto.INftTransfer} + */ + _toProtobuf() { + return { + senderAccountID: this.senderAccountId._toProtobuf(), + receiverAccountID: this.receiverAccountId._toProtobuf(), + serialNumber: this.serialNumber, + isApproval: this.isApproved, + }; + } +} diff --git a/src/token/AirdropTokenTransaction.js b/src/token/AirdropTokenTransaction.js index b185f5953..7ea241e6f 100644 --- a/src/token/AirdropTokenTransaction.js +++ b/src/token/AirdropTokenTransaction.js @@ -1,7 +1,9 @@ import Transaction, { TRANSACTION_REGISTRY, } from "../transaction/Transaction.js"; +import AccountAmount from "./AccountAmount.js"; import TokenTransfer from "./AirdropTokenTransfer.js"; +import AirdropNftTransfer from "./AirdropNftTransfer.js"; /** * @namespace proto @@ -17,11 +19,14 @@ import TokenTransfer from "./AirdropTokenTransfer.js"; /** * @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 AirdropTokenTransaction extends Transaction { /** * @param {object} props * @param {TokenTransfer[]} [props.tokenTransfer] + * @param {[]} [props.tokenTransfer] */ constructor(props = {}) { super(); @@ -39,8 +44,189 @@ export default class AirdropTokenTransaction extends Transaction { /** * @returns {TokenTransfer[]} */ + /* get tokenTransfers() { - return this._tokenTransfers; + return this._tokenTransfers.filter((tokenTransfer) => { + return tokenTransfer.accountAmounts.length > 0; + }); + } + */ + + /** + * @returns {Map>} + */ + /* + get tokenNftTransfers() { + const result = new Map(); + const nftTransfers = this._tokenTransfers.filter((tokenTransfer) => { + return tokenTransfer.tokenNftTransfers.length > 0; + }); + for (let tokenTransfer of this._tokenTransfers) { + for (let nftTransfer of tokenTransfer.tokenNftTransfers) { + const accountId = nftTransfer.receiverAccountId; + const tokenId = tokenTransfer.tokenId; + const serialNumber = nftTransfer.serialNumber; + if (!result.has(accountId)) { + result.set(accountId, new Map()); + } + } + } + + return result; + } + */ + + /** + * @returns {Map} + */ + get tokenDecimals() { + const tokenDecimals = new Map(); + this._tokenTransfers.forEach((tokenTransfer) => { + if (tokenTransfer.expectedDecimals) { + tokenDecimals.set( + tokenTransfer.tokenId, + tokenTransfer.expectedDecimals, + ); + } + }); + return tokenDecimals; + } + + /** + * @param {TokenId} tokenId + * @param {AccountId} accountId + * @param {Long} amount + */ + addTokenTransfer(tokenId, accountId, amount) { + const nonApproved = false; + const accountAmount = new AccountAmount({ + accountId, + amount, + isApproval: nonApproved, + }); + const tokenTransfer = new TokenTransfer({ + tokenId, + accountAmounts: [accountAmount], + }); + this._tokenTransfers.push(tokenTransfer); + } + + /** + * + * @param {TokenId} tokenId + * @param {AccountId} accountId + * @param {Long} amount + * @returns {this} + */ + addApprovedTokenTransfer(tokenId, accountId, amount) { + const isApproval = true; + const accountAmount = new AccountAmount({ + accountId, + amount, + isApproval, + }); + const tokenTransfer = new TokenTransfer({ + tokenId, + accountAmounts: [accountAmount], + }); + this._tokenTransfers.push(tokenTransfer); + return this; + } + + /** + * + * @param {TokenId} tokenId + * @param {AccountId} accountId + * @param {Long} amount + * @param {number} expectedDecimals + * @returns {this} + */ + addApprovedTokenTransferWithDecimals( + tokenId, + accountId, + amount, + expectedDecimals, + ) { + const isApproval = true; + const accountAmount = new AccountAmount({ + accountId, + amount, + isApproval, + }); + const tokenTransfer = new TokenTransfer({ + tokenId, + accountAmounts: [accountAmount], + expectedDecimals, + }); + this._tokenTransfers.push(tokenTransfer); + return this; + } + + /** + * + * @param {TokenId} tokenId + * @param {AccountId} accountId + * @param {Long} amount + * @param {number} expectedDecimals + * @returns {this} + */ + addTokenTransferWithDecimals(tokenId, accountId, amount, expectedDecimals) { + const isApproval = false; + const accountAmount = new AccountAmount({ + accountId, + amount, + isApproval, + }); + const tokenTransfer = new TokenTransfer({ + tokenId, + accountAmounts: [accountAmount], + expectedDecimals, + }); + this._tokenTransfers.push(tokenTransfer); + return this; + } + + /** + * @param {NftId} nftId + * @param {AccountId} senderAccountId + * @param {AccountId} receiverAccountId + */ + addNftTransfer(nftId, senderAccountId, receiverAccountId) { + const isApproved = true; + + const nftTransfer = new AirdropNftTransfer({ + senderAccountId, + receiverAccountId, + isApproved, + serialNumber: nftId.serial, + }); + const tokenTransfer = new TokenTransfer({ + tokenNftTransfers: [nftTransfer], + }); + this._tokenTransfers.push(tokenTransfer); + } + + /** + * + * @param {NftId} nftId + * @param {AccountId} sender + * @param {AccountId} receiver + * @returns + */ + addApprovedNftTransfer(nftId, sender, receiver) { + const isApproved = true; + const nftTransfer = new AirdropNftTransfer({ + senderAccountId: sender, + receiverAccountId: receiver, + serialNumber: nftId.serial, + isApproved, + }); + const tokenTransfer = new TokenTransfer({ + tokenId: nftId.tokenId, + tokenNftTransfers: [nftTransfer], + }); + this._tokenTransfers.push(tokenTransfer); + return this; } /** @@ -52,6 +238,15 @@ export default class AirdropTokenTransaction extends Transaction { return this; } + /** + * @param {TokenTransfer[]} tokenTransferList + * @returns {this} + */ + addTokenTransferList(tokenTransferList) { + this._tokenTransfers.push(...tokenTransferList); + return this; + } + /** * @returns {HashgraphProto.proto.ITokenAirdropTransactionBody} */ diff --git a/src/token/AirdropTokenTransfer.js b/src/token/AirdropTokenTransfer.js index 7678e37db..2d2edf80e 100644 --- a/src/token/AirdropTokenTransfer.js +++ b/src/token/AirdropTokenTransfer.js @@ -4,7 +4,7 @@ */ import AccountAmount from "./AccountAmount.js"; import TokenId from "./TokenId.js"; -import TokenNftTransfer from "./TokenNftTransfer.js"; +import TokenNftTransfer from "./AirdropNftTransfer.js"; export default class TokenTransfer { /** From 176d7b826f7674595ace134b82377f3e2a7f11a4 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Thu, 29 Aug 2024 02:06:12 +0300 Subject: [PATCH 12/33] refactor: use already implemented interfaces and classes Signed-off-by: Ivaylo Nikolov --- src/token/AirdropTokenTransaction.js | 184 ++++++++++++++++++--------- 1 file changed, 123 insertions(+), 61 deletions(-) diff --git a/src/token/AirdropTokenTransaction.js b/src/token/AirdropTokenTransaction.js index 7ea241e6f..d15d8b453 100644 --- a/src/token/AirdropTokenTransaction.js +++ b/src/token/AirdropTokenTransaction.js @@ -1,13 +1,15 @@ import Transaction, { TRANSACTION_REGISTRY, } from "../transaction/Transaction.js"; -import AccountAmount from "./AccountAmount.js"; -import TokenTransfer from "./AirdropTokenTransfer.js"; -import AirdropNftTransfer from "./AirdropNftTransfer.js"; +import TokenTransfer from "./TokenTransfer.js"; +import NftTransfer from "./TokenNftTransfer.js"; /** * @namespace proto * @typedef {import("@hashgraph/proto").proto.ITokenAirdropTransactionBody} HashgraphProto.proto.ITokenAirdropTransactionBody + * @typedef {import("@hashgraph/proto").proto.AccountAmount} HashgraphProto.proto.AccountAmount + * @typedef {import("@hashgraph/proto").proto.NftTransfer} HashgraphProto.proto.NftTransfer + * @typedef {import("@hashgraph/proto").proto.TokenTransferList} HashgraphProto.proto.TokenTransferList * @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 @@ -25,8 +27,8 @@ import AirdropNftTransfer from "./AirdropNftTransfer.js"; export default class AirdropTokenTransaction extends Transaction { /** * @param {object} props - * @param {TokenTransfer[]} [props.tokenTransfer] - * @param {[]} [props.tokenTransfer] + * @param {TokenTransfer[]} [props.tokenTransfers] + * @param {NftTransfer[]} [props.nftTransfers] */ constructor(props = {}) { super(); @@ -36,21 +38,31 @@ export default class AirdropTokenTransaction extends Transaction { */ this._tokenTransfers = []; - if (props.tokenTransfer != null) { - this.setTokenTransfers(props.tokenTransfer); + if (props.tokenTransfers != null) { + this.setTokenTransfers(props.tokenTransfers); + } + + /** + * @private + * @type {NftTransfer[]} + */ + this._nftTransfers = []; + + if (props.nftTransfers != null) { + this.setNftTransfers(props.nftTransfers); } } /** - * @returns {TokenTransfer[]} + * @returns {Map>} */ - /* get tokenTransfers() { - return this._tokenTransfers.filter((tokenTransfer) => { + const result = new Map(); + return result; + /*return this._tokenTransfers.filter((tokenTransfer) => { return tokenTransfer.accountAmounts.length > 0; - }); + });*/ } - */ /** * @returns {Map>} @@ -98,15 +110,13 @@ export default class AirdropTokenTransaction extends Transaction { * @param {Long} amount */ addTokenTransfer(tokenId, accountId, amount) { - const nonApproved = false; - const accountAmount = new AccountAmount({ - accountId, - amount, - isApproval: nonApproved, - }); + const NON_APPROVED = false; const tokenTransfer = new TokenTransfer({ tokenId, - accountAmounts: [accountAmount], + accountId: accountId, + amount: amount, + isApproved: NON_APPROVED, + expectedDecimals: null, }); this._tokenTransfers.push(tokenTransfer); } @@ -119,15 +129,13 @@ export default class AirdropTokenTransaction extends Transaction { * @returns {this} */ addApprovedTokenTransfer(tokenId, accountId, amount) { - const isApproval = true; - const accountAmount = new AccountAmount({ - accountId, - amount, - isApproval, - }); + const APPROVED = true; const tokenTransfer = new TokenTransfer({ tokenId, - accountAmounts: [accountAmount], + accountId: accountId, + amount: amount, + isApproved: APPROVED, + expectedDecimals: null, }); this._tokenTransfers.push(tokenTransfer); return this; @@ -147,16 +155,13 @@ export default class AirdropTokenTransaction extends Transaction { amount, expectedDecimals, ) { - const isApproval = true; - const accountAmount = new AccountAmount({ - accountId, - amount, - isApproval, - }); + const IS_APPROVED = true; const tokenTransfer = new TokenTransfer({ tokenId, - accountAmounts: [accountAmount], - expectedDecimals, + accountId, + amount, + isApproved: IS_APPROVED, + expectedDecimals: expectedDecimals, }); this._tokenTransfers.push(tokenTransfer); return this; @@ -171,15 +176,12 @@ export default class AirdropTokenTransaction extends Transaction { * @returns {this} */ addTokenTransferWithDecimals(tokenId, accountId, amount, expectedDecimals) { - const isApproval = false; - const accountAmount = new AccountAmount({ - accountId, - amount, - isApproval, - }); + const IS_APPROVED = false; const tokenTransfer = new TokenTransfer({ tokenId, - accountAmounts: [accountAmount], + accountId, + amount, + isApproved: IS_APPROVED, expectedDecimals, }); this._tokenTransfers.push(tokenTransfer); @@ -192,18 +194,16 @@ export default class AirdropTokenTransaction extends Transaction { * @param {AccountId} receiverAccountId */ addNftTransfer(nftId, senderAccountId, receiverAccountId) { - const isApproved = true; + const isApproved = false; - const nftTransfer = new AirdropNftTransfer({ + const nftTransfer = new NftTransfer({ + tokenId: nftId.tokenId, senderAccountId, receiverAccountId, - isApproved, serialNumber: nftId.serial, + isApproved, }); - const tokenTransfer = new TokenTransfer({ - tokenNftTransfers: [nftTransfer], - }); - this._tokenTransfers.push(tokenTransfer); + this._nftTransfers.push(nftTransfer); } /** @@ -215,17 +215,14 @@ export default class AirdropTokenTransaction extends Transaction { */ addApprovedNftTransfer(nftId, sender, receiver) { const isApproved = true; - const nftTransfer = new AirdropNftTransfer({ + const nftTransfer = new NftTransfer({ senderAccountId: sender, receiverAccountId: receiver, serialNumber: nftId.serial, - isApproved, - }); - const tokenTransfer = new TokenTransfer({ tokenId: nftId.tokenId, - tokenNftTransfers: [nftTransfer], + isApproved, }); - this._tokenTransfers.push(tokenTransfer); + this._nftTransfers.push(nftTransfer); return this; } @@ -247,14 +244,74 @@ export default class AirdropTokenTransaction extends Transaction { return this; } + /** + * @param {NftTransfer[]} nftTransfers + * @returns {this} + */ + setNftTransfers(nftTransfers) { + this._nftTransfers = nftTransfers; + return this; + } + /** * @returns {HashgraphProto.proto.ITokenAirdropTransactionBody} */ _makeTransactionData() { + /** + * @type {HashgraphProto.proto.AccountAmount[]} + */ + const tokenTransfers = this._tokenTransfers.map((tokenTransfer) => { + return { + accountId: tokenTransfer.accountId._toProtobuf(), + amount: tokenTransfer.amount, + isApproval: tokenTransfer.isApproved, + }; + }); + + /** + * @type {HashgraphProto.proto.NftTransfer[]} + */ + const nftTransfers = this._nftTransfers.map((nftTransfer) => { + return { + senderAccountId: nftTransfer.senderAccountId._toProtobuf(), + isApproval: nftTransfer.isApproved, + receiverAccountId: nftTransfer.receiverAccountId._toProtobuf(), + serialNumber: nftTransfer.serialNumber, + }; + }); + + const tokenTransferWithId = this._tokenTransfers.find( + (tokenTransfer) => { + return tokenTransfer.tokenId != null; + }, + ); + const tokenId = tokenTransferWithId?.tokenId; + + const tokenTransferWithDecimals = this._tokenTransfers.find( + (tokenTransfer) => { + return tokenTransfer.expectedDecimals != null; + }, + ); + const expectedDecimals = tokenTransferWithDecimals?.expectedDecimals; + + if (tokenId == null) { + throw new Error("Token ID is required"); + } + + /** + * @type {HashgraphProto.proto.TokenTransferList} + */ + const tokenTransfersList = { + transfers: tokenTransfers, + nftTransfers: nftTransfers, + token: tokenId._toProtobuf(), + expectedDecimals: { + value: expectedDecimals, + }, + }; + return { - tokenTransfers: this._tokenTransfers.map((tokenTransfer) => - tokenTransfer._toProtobuf(), - ), + tokenTransfers: [tokenTransfersList], }; } @@ -280,12 +337,17 @@ export default class AirdropTokenTransaction extends Transaction { body.tokenAirdrop ); + const tokenTransfers = TokenTransfer._fromProtobuf( + tokenAirdrop.tokenTransfers ?? [], + ); + const nftTransfers = NftTransfer._fromProtobuf( + tokenAirdrop.tokenTransfers ?? [], + ); + return Transaction._fromProtobufTransactions( new AirdropTokenTransaction({ - tokenTransfer: tokenAirdrop.tokenTransfers?.map( - (tokenTransfer) => - TokenTransfer._fromProtobuf(tokenTransfer), - ), + nftTransfers: nftTransfers, + tokenTransfers: tokenTransfers, }), transactions, signedTransactions, From af7a7df60233829804e5cb7ecc6175b406e98e28 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 30 Aug 2024 00:34:57 +0300 Subject: [PATCH 13/33] refactor: transfer transaction and airdrop transaction Signed-off-by: Ivaylo Nikolov --- src/account/TransferTransaction.js | 542 +------------------------ src/token/AbstractTokenTransfer.js | 565 +++++++++++++++++++++++++++ src/token/AirdropTokenTransaction.js | 271 ++----------- src/token/TokenNftTransfer.js | 1 - test/unit/AirdropTokenTransaction.js | 208 ++++------ 5 files changed, 674 insertions(+), 913 deletions(-) create mode 100644 src/token/AbstractTokenTransfer.js diff --git a/src/account/TransferTransaction.js b/src/account/TransferTransaction.js index 7df47038f..c1b0ef62e 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 AbstractTokenTransfer from "../token/AbstractTokenTransfer.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 AbstractTokenTransfer { /** * @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/token/AbstractTokenTransfer.js b/src/token/AbstractTokenTransfer.js new file mode 100644 index 000000000..ad8b7e7a0 --- /dev/null +++ b/src/token/AbstractTokenTransfer.js @@ -0,0 +1,565 @@ +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 AbstractTokenTransfer 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/AirdropTokenTransaction.js b/src/token/AirdropTokenTransaction.js index d15d8b453..f697b38ac 100644 --- a/src/token/AirdropTokenTransaction.js +++ b/src/token/AirdropTokenTransaction.js @@ -3,13 +3,11 @@ import Transaction, { } from "../transaction/Transaction.js"; import TokenTransfer from "./TokenTransfer.js"; import NftTransfer from "./TokenNftTransfer.js"; +import AbstractTokenTransfer from "./AbstractTokenTransfer.js"; /** * @namespace proto * @typedef {import("@hashgraph/proto").proto.ITokenAirdropTransactionBody} HashgraphProto.proto.ITokenAirdropTransactionBody - * @typedef {import("@hashgraph/proto").proto.AccountAmount} HashgraphProto.proto.AccountAmount - * @typedef {import("@hashgraph/proto").proto.NftTransfer} HashgraphProto.proto.NftTransfer - * @typedef {import("@hashgraph/proto").proto.TokenTransferList} HashgraphProto.proto.TokenTransferList * @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 @@ -24,7 +22,7 @@ import NftTransfer from "./TokenNftTransfer.js"; * @typedef {import("./NftId.js").default} NftId * @typedef {import("./TokenId.js").default} TokenId */ -export default class AirdropTokenTransaction extends Transaction { +export default class AirdropTokenTransaction extends AbstractTokenTransfer { /** * @param {object} props * @param {TokenTransfer[]} [props.tokenTransfers] @@ -39,106 +37,32 @@ export default class AirdropTokenTransaction extends Transaction { this._tokenTransfers = []; if (props.tokenTransfers != null) { - this.setTokenTransfers(props.tokenTransfers); + 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) { - this.setNftTransfers(props.nftTransfers); - } - } - - /** - * @returns {Map>} - */ - get tokenTransfers() { - const result = new Map(); - return result; - /*return this._tokenTransfers.filter((tokenTransfer) => { - return tokenTransfer.accountAmounts.length > 0; - });*/ - } - - /** - * @returns {Map>} - */ - /* - get tokenNftTransfers() { - const result = new Map(); - const nftTransfers = this._tokenTransfers.filter((tokenTransfer) => { - return tokenTransfer.tokenNftTransfers.length > 0; - }); - for (let tokenTransfer of this._tokenTransfers) { - for (let nftTransfer of tokenTransfer.tokenNftTransfers) { - const accountId = nftTransfer.receiverAccountId; - const tokenId = tokenTransfer.tokenId; - const serialNumber = nftTransfer.serialNumber; - if (!result.has(accountId)) { - result.set(accountId, new Map()); - } - } - } - - return result; - } - */ - - /** - * @returns {Map} - */ - get tokenDecimals() { - const tokenDecimals = new Map(); - this._tokenTransfers.forEach((tokenTransfer) => { - if (tokenTransfer.expectedDecimals) { - tokenDecimals.set( - tokenTransfer.tokenId, - tokenTransfer.expectedDecimals, + for (const nftTransfer of props.nftTransfers) { + this._addNftTransfer( + nftTransfer.isApproved, + nftTransfer.tokenId, + nftTransfer.serialNumber, + nftTransfer.senderAccountId, + nftTransfer.receiverAccountId, ); } - }); - return tokenDecimals; - } - - /** - * @param {TokenId} tokenId - * @param {AccountId} accountId - * @param {Long} amount - */ - addTokenTransfer(tokenId, accountId, amount) { - const NON_APPROVED = false; - const tokenTransfer = new TokenTransfer({ - tokenId, - accountId: accountId, - amount: amount, - isApproved: NON_APPROVED, - expectedDecimals: null, - }); - this._tokenTransfers.push(tokenTransfer); - } - - /** - * - * @param {TokenId} tokenId - * @param {AccountId} accountId - * @param {Long} amount - * @returns {this} - */ - addApprovedTokenTransfer(tokenId, accountId, amount) { - const APPROVED = true; - const tokenTransfer = new TokenTransfer({ - tokenId, - accountId: accountId, - amount: amount, - isApproved: APPROVED, - expectedDecimals: null, - }); - this._tokenTransfers.push(tokenTransfer); - return this; + } } /** @@ -155,164 +79,15 @@ export default class AirdropTokenTransaction extends Transaction { amount, expectedDecimals, ) { - const IS_APPROVED = true; - const tokenTransfer = new TokenTransfer({ + this._requireNotFrozen(); + this._addTokenTransfer( tokenId, accountId, amount, - isApproved: IS_APPROVED, - expectedDecimals: expectedDecimals, - }); - this._tokenTransfers.push(tokenTransfer); - return this; - } - - /** - * - * @param {TokenId} tokenId - * @param {AccountId} accountId - * @param {Long} amount - * @param {number} expectedDecimals - * @returns {this} - */ - addTokenTransferWithDecimals(tokenId, accountId, amount, expectedDecimals) { - const IS_APPROVED = false; - const tokenTransfer = new TokenTransfer({ - tokenId, - accountId, - amount, - isApproved: IS_APPROVED, + true, expectedDecimals, - }); - this._tokenTransfers.push(tokenTransfer); - return this; - } - - /** - * @param {NftId} nftId - * @param {AccountId} senderAccountId - * @param {AccountId} receiverAccountId - */ - addNftTransfer(nftId, senderAccountId, receiverAccountId) { - const isApproved = false; - - const nftTransfer = new NftTransfer({ - tokenId: nftId.tokenId, - senderAccountId, - receiverAccountId, - serialNumber: nftId.serial, - isApproved, - }); - this._nftTransfers.push(nftTransfer); - } - - /** - * - * @param {NftId} nftId - * @param {AccountId} sender - * @param {AccountId} receiver - * @returns - */ - addApprovedNftTransfer(nftId, sender, receiver) { - const isApproved = true; - const nftTransfer = new NftTransfer({ - senderAccountId: sender, - receiverAccountId: receiver, - serialNumber: nftId.serial, - tokenId: nftId.tokenId, - isApproved, - }); - this._nftTransfers.push(nftTransfer); - return this; - } - - /** - * @param {TokenTransfer[]} tokenTransfers - * @returns {this} - */ - setTokenTransfers(tokenTransfers) { - this._tokenTransfers = tokenTransfers; - return this; - } - - /** - * @param {TokenTransfer[]} tokenTransferList - * @returns {this} - */ - addTokenTransferList(tokenTransferList) { - this._tokenTransfers.push(...tokenTransferList); - return this; - } - - /** - * @param {NftTransfer[]} nftTransfers - * @returns {this} - */ - setNftTransfers(nftTransfers) { - this._nftTransfers = nftTransfers; - return this; - } - - /** - * @returns {HashgraphProto.proto.ITokenAirdropTransactionBody} - */ - _makeTransactionData() { - /** - * @type {HashgraphProto.proto.AccountAmount[]} - */ - const tokenTransfers = this._tokenTransfers.map((tokenTransfer) => { - return { - accountId: tokenTransfer.accountId._toProtobuf(), - amount: tokenTransfer.amount, - isApproval: tokenTransfer.isApproved, - }; - }); - - /** - * @type {HashgraphProto.proto.NftTransfer[]} - */ - const nftTransfers = this._nftTransfers.map((nftTransfer) => { - return { - senderAccountId: nftTransfer.senderAccountId._toProtobuf(), - isApproval: nftTransfer.isApproved, - receiverAccountId: nftTransfer.receiverAccountId._toProtobuf(), - serialNumber: nftTransfer.serialNumber, - }; - }); - - const tokenTransferWithId = this._tokenTransfers.find( - (tokenTransfer) => { - return tokenTransfer.tokenId != null; - }, ); - const tokenId = tokenTransferWithId?.tokenId; - - const tokenTransferWithDecimals = this._tokenTransfers.find( - (tokenTransfer) => { - return tokenTransfer.expectedDecimals != null; - }, - ); - const expectedDecimals = tokenTransferWithDecimals?.expectedDecimals; - - if (tokenId == null) { - throw new Error("Token ID is required"); - } - - /** - * @type {HashgraphProto.proto.TokenTransferList} - */ - const tokenTransfersList = { - transfers: tokenTransfers, - nftTransfers: nftTransfers, - token: tokenId._toProtobuf(), - expectedDecimals: { - value: expectedDecimals, - }, - }; - - return { - tokenTransfers: [tokenTransfersList], - }; + return this; } /** 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/test/unit/AirdropTokenTransaction.js b/test/unit/AirdropTokenTransaction.js index 26e50524b..bff4dc8e4 100644 --- a/test/unit/AirdropTokenTransaction.js +++ b/test/unit/AirdropTokenTransaction.js @@ -2,146 +2,102 @@ import { expect } from "chai"; import { AccountId, AirdropTokenTransaction, + NftId, TokenId, } from "../../src/index.js"; -import TokenTransfer from "../../src/token/TokenTransfer.js"; -import AccountAmount from "../../src/token/AccountAmount.js"; -import TokenNftTransfer from "../../src/token/TokenNftTransfer.js"; - -describe("Transaction", function () { +describe("AirdropTokenTransaction", function () { it("from | toBytes", async function () { - const USER = new AccountId(0, 0, 100); - const TOKEN_ID = new TokenId(0, 0, 1); - const NFT_ID = 1; - const IS_APPROVAL = true; + 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; - - let accountAmount = new AccountAmount() - .setAccountId(USER) - .setIsApproval(IS_APPROVAL) - .setAmount(AMOUNT); - - let nftTransfer = new TokenNftTransfer({ - isApproved: true, - receiverAccountId: USER, - senderAccountId: USER, - serialNumber: NFT_ID, - tokenId: TOKEN_ID, - }); - - let tokenTransfer = new TokenTransfer() - .setAccountAmounts([accountAmount]) - .setTokenNftTransfers([nftTransfer]) - .setTokenId(TOKEN_ID) - .setExpectedDecimals(1); - - const transaction = new AirdropTokenTransaction().setTokenTransfers([ - tokenTransfer, - ]); + const EXPECTED_DECIMALS = 1; + + const transaction = new AirdropTokenTransaction() + .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 = AirdropTokenTransaction.fromBytes(txBytes); - expect(tx.tokenTransfers[0].tokenId).to.deep.equal(TOKEN_ID); + // 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 tests - expect(tx.tokenTransfers[0].expectedDecimals).to.equal(1); - expect(tx.tokenTransfers[0].tokenId).to.deep.equal(TOKEN_ID); - expect(tx.tokenTransfers.length).to.equal(1); + // 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); - // account amount tests - expect(tx.tokenTransfers[0].accountAmounts[0].accountId).to.deep.equal( - USER, - ); - expect( - tx.tokenTransfers[0].accountAmounts[0].amount.toInt(), - ).to.deep.equal(accountAmount.amount); - expect(tx.tokenTransfers[0].accountAmounts[0].isApproval).to.deep.equal( - accountAmount.isApproval, + expect(tokenTransferWithDecimals.expectedDecimals).to.equal( + EXPECTED_DECIMALS, ); - expect(tx.tokenTransfers[0].accountAmounts.length).to.equal(1); - // nft transfer tests - expect(tx.tokenTransfers[0].tokenNftTransfers[0].tokenId).to.deep.equal( - TOKEN_ID, + // 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, ); - expect( - tx.tokenTransfers[0].tokenNftTransfers[0].senderAccountId, - ).to.deep.equal(USER); - expect( - tx.tokenTransfers[0].tokenNftTransfers[0].receiverAccountId, - ).to.deep.equal(USER); - expect( - tx.tokenTransfers[0].tokenNftTransfers[0].serialNumber.toInt(), - ).to.deep.equal(NFT_ID); - expect( - tx.tokenTransfers[0].tokenNftTransfers[0].isApproved, - ).to.deep.equal(IS_APPROVAL); - expect(tx.tokenTransfers[0].tokenNftTransfers.length).to.equal(1); - }); - - describe("Token Transfer", function () { - let tokenTransfer; - - beforeEach(function () { - tokenTransfer = new TokenTransfer(); - }); - - it("should set token transfer", function () { - const ACCOUNT_AMOUNT = new AccountAmount(); - tokenTransfer.setAccountAmounts(ACCOUNT_AMOUNT); - expect(tokenTransfer.accountAmounts).to.equal(ACCOUNT_AMOUNT); - }); - - it("should set expected decimals", function () { - const EXPECTED_DECIMALS = 1; - tokenTransfer.setExpectedDecimals(EXPECTED_DECIMALS); - expect(tokenTransfer.expectedDecimals).to.equal(EXPECTED_DECIMALS); - }); - - it("should set token id", function () { - const TOKEN_ID = new TokenId(0, 0, 1); - tokenTransfer.setTokenId(TOKEN_ID); - }); - - it("should set token nft transfers", function () { - const TOKEN_NFT_TRANSFER = new TokenNftTransfer({ - isApproved: true, - receiverAccountId: new AccountId(0, 0, 100), - senderAccountId: new AccountId(0, 0, 100), - serialNumber: 1, - tokenId: new TokenId(0, 0, 1), - }); - tokenTransfer.setTokenNftTransfers([TOKEN_NFT_TRANSFER]); - expect(tokenTransfer.tokenNftTransfers).to.deep.equal([ - TOKEN_NFT_TRANSFER, - ]); - }); - }); - - describe("Account Amount", function () { - let accountAmount; - beforeEach(function () { - accountAmount = new AccountAmount(); - }); - - it("should set account id", function () { - const ACCOUNT_ID = new AccountId(0, 0, 100); - accountAmount.setAccountId(ACCOUNT_ID); - expect(accountAmount.accountId).to.deep.equal(ACCOUNT_ID); - }); - - it("should set amount", function () { - const AMOUNT = 1000; - accountAmount.setAmount(AMOUNT); - expect(accountAmount.amount).to.equal(AMOUNT); - }); - it("should set is approval", function () { - const IS_APPROVAL = true; - accountAmount.setIsApproval(IS_APPROVAL); - expect(accountAmount.isApproval).to.equal(IS_APPROVAL); - }); + // 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 6de1176f4471bcb014bc78bda53b352b403beed3 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 30 Aug 2024 02:45:34 +0300 Subject: [PATCH 14/33] feat: add logid and execute Signed-off-by: Ivaylo Nikolov --- src/token/AirdropTokenTransaction.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/token/AirdropTokenTransaction.js b/src/token/AirdropTokenTransaction.js index f697b38ac..7356a8bc4 100644 --- a/src/token/AirdropTokenTransaction.js +++ b/src/token/AirdropTokenTransaction.js @@ -14,9 +14,11 @@ import AbstractTokenTransfer from "./AbstractTokenTransfer.js"; * @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 @@ -132,6 +134,17 @@ export default class AirdropTokenTransaction extends AbstractTokenTransfer { ); } + /** + * @override + * @internal + * @param {Channel} channel + * @param {HashgraphProto.proto.ITransaction} request + * @returns {Promise} + */ + _execute(channel, request) { + return channel.token.airdropTokens(request); + } + /** * @override * @protected @@ -140,6 +153,16 @@ export default class AirdropTokenTransaction extends AbstractTokenTransfer { _getTransactionDataCase() { return "tokenAirdrop"; } + + /** + * @returns {string} + */ + _getLogId() { + const timestamp = /** @type {import("../Timestamp.js").default} */ ( + this._transactionIds.current.validStart + ); + return `TokenAirdropTransaction:${timestamp.toString()}`; + } } TRANSACTION_REGISTRY.set( From 8cd560689d7b099e62756e2e7c9147bfb8a2f43b Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 30 Aug 2024 02:45:57 +0300 Subject: [PATCH 15/33] feat: add integration tests Signed-off-by: Ivaylo Nikolov --- .../AirdropTokenIntegrationTest.js | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 test/integration/AirdropTokenIntegrationTest.js diff --git a/test/integration/AirdropTokenIntegrationTest.js b/test/integration/AirdropTokenIntegrationTest.js new file mode 100644 index 000000000..2e3858200 --- /dev/null +++ b/test/integration/AirdropTokenIntegrationTest.js @@ -0,0 +1,345 @@ +import { + AccountCreateTransaction, + AirdropTokenTransaction, + TokenCreateTransaction, + TokenMintTransaction, + TokenType, + PrivateKey, + NftId, + AccountBalanceQuery, + CustomFixedFee, + TokenAssociateTransaction, + TransferTransaction, + Hbar, + AccountId, + TokenId, +} from "../../src/exports.js"; +import IntegrationTestEnv from "./client/NodeIntegrationTestEnv.js"; + +describe("AccountId", function () { + let env; + const INITIAL_SUPPLY = 1000; + + before(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("-") + .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 AirdropTokenTransaction() + .addNftTransfer( + new NftId(nftTokenId, serials[0]), + env.operatorId, + receiverId, + ) + .addTokenTransfer(ftTokenId, receiverId, INITIAL_SUPPLY) + .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) + .execute(env.client); + + await transactionResponse.getReceipt(env.client); + + 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); + //await client.submitTokenAirdrop(transaction); + }); + + it("tokens should be in pending state when no automatic autoassociation", async function () {}); + + it("should create hollow account when airdropping tokens and transfers them", async function () { + 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 receiverPrivateKey = PrivateKey.generateED25519(); + const aliasAccountId = receiverPrivateKey.publicKey.toAccountId(0, 0); + + const airdropTokenResponse = await new AirdropTokenTransaction() + .addTokenTransfer(ftTokenId, aliasAccountId, INITIAL_SUPPLY) + .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) + .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); + }); + + 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, + ); + + 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]) + .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, + ) + .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 AirdropTokenTransaction() + .addTokenTransfer( + tokenWithFeeId, + receiverId, + INITIAL_SUPPLY, + ) + .addTokenTransfer( + tokenWithFeeId, + senderAccountId, + -INITIAL_SUPPLY, + ) + .freezeWith(env.client) + .sign(senderPrivateKey) + ).execute(env.client) + ).getReceipt(env.client); + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + + expect(operatorBalance.tokens.get(tokenWithFeeId).toInt()).to.be.eq(0); + expect(operatorBalance.tokens.get(feeTokenId).toInt()).to.be.eq(1); + + 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 - FEE_AMOUNT, + ); + }); + + it("should not 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 receiverPrivateKey = PrivateKey.generateED25519(); + const receiverPublicKey = receiverPrivateKey.publicKey; + const accountCreateResponse = await ( + 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 AirdropTokenTransaction() + .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) + .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) + .execute(env.client); + + await airdropTokenResponse.getReceipt(env.client); + } catch (error) { + if (error.message.includes("INVALID_SIGNATURE")) { + err = true; + } + } + + expect(err).to.be.eq(true); + }); + + it("should not airdrop with invalid tx body", async function () { + let err = false; + + try { + await ( + await new AirdropTokenTransaction().execute(env.client) + ).getReceipt(env.client); + } catch (error) { + // SHOULD IT FAIL WITH INVALID TX BODY + if (error.message.includes("FAIL_INVALID")) { + err = true; + } + } + expect(err).to.be.eq(true); + + err = false; + try { + await ( + await new AirdropTokenTransaction() + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .addTokenTransfer(new TokenId(1), new AccountId(1), 1) + .execute(env.client) + ).getReceipt(env.client); + } catch (error) { + if (error.message.includes("INVALID_TRANSACTION_BODY")) { + err = true; + } + } + expect(err).to.be.eq(true); + }); +}); From f41a54d06d9bbb40f89cd6b592c7751d2d9767cb Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 30 Aug 2024 15:34:15 +0300 Subject: [PATCH 16/33] feat: add pending airdrop to rectord Signed-off-by: Ivaylo Nikolov --- src/token/PendingAirdrop.js | 49 +++++++++++++ src/token/PendingAirdropId.js | 72 +++++++++++++++++++ src/transaction/TransactionRecord.js | 21 ++++++ .../AirdropTokenIntegrationTest.js | 72 +++++++++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 src/token/PendingAirdrop.js create mode 100644 src/token/PendingAirdropId.js diff --git a/src/token/PendingAirdrop.js b/src/token/PendingAirdrop.js new file mode 100644 index 000000000..7963f78ee --- /dev/null +++ b/src/token/PendingAirdrop.js @@ -0,0 +1,49 @@ +/** + * @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/PendingAirdropId.js b/src/token/PendingAirdropId.js new file mode 100644 index 000000000..db4e35c12 --- /dev/null +++ b/src/token/PendingAirdropId.js @@ -0,0 +1,72 @@ +/** + * @namespace proto + * @typedef {import("@hashgraph/proto").proto.PendingAirdropId} HashgraphProto.proto.PendingAirdropId + */ + +import { AccountId, NftId, TokenId, TokenType } from "../exports.js"; +import TokenReference from "../token/TokenReference.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 = props.senderId; + this.receiverId = props.receiverId; + 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, + }); + } + + /** + * @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/transaction/TransactionRecord.js b/src/transaction/TransactionRecord.js index fd256828d..bbc1f1720 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/PendingAirdrop.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/test/integration/AirdropTokenIntegrationTest.js b/test/integration/AirdropTokenIntegrationTest.js index 2e3858200..5af50aa9a 100644 --- a/test/integration/AirdropTokenIntegrationTest.js +++ b/test/integration/AirdropTokenIntegrationTest.js @@ -342,4 +342,76 @@ describe("AccountId", function () { } expect(err).to.be.eq(true); }); + it("tokens should be in pending state", 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 AirdropTokenTransaction() + .addTokenTransfer(ftTokenId, receiverId, INITIAL_SUPPLY) + .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) + .addNftTransfer(nftTokenId, serials[0], env.operatorId, receiverId) + .execute(env.client); + + await airdropTokenResponse.getReceipt(env.client); + + const airdropTokenRecord = await airdropTokenResponse.getRecord( + env.client, + ); + + expect(airdropTokenRecord.newPendingAirdrops.length).to.be.above(0); + + 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); + }); }); From 8b305af2faa507ca2173563ac6b8afe9e5102198 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 30 Aug 2024 15:34:50 +0300 Subject: [PATCH 17/33] test: add nft transfers to all test cases Signed-off-by: Ivaylo Nikolov --- .../AirdropTokenIntegrationTest.js | 273 ++++++++++++------ 1 file changed, 182 insertions(+), 91 deletions(-) diff --git a/test/integration/AirdropTokenIntegrationTest.js b/test/integration/AirdropTokenIntegrationTest.js index 5af50aa9a..604766064 100644 --- a/test/integration/AirdropTokenIntegrationTest.js +++ b/test/integration/AirdropTokenIntegrationTest.js @@ -16,11 +16,11 @@ import { } from "../../src/exports.js"; import IntegrationTestEnv from "./client/NodeIntegrationTestEnv.js"; -describe("AccountId", function () { +describe("AirdropTokenIntegrationTest", function () { let env; const INITIAL_SUPPLY = 1000; - before(async function () { + beforeEach(async function () { env = await IntegrationTestEnv.new(); }); @@ -53,7 +53,7 @@ describe("AccountId", function () { const mintResponse = await new TokenMintTransaction() .setTokenId(nftTokenId) - .addMetadata("-") + .addMetadata(Buffer.from("-")) .execute(env.client); const { serials } = await mintResponse.getReceipt(env.client); @@ -97,10 +97,81 @@ describe("AccountId", function () { expect(receiverBalance.tokens.get(nftTokenId).toInt()).to.be.eq(1); //await client.submitTokenAirdrop(transaction); }); + 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 AirdropTokenTransaction() + .addTokenTransfer(ftTokenId, receiverId, INITIAL_SUPPLY) + .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) + .addNftTransfer(nftTokenId, serials[0], env.operatorId, receiverId) + .execute(env.client); + + await airdropTokenResponse.getReceipt(env.client); + + const airdropTokenRecord = await airdropTokenResponse.getRecord( + env.client, + ); + + expect(airdropTokenRecord.newPendingAirdrops.length).to.be.above(0); + + const operatorBalance = await new AccountBalanceQuery() + .setAccountId(env.operatorId) + .execute(env.client); + const receiverBalance = await new AccountBalanceQuery() + .setAccountId(receiverId) + .execute(env.client); - it("tokens should be in pending state when no automatic autoassociation", async function () {}); + // 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); + }); 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") @@ -113,12 +184,36 @@ describe("AccountId", function () { 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 AirdropTokenTransaction() .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); @@ -134,6 +229,9 @@ describe("AccountId", function () { 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 () { @@ -180,6 +278,26 @@ describe("AccountId", function () { 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() @@ -193,7 +311,7 @@ describe("AccountId", function () { await ( await new TokenAssociateTransaction() .setAccountId(senderAccountId) - .setTokenIds([tokenWithFeeId, feeTokenId]) + .setTokenIds([tokenWithFeeId, feeTokenId, nftTokenId]) .freezeWith(env.client) .sign(senderPrivateKey) ).execute(env.client) @@ -211,6 +329,12 @@ describe("AccountId", function () { senderAccountId, INITIAL_SUPPLY, ) + .addNftTransfer( + nftTokenId, + serials[0], + env.operatorId, + senderAccountId, + ) .execute(env.client) ).getReceipt(env.client); @@ -234,6 +358,12 @@ describe("AccountId", function () { senderAccountId, -INITIAL_SUPPLY, ) + .addNftTransfer( + nftTokenId, + serials[0], + senderAccountId, + receiverId, + ) .freezeWith(env.client) .sign(senderPrivateKey) ).execute(env.client) @@ -243,8 +373,12 @@ describe("AccountId", function () { .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(1); + expect(operatorBalance.tokens.get(feeTokenId).toInt()).to.be.eq( + DISTINCT_TRANSACTIONS, + ); const receiverBalance = await new AccountBalanceQuery() .setAccountId(receiverId) @@ -258,8 +392,10 @@ describe("AccountId", function () { .execute(env.client); expect(senderBalance.tokens.get(tokenWithFeeId).toInt()).to.be.eq(0); expect(senderBalance.tokens.get(feeTokenId).toInt()).to.be.eq( - INITIAL_SUPPLY - FEE_AMOUNT, + 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 not airdrop with receiver sig set to true", async function () { @@ -273,10 +409,29 @@ describe("AccountId", function () { 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 await new AccountCreateTransaction() + await new AccountCreateTransaction() .setKey(receiverPublicKey) .setReceiverSignatureRequired(true) .freezeWith(env.client) @@ -291,6 +446,12 @@ describe("AccountId", function () { const airdropTokenResponse = await new AirdropTokenTransaction() .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); @@ -305,6 +466,8 @@ describe("AccountId", function () { 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 ( @@ -322,17 +485,17 @@ describe("AccountId", function () { try { await ( await new AirdropTokenTransaction() - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 1) - .addTokenTransfer(new TokenId(1), new AccountId(1), 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) + .addTokenTransfer(tokenId, accountId, 1) + .addNftTransfer(new NftId(tokenId, 1), accountId, accountId) .execute(env.client) ).getReceipt(env.client); } catch (error) { @@ -342,76 +505,4 @@ describe("AccountId", function () { } expect(err).to.be.eq(true); }); - it("tokens should be in pending state", 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 AirdropTokenTransaction() - .addTokenTransfer(ftTokenId, receiverId, INITIAL_SUPPLY) - .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) - .addNftTransfer(nftTokenId, serials[0], env.operatorId, receiverId) - .execute(env.client); - - await airdropTokenResponse.getReceipt(env.client); - - const airdropTokenRecord = await airdropTokenResponse.getRecord( - env.client, - ); - - expect(airdropTokenRecord.newPendingAirdrops.length).to.be.above(0); - - 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); - }); }); From fd7b686bdea83f71a05025b747f5f1a182bd6fa8 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 30 Aug 2024 15:36:51 +0300 Subject: [PATCH 18/33] chore: remove unused files for airdrop Signed-off-by: Ivaylo Nikolov --- src/token/AirdropNftTransfer.js | 119 ---------------------- src/token/AirdropTokenTransfer.js | 162 ------------------------------ 2 files changed, 281 deletions(-) delete mode 100644 src/token/AirdropNftTransfer.js delete mode 100644 src/token/AirdropTokenTransfer.js diff --git a/src/token/AirdropNftTransfer.js b/src/token/AirdropNftTransfer.js deleted file mode 100644 index 8604d9424..000000000 --- a/src/token/AirdropNftTransfer.js +++ /dev/null @@ -1,119 +0,0 @@ -/*- - * ‌ - * Hedera JavaScript SDK - * ​ - * Copyright (C) 2020 - 2023 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 Long from "long"; -import AccountId from "../account/AccountId.js"; - -/** - * @namespace proto - * @typedef {import("@hashgraph/proto").proto.ITokenTransferList} HashgraphProto.proto.ITokenTransferList - * @typedef {import("@hashgraph/proto").proto.IAccountAmount} HashgraphProto.proto.IAccountAmount - * @typedef {import("@hashgraph/proto").proto.INftTransfer} HashgraphProto.proto.INftTransfer - * @typedef {import("@hashgraph/proto").proto.IAccountID} HashgraphProto.proto.IAccountID - * @typedef {import("@hashgraph/proto").proto.ITokenID} HashgraphProto.proto.ITokenID - */ - -/** - * @typedef {import("bignumber.js").default} BigNumber - */ - -/** - * An account, and the amount that it sends or receives during a cryptocurrency tokentransfer. - */ -export default class NftTransfer { - /** - * @internal - * @param {object} props - * @param {AccountId | string} props.senderAccountId - * @param {AccountId | string} props.receiverAccountId - * @param {Long | number} props.serialNumber - * @param {boolean} props.isApproved - */ - constructor(props) { - /** - * The Account ID that sends or receives cryptocurrency. - */ - this.senderAccountId = - props.senderAccountId instanceof AccountId - ? props.senderAccountId - : AccountId.fromString(props.senderAccountId); - - /** - * The Account ID that sends or receives cryptocurrency. - */ - this.receiverAccountId = - props.receiverAccountId instanceof AccountId - ? props.receiverAccountId - : AccountId.fromString(props.receiverAccountId); - - this.serialNumber = Long.fromValue(props.serialNumber); - this.isApproved = props.isApproved; - } - - /** - * @internal - * @param {HashgraphProto.proto.ITokenTransferList[]} tokenTransfers - * @returns {NftTransfer[]} - */ - static _fromProtobuf(tokenTransfers) { - const transfers = []; - - for (const tokenTransfer of tokenTransfers) { - for (const transfer of tokenTransfer.nftTransfers != null - ? tokenTransfer.nftTransfers - : []) { - transfers.push( - new NftTransfer({ - senderAccountId: AccountId._fromProtobuf( - /** @type {HashgraphProto.proto.IAccountID} */ ( - transfer.senderAccountID - ), - ), - receiverAccountId: AccountId._fromProtobuf( - /** @type {HashgraphProto.proto.IAccountID} */ ( - transfer.receiverAccountID - ), - ), - serialNumber: - transfer.serialNumber != null - ? transfer.serialNumber - : Long.ZERO, - isApproved: transfer.isApproval == true, - }), - ); - } - } - - return transfers; - } - - /** - * @internal - * @returns {HashgraphProto.proto.INftTransfer} - */ - _toProtobuf() { - return { - senderAccountID: this.senderAccountId._toProtobuf(), - receiverAccountID: this.receiverAccountId._toProtobuf(), - serialNumber: this.serialNumber, - isApproval: this.isApproved, - }; - } -} diff --git a/src/token/AirdropTokenTransfer.js b/src/token/AirdropTokenTransfer.js deleted file mode 100644 index 2d2edf80e..000000000 --- a/src/token/AirdropTokenTransfer.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @namespace proto - * @typedef {import("@hashgraph/proto").proto.ITokenTransferList} HashgraphProto.proto.ITokenTransferList - */ -import AccountAmount from "./AccountAmount.js"; -import TokenId from "./TokenId.js"; -import TokenNftTransfer from "./AirdropNftTransfer.js"; - -export default class TokenTransfer { - /** - * @param {object} props - * @param {TokenId} [props.tokenId] - * @param {AccountAmount[]} [props.accountAmounts] - * @param {TokenNftTransfer[]} [props.tokenNftTransfers] - * @param {number} [props.expectedDecimals] - */ - constructor(props = {}) { - /** - * @private - * @type {?TokenId} - */ - this._tokenId = null; - - /** - * @private - * @type {AccountAmount[]} - */ - this._accountAmounts = []; - - /** - * @private - * @type {TokenNftTransfer[]} - */ - this._tokenNftTransfers = []; - - /** - * @private - * @type {?number} - */ - this._expectedDecimals = null; - - if (props.tokenId != null) { - this.setTokenId(props.tokenId); - } - - if (props.accountAmounts != null) { - this.setAccountAmounts(props.accountAmounts); - } - - if (props.tokenNftTransfers != null) { - this.setTokenNftTransfers(props.tokenNftTransfers); - } - - if (props.expectedDecimals != null) { - this.setExpectedDecimals(props.expectedDecimals); - } - } - - /** - * @returns {?TokenId} - */ - get tokenId() { - return this._tokenId; - } - - /** - * @param {TokenId} tokenId - * @returns {this} - */ - setTokenId(tokenId) { - this._tokenId = tokenId; - return this; - } - - /** - * @returns {AccountAmount[]} - */ - get accountAmounts() { - return this._accountAmounts; - } - - /** - * @param {AccountAmount[]} accountAmounts - * @returns {this} - */ - setAccountAmounts(accountAmounts) { - this._accountAmounts = accountAmounts; - return this; - } - - /** - * @returns {TokenNftTransfer[]} - */ - get tokenNftTransfers() { - return this._tokenNftTransfers; - } - - /** - * - * @param {TokenNftTransfer[]} tokenNftTransfers - * @returns {this} - */ - setTokenNftTransfers(tokenNftTransfers) { - this._tokenNftTransfers = tokenNftTransfers; - return this; - } - - /** - * @returns {?number} - */ - get expectedDecimals() { - return this._expectedDecimals; - } - - /** - * - * @param {number} expectedDecimals - * @returns {this} - */ - setExpectedDecimals(expectedDecimals) { - this._expectedDecimals = expectedDecimals; - return this; - } - - /** - * @returns {HashgraphProto.proto.ITokenTransferList} - */ - _toProtobuf() { - return { - token: this._tokenId != null ? this._tokenId._toProtobuf() : null, - transfers: this._accountAmounts.map((accountAmount) => - accountAmount._toProtobuf(), - ), - nftTransfers: this._tokenNftTransfers.map((tokenNftTransfer) => - tokenNftTransfer._toProtobuf(), - ), - expectedDecimals: - this._expectedDecimals != null - ? { value: this._expectedDecimals } - : null, - }; - } - - /** - * @param {HashgraphProto.proto.ITokenTransferList} tokenTransfer - * @returns {TokenTransfer} - */ - static _fromProtobuf(tokenTransfer) { - return new TokenTransfer({ - tokenId: - tokenTransfer.token != null - ? TokenId._fromProtobuf(tokenTransfer.token) - : undefined, - accountAmounts: tokenTransfer.transfers?.map((transfer) => - AccountAmount._fromProtobuf(transfer), - ), - tokenNftTransfers: TokenNftTransfer._fromProtobuf([tokenTransfer]), - expectedDecimals: - tokenTransfer.expectedDecimals?.value || undefined, - }); - } -} From 43f6175c4308476964edb041fc320407a20066f6 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 30 Aug 2024 15:52:55 +0300 Subject: [PATCH 19/33] refactor: remove circular dependancy and unused import Signed-off-by: Ivaylo Nikolov --- src/token/PendingAirdropId.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/token/PendingAirdropId.js b/src/token/PendingAirdropId.js index db4e35c12..8d5f79261 100644 --- a/src/token/PendingAirdropId.js +++ b/src/token/PendingAirdropId.js @@ -3,8 +3,9 @@ * @typedef {import("@hashgraph/proto").proto.PendingAirdropId} HashgraphProto.proto.PendingAirdropId */ -import { AccountId, NftId, TokenId, TokenType } from "../exports.js"; -import TokenReference from "../token/TokenReference.js"; +import AccountId from "../account/AccountId.js"; +import TokenId from "./TokenId.js"; +import NftId from "./NftId.js"; export default class PendingAirdropId { /** From 778b7421faf6304e47e8194b2f49d817a7f18a6d Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 30 Aug 2024 15:54:56 +0300 Subject: [PATCH 20/33] refactor: remove duplicated property Signed-off-by: Ivaylo Nikolov --- src/token/AirdropTokenTransaction.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/token/AirdropTokenTransaction.js b/src/token/AirdropTokenTransaction.js index 7356a8bc4..88752b4e8 100644 --- a/src/token/AirdropTokenTransaction.js +++ b/src/token/AirdropTokenTransaction.js @@ -32,11 +32,6 @@ export default class AirdropTokenTransaction extends AbstractTokenTransfer { */ constructor(props = {}) { super(); - /** - * @private - * @type {TokenTransfer[]} - */ - this._tokenTransfers = []; if (props.tokenTransfers != null) { for (const tokenTransfer of props.tokenTransfers) { From ec04f31c3fb807484d8ea3acb397e88260529b0e Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 3 Sep 2024 01:35:06 +0300 Subject: [PATCH 21/33] refactor: rename pendngairdroprecord Signed-off-by: Ivaylo Nikolov --- src/token/{PendingAirdrop.js => PendingAirdropRecord.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/token/{PendingAirdrop.js => PendingAirdropRecord.js} (100%) diff --git a/src/token/PendingAirdrop.js b/src/token/PendingAirdropRecord.js similarity index 100% rename from src/token/PendingAirdrop.js rename to src/token/PendingAirdropRecord.js From dda7ebcbd26dffdf2fa37ac9a43fb0ac295c7e1e Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 3 Sep 2024 01:38:57 +0300 Subject: [PATCH 22/33] refactor: remove unused files Signed-off-by: Ivaylo Nikolov --- src/token/AccountAmount.js | 106 ------------------------------------- 1 file changed, 106 deletions(-) delete mode 100644 src/token/AccountAmount.js diff --git a/src/token/AccountAmount.js b/src/token/AccountAmount.js deleted file mode 100644 index d2698c3da..000000000 --- a/src/token/AccountAmount.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @namespace proto - * @typedef {import("@hashgraph/proto").proto.IAccountAmount} Hashgraph.proto.IAccountAmount - */ - -import Long from "long"; -import AccountId from "../account/AccountId.js"; - -export default class AccountAmount { - /** - * @param {object} props - * @param {AccountId} [props.accountId] - * @param {Long} [props.amount] - * @param {boolean} [props.isApproval] - */ - constructor(props = {}) { - this._accountId = null; - this._amount = Long.ZERO; - this._isApproval = null; - - if (props.accountId != null) { - this.setAccountId(props.accountId); - } - if (props.amount != null) { - this.setAmount(props.amount); - } - if (props.isApproval != null) { - this.setIsApproval(props.isApproval); - } - } - - /** - * @returns {?AccountId} - */ - get accountId() { - return this._accountId; - } - - /** - * @param {AccountId} accountId - * @returns {this} - */ - setAccountId(accountId) { - this._accountId = accountId; - return this; - } - - /** - * @returns {Long} - */ - get amount() { - return this._amount; - } - - /** - * @param {Long} amount - * @returns {this} - */ - setAmount(amount) { - this._amount = amount; - return this; - } - - /** - * @returns {boolean?} - */ - get isApproval() { - return this._isApproval; - } - - /** - * @param {boolean} isApproval - * @returns {this} - */ - setIsApproval(isApproval) { - this._isApproval = isApproval; - return this; - } - - /** - * @returns {Hashgraph.proto.IAccountAmount} - */ - _toProtobuf() { - return { - accountID: - this._accountId != null ? this._accountId._toProtobuf() : null, - amount: this._amount, - isApproval: this._isApproval != null ? this._isApproval : false, - }; - } - - /** - * @param {Hashgraph.proto.IAccountAmount} pb - * @returns {AccountAmount} - */ - static _fromProtobuf(pb) { - return new AccountAmount({ - accountId: - pb.accountID != null - ? AccountId._fromProtobuf(pb.accountID) - : undefined, - amount: pb.amount != null ? pb.amount : Long.ZERO, - isApproval: pb.isApproval != null ? pb.isApproval : false, - }); - } -} From fc6ded6c3a95183139a0dcfd5af6c86462a3c587 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 3 Sep 2024 02:00:09 +0300 Subject: [PATCH 23/33] refactor: rename AirdropTokenTransaction Signed-off-by: Ivaylo Nikolov --- src/exports.js | 2 +- ...ansaction.js => TokenAirdropTransaction.js} | 8 ++++---- ...nTest.js => TokenAirdropIntegrationTest.js} | 18 +++++++++--------- ...ansaction.js => TokenAirdropTransaction.js} | 8 ++++---- 4 files changed, 18 insertions(+), 18 deletions(-) rename src/token/{AirdropTokenTransaction.js => TokenAirdropTransaction.js} (96%) rename test/integration/{AirdropTokenIntegrationTest.js => TokenAirdropIntegrationTest.js} (97%) rename test/unit/{AirdropTokenTransaction.js => TokenAirdropTransaction.js} (95%) diff --git a/src/exports.js b/src/exports.js index 27beab2bb..431a17815 100644 --- a/src/exports.js +++ b/src/exports.js @@ -33,7 +33,7 @@ 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 AirdropTokenTransaction } from "./token/AirdropTokenTransaction.js"; +export { default as TokenAirdropTransaction } from "./token/TokenAirdropTransaction.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/AirdropTokenTransaction.js b/src/token/TokenAirdropTransaction.js similarity index 96% rename from src/token/AirdropTokenTransaction.js rename to src/token/TokenAirdropTransaction.js index 88752b4e8..0eca1eefa 100644 --- a/src/token/AirdropTokenTransaction.js +++ b/src/token/TokenAirdropTransaction.js @@ -24,7 +24,7 @@ import AbstractTokenTransfer from "./AbstractTokenTransfer.js"; * @typedef {import("./NftId.js").default} NftId * @typedef {import("./TokenId.js").default} TokenId */ -export default class AirdropTokenTransaction extends AbstractTokenTransfer { +export default class TokenAirdropTransaction extends AbstractTokenTransfer { /** * @param {object} props * @param {TokenTransfer[]} [props.tokenTransfers] @@ -94,7 +94,7 @@ export default class AirdropTokenTransaction extends AbstractTokenTransfer { * @param {TransactionId[]} transactionIds * @param {AccountId[]} nodeIds * @param {HashgraphProto.proto.ITransactionBody[]} bodies - * @returns {AirdropTokenTransaction} + * @returns {TokenAirdropTransaction} */ static _fromProtobuf( transactions, @@ -117,7 +117,7 @@ export default class AirdropTokenTransaction extends AbstractTokenTransfer { ); return Transaction._fromProtobufTransactions( - new AirdropTokenTransaction({ + new TokenAirdropTransaction({ nftTransfers: nftTransfers, tokenTransfers: tokenTransfers, }), @@ -163,5 +163,5 @@ export default class AirdropTokenTransaction extends AbstractTokenTransfer { TRANSACTION_REGISTRY.set( "tokenAirdrop", // eslint-disable-next-line @typescript-eslint/unbound-method - AirdropTokenTransaction._fromProtobuf, + TokenAirdropTransaction._fromProtobuf, ); diff --git a/test/integration/AirdropTokenIntegrationTest.js b/test/integration/TokenAirdropIntegrationTest.js similarity index 97% rename from test/integration/AirdropTokenIntegrationTest.js rename to test/integration/TokenAirdropIntegrationTest.js index 604766064..c53bd0ffc 100644 --- a/test/integration/AirdropTokenIntegrationTest.js +++ b/test/integration/TokenAirdropIntegrationTest.js @@ -1,6 +1,6 @@ import { AccountCreateTransaction, - AirdropTokenTransaction, + TokenAirdropTransaction, TokenCreateTransaction, TokenMintTransaction, TokenType, @@ -16,7 +16,7 @@ import { } from "../../src/exports.js"; import IntegrationTestEnv from "./client/NodeIntegrationTestEnv.js"; -describe("AirdropTokenIntegrationTest", function () { +describe("TokenAirdropIntegrationTest", function () { let env; const INITIAL_SUPPLY = 1000; @@ -68,7 +68,7 @@ describe("AirdropTokenIntegrationTest", function () { await accountCreateResponse.getReceipt(env.client); // airdrop the tokens - const transactionResponse = await new AirdropTokenTransaction() + const transactionResponse = await new TokenAirdropTransaction() .addNftTransfer( new NftId(nftTokenId, serials[0]), env.operatorId, @@ -138,7 +138,7 @@ describe("AirdropTokenIntegrationTest", function () { const { accountId: receiverId } = await accountCreateResponse.getReceipt(env.client); - const airdropTokenResponse = await new AirdropTokenTransaction() + const airdropTokenResponse = await new TokenAirdropTransaction() .addTokenTransfer(ftTokenId, receiverId, INITIAL_SUPPLY) .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) .addNftTransfer(nftTokenId, serials[0], env.operatorId, receiverId) @@ -205,7 +205,7 @@ describe("AirdropTokenIntegrationTest", function () { const receiverPrivateKey = PrivateKey.generateED25519(); const aliasAccountId = receiverPrivateKey.publicKey.toAccountId(0, 0); - const airdropTokenResponse = await new AirdropTokenTransaction() + const airdropTokenResponse = await new TokenAirdropTransaction() .addTokenTransfer(ftTokenId, aliasAccountId, INITIAL_SUPPLY) .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) .addNftTransfer( @@ -347,7 +347,7 @@ describe("AirdropTokenIntegrationTest", function () { await ( await ( - await new AirdropTokenTransaction() + await new TokenAirdropTransaction() .addTokenTransfer( tokenWithFeeId, receiverId, @@ -443,7 +443,7 @@ describe("AirdropTokenIntegrationTest", function () { let err = false; try { - const airdropTokenResponse = await new AirdropTokenTransaction() + const airdropTokenResponse = await new TokenAirdropTransaction() .addTokenTransfer(tokenId, receiverId, INITIAL_SUPPLY) .addTokenTransfer(tokenId, env.operatorId, -INITIAL_SUPPLY) .addNftTransfer( @@ -471,7 +471,7 @@ describe("AirdropTokenIntegrationTest", function () { try { await ( - await new AirdropTokenTransaction().execute(env.client) + await new TokenAirdropTransaction().execute(env.client) ).getReceipt(env.client); } catch (error) { // SHOULD IT FAIL WITH INVALID TX BODY @@ -484,7 +484,7 @@ describe("AirdropTokenIntegrationTest", function () { err = false; try { await ( - await new AirdropTokenTransaction() + await new TokenAirdropTransaction() .addTokenTransfer(tokenId, accountId, 1) .addTokenTransfer(tokenId, accountId, 1) .addTokenTransfer(tokenId, accountId, 1) diff --git a/test/unit/AirdropTokenTransaction.js b/test/unit/TokenAirdropTransaction.js similarity index 95% rename from test/unit/AirdropTokenTransaction.js rename to test/unit/TokenAirdropTransaction.js index bff4dc8e4..70d1f944b 100644 --- a/test/unit/AirdropTokenTransaction.js +++ b/test/unit/TokenAirdropTransaction.js @@ -1,12 +1,12 @@ import { expect } from "chai"; import { AccountId, - AirdropTokenTransaction, + TokenAirdropTransaction, NftId, TokenId, } from "../../src/index.js"; -describe("AirdropTokenTransaction", function () { +describe("TokenAirdropTransaction", function () { it("from | toBytes", async function () { const SENDER = new AccountId(0, 0, 100); const RECEIVER = new AccountId(0, 0, 101); @@ -23,7 +23,7 @@ describe("AirdropTokenTransaction", function () { const AMOUNT = 1000; const EXPECTED_DECIMALS = 1; - const transaction = new AirdropTokenTransaction() + const transaction = new TokenAirdropTransaction() .addTokenTransfer(TOKEN_IDS[0], SENDER, AMOUNT) .addTokenTransferWithDecimals( TOKEN_IDS[1], @@ -42,7 +42,7 @@ describe("AirdropTokenTransaction", function () { .addApprovedNftTransfer(NFT_IDS[1], SENDER, RECEIVER); const txBytes = transaction.toBytes(); - const tx = AirdropTokenTransaction.fromBytes(txBytes); + const tx = TokenAirdropTransaction.fromBytes(txBytes); // normal token transfer const tokenNormalTransfer = tx._tokenTransfers[0]; From 76fe0e1e584ae3bb676d4b37e0408ad03ac88612 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 3 Sep 2024 02:06:51 +0300 Subject: [PATCH 24/33] test: add additional tests Signed-off-by: Ivaylo Nikolov --- .../TokenAirdropIntegrationTest.js | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/test/integration/TokenAirdropIntegrationTest.js b/test/integration/TokenAirdropIntegrationTest.js index c53bd0ffc..efbebe669 100644 --- a/test/integration/TokenAirdropIntegrationTest.js +++ b/test/integration/TokenAirdropIntegrationTest.js @@ -97,6 +97,7 @@ describe("TokenAirdropIntegrationTest", function () { expect(receiverBalance.tokens.get(nftTokenId).toInt()).to.be.eq(1); //await client.submitTokenAirdrop(transaction); }); + it("tokens should be in pending state when no automatic association", async function () { this.timeout(1200000); const ftCreateResponse = await new TokenCreateTransaction() @@ -150,7 +151,7 @@ describe("TokenAirdropIntegrationTest", function () { env.client, ); - expect(airdropTokenRecord.newPendingAirdrops.length).to.be.above(0); + const { newPendingAirdrops } = airdropTokenRecord; const operatorBalance = await new AccountBalanceQuery() .setAccountId(env.operatorId) @@ -168,6 +169,32 @@ describe("TokenAirdropIntegrationTest", function () { // 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(undefined); + + expect(newPendingAirdrops[1].airdropId.senderId).deep.equal( + env.operatorId, + ); + expect(newPendingAirdrops[1].airdropId.receiverId).deep.equal( + receiverId, + ); + expect(newPendingAirdrops[1].airdropId.tokenId).deep.equal(undefined); + expect(newPendingAirdrops[1].airdropId.nftId.tokenId).to.deep.equal( + nftTokenId, + ); + expect(newPendingAirdrops[1].airdropId.nftId.serial).to.deep.equal( + serials[0], + ); + // expect(newPendingAirdrops[0]); }); it("should create hollow account when airdropping tokens and transfers them", async function () { From e540dd1d5791c87bf9d407bdc5baa83a44011bb2 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 3 Sep 2024 02:07:20 +0300 Subject: [PATCH 25/33] fix: rename file reference Signed-off-by: Ivaylo Nikolov --- src/transaction/TransactionRecord.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transaction/TransactionRecord.js b/src/transaction/TransactionRecord.js index bbc1f1720..fe3c78211 100644 --- a/src/transaction/TransactionRecord.js +++ b/src/transaction/TransactionRecord.js @@ -35,7 +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/PendingAirdrop.js"; +import PendingAirdropRecord from "../token/PendingAirdropRecord.js"; /** * @typedef {import("../token/TokenId.js").default} TokenId From df64cfc53c121ed8ffaf2230f41074df4dff3997 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 3 Sep 2024 02:09:50 +0300 Subject: [PATCH 26/33] test: check if newPendingAirdrops is empty for auto associated test Signed-off-by: Ivaylo Nikolov --- test/integration/TokenAirdropIntegrationTest.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/integration/TokenAirdropIntegrationTest.js b/test/integration/TokenAirdropIntegrationTest.js index efbebe669..49aeec8d5 100644 --- a/test/integration/TokenAirdropIntegrationTest.js +++ b/test/integration/TokenAirdropIntegrationTest.js @@ -78,7 +78,10 @@ describe("TokenAirdropIntegrationTest", function () { .addTokenTransfer(ftTokenId, env.operatorId, -INITIAL_SUPPLY) .execute(env.client); - await transactionResponse.getReceipt(env.client); + const { newPendingAirdrops } = await transactionResponse.getRecord( + env.client, + ); + expect(newPendingAirdrops.length).to.be.eq(0); const operatorBalance = await new AccountBalanceQuery() .setAccountId(env.operatorId) From 72a0838f934f5574ca44056219940239c99fc5da Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Wed, 4 Sep 2024 13:49:09 +0300 Subject: [PATCH 27/33] refactor: remove comment lines Signed-off-by: Ivaylo Nikolov --- test/integration/TokenAirdropIntegrationTest.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/TokenAirdropIntegrationTest.js b/test/integration/TokenAirdropIntegrationTest.js index 49aeec8d5..77a4f52ca 100644 --- a/test/integration/TokenAirdropIntegrationTest.js +++ b/test/integration/TokenAirdropIntegrationTest.js @@ -98,7 +98,6 @@ describe("TokenAirdropIntegrationTest", function () { expect(operatorBalance.tokens.get(nftTokenId).toInt()).to.be.eq(0); expect(receiverBalance.tokens.get(nftTokenId).toInt()).to.be.eq(1); - //await client.submitTokenAirdrop(transaction); }); it("tokens should be in pending state when no automatic association", async function () { @@ -197,7 +196,6 @@ describe("TokenAirdropIntegrationTest", function () { expect(newPendingAirdrops[1].airdropId.nftId.serial).to.deep.equal( serials[0], ); - // expect(newPendingAirdrops[0]); }); it("should create hollow account when airdropping tokens and transfers them", async function () { From e84809d8bd97a834aaf20a4a8e6692f7f2a59bf7 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Wed, 4 Sep 2024 13:53:51 +0300 Subject: [PATCH 28/33] test: remove get receipt line because we call get record Signed-off-by: Ivaylo Nikolov --- test/integration/TokenAirdropIntegrationTest.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/integration/TokenAirdropIntegrationTest.js b/test/integration/TokenAirdropIntegrationTest.js index 77a4f52ca..97931a663 100644 --- a/test/integration/TokenAirdropIntegrationTest.js +++ b/test/integration/TokenAirdropIntegrationTest.js @@ -147,8 +147,6 @@ describe("TokenAirdropIntegrationTest", function () { .addNftTransfer(nftTokenId, serials[0], env.operatorId, receiverId) .execute(env.client); - await airdropTokenResponse.getReceipt(env.client); - const airdropTokenRecord = await airdropTokenResponse.getRecord( env.client, ); @@ -502,8 +500,7 @@ describe("TokenAirdropIntegrationTest", function () { await new TokenAirdropTransaction().execute(env.client) ).getReceipt(env.client); } catch (error) { - // SHOULD IT FAIL WITH INVALID TX BODY - if (error.message.includes("FAIL_INVALID")) { + if (error.message.includes("EMPTY_TOKEN_TRANSFER_BODY")) { err = true; } } From 33d4a66b32ee2bd891b9c0c2709ddf667f99f11d Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Wed, 4 Sep 2024 13:58:54 +0300 Subject: [PATCH 29/33] test: should be able to airdrop when receiver sig set to true Signed-off-by: Ivaylo Nikolov --- test/integration/TokenAirdropIntegrationTest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/TokenAirdropIntegrationTest.js b/test/integration/TokenAirdropIntegrationTest.js index 97931a663..47de10d77 100644 --- a/test/integration/TokenAirdropIntegrationTest.js +++ b/test/integration/TokenAirdropIntegrationTest.js @@ -424,7 +424,7 @@ describe("TokenAirdropIntegrationTest", function () { expect(receiverBalance.tokens.get(nftTokenId).toInt()).to.be.eq(1); }); - it("should not airdrop with receiver sig set to true", async function () { + it("should airdrop with receiver sig set to true", async function () { this.timeout(1200000); const tokenCreateResponse = await new TokenCreateTransaction() .setTokenName("FFFFFFFFFF") @@ -487,7 +487,7 @@ describe("TokenAirdropIntegrationTest", function () { } } - expect(err).to.be.eq(true); + expect(err).to.be.eq(false); }); it("should not airdrop with invalid tx body", async function () { From 394ace80b59375dcecfbbf587d10f38e53a13db6 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Tue, 10 Sep 2024 02:37:20 +0300 Subject: [PATCH 30/33] refactor: rename AbstractTokenTransfer Signed-off-by: Ivaylo Nikolov --- src/account/TransferTransaction.js | 4 ++-- ...ctTokenTransfer.js => AbstractTokenTransferTransaction.js} | 2 +- src/token/TokenAirdropTransaction.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/token/{AbstractTokenTransfer.js => AbstractTokenTransferTransaction.js} (99%) diff --git a/src/account/TransferTransaction.js b/src/account/TransferTransaction.js index c1b0ef62e..ac07e0769 100644 --- a/src/account/TransferTransaction.js +++ b/src/account/TransferTransaction.js @@ -29,7 +29,7 @@ import TokenTransfer from "../token/TokenTransfer.js"; import HbarTransferMap from "./HbarTransferMap.js"; import TokenNftTransfer from "../token/TokenNftTransfer.js"; import NftId from "../token/NftId.js"; -import AbstractTokenTransfer from "../token/AbstractTokenTransfer.js"; +import AbstractTokenTransferTransaction from "../token/AbstractTokenTransferTransaction.js"; /** * @typedef {import("../long.js").LongObject} LongObject @@ -83,7 +83,7 @@ import AbstractTokenTransfer from "../token/AbstractTokenTransfer.js"; /** * Transfers a new Hedera™ crypto-currency token. */ -export default class TransferTransaction extends AbstractTokenTransfer { +export default class TransferTransaction extends AbstractTokenTransferTransaction { /** * @param {object} [props] * @param {(TransferTokensInput)[]} [props.tokenTransfers] diff --git a/src/token/AbstractTokenTransfer.js b/src/token/AbstractTokenTransferTransaction.js similarity index 99% rename from src/token/AbstractTokenTransfer.js rename to src/token/AbstractTokenTransferTransaction.js index ad8b7e7a0..9c17dfbee 100644 --- a/src/token/AbstractTokenTransfer.js +++ b/src/token/AbstractTokenTransferTransaction.js @@ -28,7 +28,7 @@ import TokenNftTransferMap from "../account/TokenNftTransferMap.js"; * @property {Long | number} serial */ -export default class AbstractTokenTransfer extends Transaction { +export default class AbstractTokenTransferTransaction extends Transaction { /** * @param {object} [props] * @param {(TransferTokensInput)[]} [props.tokenTransfers] diff --git a/src/token/TokenAirdropTransaction.js b/src/token/TokenAirdropTransaction.js index 0eca1eefa..fea6a811a 100644 --- a/src/token/TokenAirdropTransaction.js +++ b/src/token/TokenAirdropTransaction.js @@ -3,7 +3,7 @@ import Transaction, { } from "../transaction/Transaction.js"; import TokenTransfer from "./TokenTransfer.js"; import NftTransfer from "./TokenNftTransfer.js"; -import AbstractTokenTransfer from "./AbstractTokenTransfer.js"; +import AbstractTokenTransferTransaction from "./AbstractTokenTransferTransaction.js"; /** * @namespace proto @@ -24,7 +24,7 @@ import AbstractTokenTransfer from "./AbstractTokenTransfer.js"; * @typedef {import("./NftId.js").default} NftId * @typedef {import("./TokenId.js").default} TokenId */ -export default class TokenAirdropTransaction extends AbstractTokenTransfer { +export default class TokenAirdropTransaction extends AbstractTokenTransferTransaction { /** * @param {object} props * @param {TokenTransfer[]} [props.tokenTransfers] From f592ba85e1afcbccdb4d18538a9d0a6df52b59d8 Mon Sep 17 00:00:00 2001 From: ivaylonikolov7 Date: Wed, 11 Sep 2024 16:16:24 +0300 Subject: [PATCH 31/33] feat: Token Claim and Cancel Transaction (#2499) * feat: add airdrop claim and cancel transactions Signed-off-by: Ivaylo Nikolov * chore: update protobufs Signed-off-by: Ivaylo Nikolov * feat: update commit messages Signed-off-by: Ivaylo Nikolov * test: add unit tests for cancel and claim Signed-off-by: Ivaylo Nikolov * wip(test): add integration tests for cancel and claim transactions Signed-off-by: Ivaylo Nikolov * fix: claim used the wrong channel function Signed-off-by: Ivaylo Nikolov * refactor: rename transaction name Signed-off-by: Ivaylo Nikolov * refactor: remove claim references in airdropcancel integration test Signed-off-by: Ivaylo Nikolov * test(fix): fix not working test for airdrop cancel and claim Signed-off-by: Ivaylo Nikolov * fix: airdropcancel transaction should work on sender not receiver Signed-off-by: Ivaylo Nikolov * feat: add token airdrop example Signed-off-by: Ivaylo Nikolov * refactor: rename transactions to have the same name as java Signed-off-by: Ivaylo Nikolov * refactor: remove redundant imports and empty lines Signed-off-by: Ivaylo Nikolov * refactor: rename transaction tests to have the same name as java sdk Signed-off-by: Ivaylo Nikolov * docs: add licenses Signed-off-by: Ivaylo Nikolov * fix: remove dead code from token cancel Signed-off-by: Ivaylo Nikolov * refactor: consistency in constructor Signed-off-by: Ivaylo Nikolov * refactor: change airdrop_supply_per_person naming Signed-off-by: Ivaylo Nikolov * test: fix setting sender id Signed-off-by: Ivaylo Nikolov * refactor: specify what kind of tokens are airdropped Signed-off-by: Ivaylo Nikolov --------- Signed-off-by: Ivaylo Nikolov --- examples/token-airdrop-example.js | 419 ++++++++++++ packages/proto/src/proto | 2 +- src/Status.js | 47 ++ src/exports.js | 2 + src/token/AbstractTokenTransferTransaction.js | 20 + src/token/AirdropPendingTransaction.js | 77 +++ src/token/PendingAirdropId.js | 114 +++- src/token/PendingAirdropRecord.js | 20 + src/token/TokenAirdropTransaction.js | 20 + src/token/TokenCancelAirdropTransaction.js | 133 ++++ src/token/TokenClaimAirdropTransaction.js | 135 ++++ .../TokenCancelAirdropTransaction.js | 586 +++++++++++++++++ .../TokenClaimAirdropTransaction.js | 606 ++++++++++++++++++ test/unit/AirdropCancelTransaction.js | 23 + test/unit/AirdropClaimTransaction.js | 21 + 15 files changed, 2214 insertions(+), 11 deletions(-) create mode 100644 examples/token-airdrop-example.js create mode 100644 src/token/AirdropPendingTransaction.js create mode 100644 src/token/TokenCancelAirdropTransaction.js create mode 100644 src/token/TokenClaimAirdropTransaction.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 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/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/exports.js b/src/exports.js index 431a17815..cb0a1e93a 100644 --- a/src/exports.js +++ b/src/exports.js @@ -34,6 +34,8 @@ 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 index 9c17dfbee..7a0432fdb 100644 --- a/src/token/AbstractTokenTransferTransaction.js +++ b/src/token/AbstractTokenTransferTransaction.js @@ -1,3 +1,23 @@ +/*- + * ‌ + * 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"; 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 index 8d5f79261..ebac42580 100644 --- a/src/token/PendingAirdropId.js +++ b/src/token/PendingAirdropId.js @@ -1,3 +1,23 @@ +/*- + * ‌ + * 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 @@ -11,18 +31,27 @@ export default class PendingAirdropId { /** * * @param {object} props - * @param {AccountId} props.senderId - * @param {AccountId} props.receiverId + * @param {AccountId} [props.senderId] + * @param {AccountId} [props.receiverId] * @param {TokenId?} props.tokenId * @param {NftId?} props.nftId */ constructor(props) { - this.senderId = props.senderId; - this.receiverId = props.receiverId; + 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); + this._tokenId = new TokenId(props.tokenId); } else if (props.nftId) { - this.nftId = new NftId(props.nftId?.tokenId, props.nftId?.serial); + this._nftId = new NftId(props.nftId?.tokenId, props.nftId?.serial); } } @@ -59,15 +88,80 @@ export default class PendingAirdropId { }); } + /** + * + * @param {AccountId} senderId + * @returns + */ + 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(), + 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 index 7963f78ee..33338aa53 100644 --- a/src/token/PendingAirdropRecord.js +++ b/src/token/PendingAirdropRecord.js @@ -1,3 +1,23 @@ +/*- + * ‌ + * 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 diff --git a/src/token/TokenAirdropTransaction.js b/src/token/TokenAirdropTransaction.js index fea6a811a..805a69b7c 100644 --- a/src/token/TokenAirdropTransaction.js +++ b/src/token/TokenAirdropTransaction.js @@ -1,3 +1,23 @@ +/*- + * ‌ + * 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"; 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/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); + }); +}); From f6f5928806f0d885e84058fbe829181bc279c395 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Thu, 12 Sep 2024 01:09:22 +0300 Subject: [PATCH 32/33] fix(test): when empty id should be null Signed-off-by: Ivaylo Nikolov --- test/integration/TokenAirdropIntegrationTest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/TokenAirdropIntegrationTest.js b/test/integration/TokenAirdropIntegrationTest.js index 47de10d77..0a4b179c8 100644 --- a/test/integration/TokenAirdropIntegrationTest.js +++ b/test/integration/TokenAirdropIntegrationTest.js @@ -179,7 +179,7 @@ describe("TokenAirdropIntegrationTest", function () { receiverId, ); expect(newPendingAirdrops[0].airdropId.tokenId).deep.equal(ftTokenId); - expect(newPendingAirdrops[0].airdropId.nftId).to.equal(undefined); + expect(newPendingAirdrops[0].airdropId.nftId).to.equal(null); expect(newPendingAirdrops[1].airdropId.senderId).deep.equal( env.operatorId, @@ -187,7 +187,7 @@ describe("TokenAirdropIntegrationTest", function () { expect(newPendingAirdrops[1].airdropId.receiverId).deep.equal( receiverId, ); - expect(newPendingAirdrops[1].airdropId.tokenId).deep.equal(undefined); + expect(newPendingAirdrops[1].airdropId.tokenId).deep.equal(null); expect(newPendingAirdrops[1].airdropId.nftId.tokenId).to.deep.equal( nftTokenId, ); From 0e6079f2911e82d9107a8242070d791208d02f94 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Thu, 12 Sep 2024 01:10:05 +0300 Subject: [PATCH 33/33] refactor: should return always in jsdoc Signed-off-by: Ivaylo Nikolov --- src/token/PendingAirdropId.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/token/PendingAirdropId.js b/src/token/PendingAirdropId.js index ebac42580..18d357f51 100644 --- a/src/token/PendingAirdropId.js +++ b/src/token/PendingAirdropId.js @@ -91,7 +91,7 @@ export default class PendingAirdropId { /** * * @param {AccountId} senderId - * @returns + * @returns {this} */ setSenderid(senderId) { this._senderId = senderId;