From 0b5a739ba62427cd4d0029df6f1e875699f73540 Mon Sep 17 00:00:00 2001 From: Xin Li <59580070+xin-hedera@users.noreply.github.com> Date: Wed, 27 Apr 2022 15:26:14 -0500 Subject: [PATCH] Dedup allowances in crypto approve allowance transaction handler (#3663) Signed-off-by: Xin Li --- ...ptoApproveAllowanceTransactionHandler.java | 70 +++++- .../parser/domain/RecordItemBuilder.java | 5 + .../EntityRecordItemListenerCryptoTest.java | 4 + ...pproveAllowanceTransactionHandlerTest.java | 216 +++++++++++++----- 4 files changed, 222 insertions(+), 73 deletions(-) diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandler.java index a220d7918a2..e1c7c62cefc 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandler.java @@ -24,6 +24,8 @@ import static com.hedera.mirror.importer.parser.PartialDataAction.ERROR; import com.hederahashgraph.api.proto.java.AccountID; +import java.util.HashMap; +import java.util.List; import javax.inject.Named; import lombok.RequiredArgsConstructor; @@ -32,6 +34,7 @@ import com.hedera.mirror.common.domain.entity.NftAllowance; import com.hedera.mirror.common.domain.entity.TokenAllowance; import com.hedera.mirror.common.domain.token.Nft; +import com.hedera.mirror.common.domain.token.NftId; import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.common.domain.transaction.Transaction; import com.hedera.mirror.common.domain.transaction.TransactionType; @@ -66,12 +69,22 @@ public void updateTransaction(Transaction transaction, RecordItem recordItem) { return; } - long consensusTimestamp = transaction.getConsensusTimestamp(); - var payerAccountId = recordItem.getPayerAccountId(); - var transactionBody = recordItem.getTransactionBody().getCryptoApproveAllowance(); + parseCryptoAllowances(transactionBody.getCryptoAllowancesList(), recordItem); + parseNftAllowances(transactionBody.getNftAllowancesList(), recordItem); + parseTokenAllowances(transactionBody.getTokenAllowancesList(), recordItem); + } + + private void parseCryptoAllowances(List cryptoAllowances, + RecordItem recordItem) { + var consensusTimestamp = recordItem.getConsensusTimestamp(); + var cryptoAllowanceState = new HashMap(); + var payerAccountId = recordItem.getPayerAccountId(); - for (var cryptoApproval : transactionBody.getCryptoAllowancesList()) { + // iterate the crypto allowance list in reverse order and honor the last allowance for the same owner and spender + var iterator = cryptoAllowances.listIterator(cryptoAllowances.size()); + while (iterator.hasPrevious()) { + var cryptoApproval = iterator.previous(); EntityId ownerAccountId = getOwnerAccountId(cryptoApproval.getOwner(), payerAccountId); if (ownerAccountId == EntityId.EMPTY) { // ownerAccountId will be EMPTY only when getOwnerAccountId fails to resolve the owner in the alias form @@ -85,10 +98,26 @@ public void updateTransaction(Transaction transaction, RecordItem recordItem) { cryptoAllowance.setPayerAccountId(payerAccountId); cryptoAllowance.setSpender(EntityId.of(cryptoApproval.getSpender()).getId()); cryptoAllowance.setTimestampLower(consensusTimestamp); - entityListener.onCryptoAllowance(cryptoAllowance); + + if (cryptoAllowanceState.putIfAbsent(cryptoAllowance.getId(), cryptoAllowance) == null) { + entityListener.onCryptoAllowance(cryptoAllowance); + } } + } - for (var nftApproval : transactionBody.getNftAllowancesList()) { + private void parseNftAllowances(List nftAllowances, + RecordItem recordItem) { + var consensusTimestamp = recordItem.getConsensusTimestamp(); + var payerAccountId = recordItem.getPayerAccountId(); + var nftAllowanceState = new HashMap(); + var nftSerialAllowanceState = new HashMap(); + + // iterate the nft allowance list in reverse order and honor the last allowance for either + // the same owner, spender, and token for approved for all allowances, or the last serial allowance for + // the same owner, spender, token, and serial + var iterator = nftAllowances.listIterator(nftAllowances.size()); + while (iterator.hasPrevious()) { + var nftApproval = iterator.previous(); EntityId ownerAccountId = getOwnerAccountId(nftApproval.getOwner(), payerAccountId); if (ownerAccountId == EntityId.EMPTY) { // ownerAccountId will be EMPTY only when getOwnerAccountId fails to resolve the owner in the alias form @@ -108,12 +137,14 @@ public void updateTransaction(Transaction transaction, RecordItem recordItem) { nftAllowance.setSpender(spender.getId()); nftAllowance.setTokenId(tokenId.getId()); nftAllowance.setTimestampLower(consensusTimestamp); - entityListener.onNftAllowance(nftAllowance); + + if (nftAllowanceState.putIfAbsent(nftAllowance.getId(), nftAllowance) == null) { + entityListener.onNftAllowance(nftAllowance); + } } EntityId delegatingSpender = EntityId.of(nftApproval.getDelegatingSpender()); for (var serialNumber : nftApproval.getSerialNumbersList()) { - // nft instance allowance update doesn't set nft modifiedTimestamp // services allows the same serial number of a nft token appears in multiple nft allowances to // different spenders. The last spender will be granted such allowance. Nft nft = new Nft(serialNumber, tokenId); @@ -121,11 +152,25 @@ public void updateTransaction(Transaction transaction, RecordItem recordItem) { nft.setDelegatingSpender(delegatingSpender); nft.setModifiedTimestamp(consensusTimestamp); nft.setSpender(spender); - entityListener.onNft(nft); + + if (nftSerialAllowanceState.putIfAbsent(nft.getId(), nft) == null) { + entityListener.onNft(nft); + } } } + } - for (var tokenApproval : transactionBody.getTokenAllowancesList()) { + private void parseTokenAllowances(List tokenAllowances, + RecordItem recordItem) { + var consensusTimestamp = recordItem.getConsensusTimestamp(); + var payerAccountId = recordItem.getPayerAccountId(); + var tokenAllowanceState = new HashMap(); + + // iterate the token allowance list in reverse order and honor the last allowance for the same owner, spender, + // and token + var iterator = tokenAllowances.listIterator(tokenAllowances.size()); + while (iterator.hasPrevious()) { + var tokenApproval = iterator.previous(); EntityId ownerAccountId = getOwnerAccountId(tokenApproval.getOwner(), payerAccountId); if (ownerAccountId == EntityId.EMPTY) { // ownerAccountId will be EMPTY only when getOwnerAccountId fails to resolve the owner in the alias form @@ -140,7 +185,10 @@ public void updateTransaction(Transaction transaction, RecordItem recordItem) { tokenAllowance.setSpender(EntityId.of(tokenApproval.getSpender()).getId()); tokenAllowance.setTokenId(EntityId.of(tokenApproval.getTokenId()).getId()); tokenAllowance.setTimestampLower(consensusTimestamp); - entityListener.onTokenAllowance(tokenAllowance); + + if (tokenAllowanceState.putIfAbsent(tokenAllowance.getId(), tokenAllowance) == null) { + entityListener.onTokenAllowance(tokenAllowance); + } } } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/RecordItemBuilder.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/RecordItemBuilder.java index 57bd2074d20..0e9b7d82afa 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/RecordItemBuilder.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/RecordItemBuilder.java @@ -236,6 +236,11 @@ public Builder cryptoApproveAllow .setOwner(accountId()) .setSpender(accountId()) .setTokenId(tokenId())); + // duplicate allowances + builder.addCryptoAllowances(builder.getCryptoAllowances(0)) + .addTokenAllowances(builder.getTokenAllowances(0)) + .addNftAllowances(builder.getNftAllowances(0)) + .addNftAllowances(builder.getNftAllowances(2).toBuilder().setApprovedForAll(BoolValue.of(false))); return new Builder<>(TransactionType.CRYPTOAPPROVEALLOWANCE, builder); } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerCryptoTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerCryptoTest.java index 43d7adb404f..700048d6d6d 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerCryptoTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerCryptoTest.java @@ -1087,6 +1087,10 @@ private List customizeNftAllowances(Timestamp consensusTimestamp, .setSpender(spender2) .setTokenId(tokenId) .build()); + + // duplicate nft allowance + nftAllowances.add(nftAllowances.get(nftAllowances.size() - 1)); + // serial number 2's allowance is granted twice, the allowance should be granted to spender2 since it appears // after the nft allowance to spender1 expectedNfts.add(nft2.toBuilder().modifiedTimestamp(timestamp).spender(EntityId.of(spender2)).build()); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandlerTest.java index 4029add3816..534c014c47e 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandlerTest.java @@ -20,7 +20,6 @@ * ‍ */ -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -29,20 +28,19 @@ import static org.mockito.Mockito.when; import com.google.common.collect.Range; +import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.CryptoApproveAllowanceTransactionBody; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.util.HashMap; import java.util.Map; -import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import com.hedera.mirror.common.domain.entity.CryptoAllowance; import com.hedera.mirror.common.domain.entity.EntityId; @@ -50,10 +48,12 @@ import com.hedera.mirror.common.domain.entity.NftAllowance; import com.hedera.mirror.common.domain.entity.TokenAllowance; import com.hedera.mirror.common.domain.token.Nft; +import com.hedera.mirror.common.domain.token.NftId; import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.common.domain.transaction.Transaction; import com.hedera.mirror.common.exception.InvalidEntityException; import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.TestUtils; import com.hedera.mirror.importer.parser.PartialDataAction; import com.hedera.mirror.importer.parser.record.RecordParserProperties; @@ -61,14 +61,57 @@ class CryptoApproveAllowanceTransactionHandlerTest extends AbstractTransactionHa private Map aliasMap; - @Captor - private ArgumentCaptor nftAllowanceCaptor; + private long consensusTimestamp; + + private CryptoAllowance expectedCryptoAllowance; + + private Nft expectedNft; + + private NftAllowance expectedNftAllowance; + + private TokenAllowance expectedTokenAllowance; private RecordParserProperties recordParserProperties; + private EntityId payerAccountId; + @BeforeEach void beforeEach() { aliasMap = new HashMap<>(); + + consensusTimestamp = DomainUtils.timestampInNanosMax(recordItemBuilder.timestamp()); + payerAccountId = EntityId.of(recordItemBuilder.accountId()); + expectedCryptoAllowance = CryptoAllowance.builder() + .owner(recordItemBuilder.accountId().getAccountNum()) + .payerAccountId(payerAccountId) + .spender(recordItemBuilder.accountId().getAccountNum()).amount(100L) + .timestampRange(Range.atLeast(consensusTimestamp)) + .build(); + var nftTokenId = recordItemBuilder.tokenId().getTokenNum(); + expectedNft = Nft.builder() + .id(new NftId(1L, EntityId.of(nftTokenId, EntityType.TOKEN))) + .accountId(EntityId.of(recordItemBuilder.accountId())) + .delegatingSpender(EntityId.EMPTY) + .modifiedTimestamp(consensusTimestamp) + .spender(EntityId.of(recordItemBuilder.accountId())) + .build(); + expectedNftAllowance = NftAllowance.builder() + .approvedForAll(true) + .owner(expectedNft.getAccountId().getId()) + .payerAccountId(payerAccountId) + .spender(expectedNft.getSpender().getId()) + .timestampRange(Range.atLeast(consensusTimestamp)) + .tokenId(nftTokenId) + .build(); + expectedTokenAllowance = TokenAllowance.builder() + .amount(200L) + .owner(recordItemBuilder.accountId().getAccountNum()) + .payerAccountId(payerAccountId) + .spender(recordItemBuilder.accountId().getAccountNum()) + .timestampRange(Range.atLeast(consensusTimestamp)) + .tokenId(recordItemBuilder.tokenId().getTokenNum()) + .build(); + when(entityIdService.lookup(any(AccountID.class))).thenAnswer(invocation -> { var accountId = invocation.getArgument(0, AccountID.class); if (accountId == AccountID.getDefaultInstance()) { @@ -118,25 +161,34 @@ void updateTransactionUnsuccessful() { @Test void updateTransactionSuccessful() { - var recordItem = recordItemBuilder.cryptoApproveAllowance().build(); - var timestamp = recordItem.getConsensusTimestamp(); - var transaction = domainBuilder.transaction().customize(t -> t.consensusTimestamp(timestamp)).get(); + var recordItem = recordItemBuilder.cryptoApproveAllowance() + .transactionBody(this::customizeTransactionBody) + .transactionBodyWrapper(this::setTransactionPayer) + .record(r -> r.setConsensusTimestamp(TestUtils.toTimestamp(consensusTimestamp))) + .build(); + var transaction = domainBuilder.transaction() + .customize(t -> t.consensusTimestamp(consensusTimestamp)).get(); transactionHandler.updateTransaction(transaction, recordItem); - assertAllowances(recordItem, owner -> assertThat(owner).isPositive()); + assertAllowances(null); } @Test void updateTransactionSuccessfulWithImplicitOwner() { - var recordItem = recordItemBuilder.cryptoApproveAllowance().transactionBody(b -> { - b.getCryptoAllowancesBuilderList().forEach(builder -> builder.clearOwner()); - b.getNftAllowancesBuilderList().forEach(builder -> builder.clearOwner()); - b.getTokenAllowancesBuilderList().forEach(builder -> builder.clearOwner()); - }).build(); + var recordItem = recordItemBuilder.cryptoApproveAllowance() + .transactionBody(this::customizeTransactionBody) + .transactionBody(b -> { + b.getCryptoAllowancesBuilderList().forEach(builder -> builder.clearOwner()); + b.getNftAllowancesBuilderList().forEach(builder -> builder.clearOwner()); + b.getTokenAllowancesBuilderList().forEach(builder -> builder.clearOwner()); + }) + .transactionBodyWrapper(this::setTransactionPayer) + .record(r -> r.setConsensusTimestamp(TestUtils.toTimestamp(consensusTimestamp))) + .build(); var effectiveOwner = recordItem.getPayerAccountId().getId(); - var timestamp = recordItem.getConsensusTimestamp(); - var transaction = domainBuilder.transaction().customize(t -> t.consensusTimestamp(timestamp)).get(); + var transaction = domainBuilder.transaction() + .customize(t -> t.consensusTimestamp(consensusTimestamp)).get(); transactionHandler.updateTransaction(transaction, recordItem); - assertAllowances(recordItem, owner -> assertThat(owner).isEqualTo(effectiveOwner)); + assertAllowances(effectiveOwner); } @ParameterizedTest(name = "{0}") @@ -175,54 +227,94 @@ void updateTransactionWithAlias() { var alias = DomainUtils.fromBytes(domainBuilder.key()); var ownerEntityId = EntityId.of(recordItemBuilder.accountId()); aliasMap.put(alias, ownerEntityId); - var recordItem = recordItemBuilder.cryptoApproveAllowance().transactionBody(b -> { - b.getCryptoAllowancesBuilderList().forEach(builder -> builder.getOwnerBuilder().setAlias(alias)); - b.getNftAllowancesBuilderList().forEach(builder -> builder.getOwnerBuilder().setAlias(alias)); - b.getTokenAllowancesBuilderList().forEach(builder -> builder.getOwnerBuilder().setAlias(alias)); - }).build(); + var recordItem = recordItemBuilder.cryptoApproveAllowance() + .transactionBody(this::customizeTransactionBody) + .transactionBody(b -> { + b.getCryptoAllowancesBuilderList().forEach(builder -> builder.getOwnerBuilder().setAlias(alias)); + b.getNftAllowancesBuilderList().forEach(builder -> builder.getOwnerBuilder().setAlias(alias)); + b.getTokenAllowancesBuilderList().forEach(builder -> builder.getOwnerBuilder().setAlias(alias)); + }) + .transactionBodyWrapper(this::setTransactionPayer) + .record(r -> r.setConsensusTimestamp(TestUtils.toTimestamp(consensusTimestamp))) + .build(); var timestamp = recordItem.getConsensusTimestamp(); var transaction = domainBuilder.transaction().customize(t -> t.consensusTimestamp(timestamp)).get(); transactionHandler.updateTransaction(transaction, recordItem); - assertAllowances(recordItem, owner -> assertThat(owner).isEqualTo(ownerEntityId.getId())); + assertAllowances(ownerEntityId.getId()); } - private void assertAllowances(RecordItem recordItem, Consumer assertOwner) { - var timestamp = recordItem.getConsensusTimestamp(); - verify(entityListener).onCryptoAllowance(assertArg(t -> assertThat(t) - .isNotNull() - .satisfies(a -> assertThat(a.getAmount()).isPositive()) - .satisfies(a -> assertOwner.accept(a.getOwner())) - .returns(recordItem.getPayerAccountId(), CryptoAllowance::getPayerAccountId) - .returns(timestamp, CryptoAllowance::getTimestampLower))); - - verify(entityListener, times(3)).onNftAllowance(nftAllowanceCaptor.capture()); - assertThat(nftAllowanceCaptor.getAllValues()) - .allSatisfy(n -> assertAll( - () -> assertOwner.accept(n.getOwner()), - () -> assertThat(n.getSpender()).isPositive(), - () -> assertThat(n.getTokenId()).isPositive(), - () -> assertThat(n.getPayerAccountId()).isEqualTo(recordItem.getPayerAccountId()), - () -> assertThat(n.getTimestampRange()).isEqualTo(Range.atLeast(timestamp)) - )) - .extracting("approvedForAll") - .containsExactlyInAnyOrder(false, true, true); - - verify(entityListener, times(4)).onNft(assertArg(t -> assertThat(t) - .isNotNull() - .satisfies(a -> assertOwner.accept(a.getAccountId().getId())) - .satisfies(a -> assertThat(a.getDelegatingSpender()).isEqualTo(EntityId.EMPTY)) - .returns(timestamp, Nft::getModifiedTimestamp) - .satisfies(a -> assertThat(a.getId().getSerialNumber()).isPositive()) - .satisfies(a -> assertThat(a.getId().getTokenId()).isNotNull()) - .satisfies(a -> assertThat(a.getSpender()).isNotNull()))); - - verify(entityListener).onTokenAllowance(assertArg(t -> assertThat(t) - .isNotNull() - .satisfies(a -> assertThat(a.getAmount()).isPositive()) - .satisfies(a -> assertOwner.accept(a.getOwner())) - .satisfies(a -> assertThat(a.getSpender()).isNotNull()) - .satisfies(a -> assertThat(a.getTokenId()).isPositive()) - .returns(recordItem.getPayerAccountId(), TokenAllowance::getPayerAccountId) - .returns(timestamp, TokenAllowance::getTimestampLower))); + private void assertAllowances(Long effectiveOwner) { + if (effectiveOwner != null) { + expectedCryptoAllowance.setOwner(effectiveOwner); + expectedNft.setAccountId(EntityId.of(effectiveOwner, EntityType.ACCOUNT)); + expectedNftAllowance.setOwner(effectiveOwner); + expectedTokenAllowance.setOwner(effectiveOwner); + } + + verify(entityListener, times(1)).onCryptoAllowance(assertArg(t -> assertEquals(expectedCryptoAllowance, t))); + verify(entityListener, times(1)).onNft(assertArg(t -> assertEquals(expectedNft, t))); + verify(entityListener, times(1)).onNftAllowance(assertArg(t -> assertEquals(expectedNftAllowance, t))); + verify(entityListener, times(1)).onTokenAllowance(assertArg(t -> assertEquals(expectedTokenAllowance, t))); + } + + private void customizeTransactionBody(CryptoApproveAllowanceTransactionBody.Builder builder) { + builder.clear(); + + // duplicate with different amount + builder.addCryptoAllowances(com.hederahashgraph.api.proto.java.CryptoAllowance.newBuilder() + .setAmount(expectedCryptoAllowance.getAmount() - 10) + .setOwner(AccountID.newBuilder().setAccountNum(expectedCryptoAllowance.getOwner())) + .setSpender(AccountID.newBuilder().setAccountNum(expectedCryptoAllowance.getSpender())) + ); + // the last one is honored + builder.addCryptoAllowances(com.hederahashgraph.api.proto.java.CryptoAllowance.newBuilder() + .setAmount(expectedCryptoAllowance.getAmount()) + .setOwner(AccountID.newBuilder().setAccountNum(expectedCryptoAllowance.getOwner())) + .setSpender(AccountID.newBuilder().setAccountNum(expectedCryptoAllowance.getSpender())) + ); + + // duplicate nft allowance by serial + builder.addNftAllowances(com.hederahashgraph.api.proto.java.NftAllowance.newBuilder() + .setOwner(AccountID.newBuilder().setAccountNum(expectedNft.getAccountId().getEntityNum())) + .addSerialNumbers(expectedNft.getId().getSerialNumber()) + .setSpender(AccountID.newBuilder().setAccountNum(expectedNft.getSpender().getEntityNum() + 1)) + .setTokenId(TokenID.newBuilder().setTokenNum(expectedNft.getId().getTokenId().getEntityNum())) + ); + // duplicate nft approved for all allowance, approved for all flag is flipped from the last one + builder.addNftAllowances(com.hederahashgraph.api.proto.java.NftAllowance.newBuilder() + .setApprovedForAll(BoolValue.of(!expectedNftAllowance.isApprovedForAll())) + .setOwner(AccountID.newBuilder().setAccountNum(expectedNft.getAccountId().getEntityNum())) + .addSerialNumbers(expectedNft.getId().getSerialNumber()) + .setSpender(AccountID.newBuilder().setAccountNum(expectedNft.getSpender().getEntityNum())) + .setTokenId(TokenID.newBuilder().setTokenNum(expectedNft.getId().getTokenId().getEntityNum())) + ); + // the last one is honored + builder.addNftAllowances(com.hederahashgraph.api.proto.java.NftAllowance.newBuilder() + .setApprovedForAll(BoolValue.of(expectedNftAllowance.isApprovedForAll())) + .setOwner(AccountID.newBuilder().setAccountNum(expectedNft.getAccountId().getEntityNum())) + .addSerialNumbers(expectedNft.getId().getSerialNumber()) + .setSpender(AccountID.newBuilder().setAccountNum(expectedNft.getSpender().getEntityNum())) + .setTokenId(TokenID.newBuilder().setTokenNum(expectedNft.getId().getTokenId().getEntityNum())) + ); + + // duplicate token allowance + builder.addTokenAllowances(com.hederahashgraph.api.proto.java.TokenAllowance.newBuilder() + .setAmount(expectedTokenAllowance.getAmount() - 10) + .setOwner(AccountID.newBuilder().setAccountNum(expectedTokenAllowance.getOwner())) + .setSpender(AccountID.newBuilder().setAccountNum(expectedTokenAllowance.getSpender())) + .setTokenId(TokenID.newBuilder().setTokenNum(expectedTokenAllowance.getTokenId())) + ); + // the last one is honored + builder.addTokenAllowances(com.hederahashgraph.api.proto.java.TokenAllowance.newBuilder() + .setAmount(expectedTokenAllowance.getAmount()) + .setOwner(AccountID.newBuilder().setAccountNum(expectedTokenAllowance.getOwner())) + .setSpender(AccountID.newBuilder().setAccountNum(expectedTokenAllowance.getSpender())) + .setTokenId(TokenID.newBuilder().setTokenNum(expectedTokenAllowance.getTokenId())) + ); + } + + private void setTransactionPayer(TransactionBody.Builder builder) { + builder.getTransactionIDBuilder() + .setAccountID(AccountID.newBuilder().setAccountNum(payerAccountId.getEntityNum())); } }