Skip to content

Commit

Permalink
HIP-904 Add support for Token Airdrop (#8856)
Browse files Browse the repository at this point in the history
* Add support for Token Airdrop

Signed-off-by: Edwin Greene <[email protected]>
  • Loading branch information
edwin-greene authored Sep 3, 2024
1 parent 86e35de commit b990a26
Show file tree
Hide file tree
Showing 36 changed files with 1,605 additions and 21 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/charts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ name: Charts

on:
pull_request:
branches: [ main, release/** ]
paths: [ charts/** ]
branches: [main, release/**]
paths: [charts/**]
push:
branches: [ main, release/** ]
tags: [ v* ]
branches: [main, release/**]
tags: [v*]

permissions:
contents: read
Expand Down
4 changes: 4 additions & 0 deletions docs/checklist/feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ aims to document the required changes associated with any new feature.
- [ ] `TransactionType`
- [ ] `DomainBuilder`

## GraphQL API

- [ ] Transaction type

## Importer

- [ ] V1 migration
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ value, it is recommended to only populate overridden properties in the custom `a
| `hedera.mirror.importer.parser.record.entity.persist.syntheticContractLogs` | true | Persist synthetic contract logs from HAPI transaction to the database |
| `hedera.mirror.importer.parser.record.entity.persist.syntheticContractResults` | false | Persist synthetic contract results from HAPI transaction to the database |
| `hedera.mirror.importer.parser.record.entity.persist.systemFiles` | true | Persist only system files (number lower than `1000`) to the database |
| `hedera.mirror.importer.parser.record.entity.persist.tokenAirdrops` | true | Persist token airdrop data to the database |
| `hedera.mirror.importer.parser.record.entity.persist.tokens` | true | Persist token data to the database |
| `hedera.mirror.importer.parser.record.entity.persist.topics` | true | Persist topic messages to the database |
| `hedera.mirror.importer.parser.record.entity.persist.topicMessageLookups` | false | Persist topic message lookups to the database |
Expand Down
2 changes: 1 addition & 1 deletion docs/database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ work_mem = 50MB
The table below documents the database indexes with the usage in APIs / services.

| Table | Indexed Columns | Component | Service | Description |
|-----------------|----------------------------------------------|---------------|----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| --------------- | -------------------------------------------- | ------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| contract_result | consensus_timestamp | REST API | `/api/v1/contracts/results` | Used to query contract results with timestamp filter |
| contract_result | contract_id, sender_id, consensus_timestamp | REST API | `/api/v1/contracts/:idOrAddress/results?from=:from` | Used to query a specific contract's results with `from` filter |
| contract_result | contract_id, consensus_timestamp | REST API | `/api/v1/contracts/:idOrAddress/results` | Used to query a specific contract's results with optional timestamp filter |
Expand Down
1 change: 0 additions & 1 deletion docs/runbook/perform-stackgres-security-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,3 @@ spec:
securityUpgrade:
mode: InPlace
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (C) 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.
*/

package com.hedera.mirror.common.domain.token;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.Range;
import com.hedera.mirror.common.domain.History;
import com.hedera.mirror.common.domain.Upsertable;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.IdClass;
import jakarta.persistence.MappedSuperclass;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

@Data
@IdClass(AbstractTokenAirdrop.Id.class)
@MappedSuperclass
@NoArgsConstructor
@SuperBuilder(toBuilder = true)
@Upsertable(history = true)
public class AbstractTokenAirdrop implements History {

private Long amount;

@jakarta.persistence.Id
private long receiverAccountId;

@jakarta.persistence.Id
private long senderAccountId;

@jakarta.persistence.Id
private long serialNumber;

@Enumerated(EnumType.STRING)
@JdbcTypeCode(SqlTypes.NAMED_ENUM)
private TokenAirdropStateEnum state;

private Range<Long> timestampRange;

@jakarta.persistence.Id
private long tokenId;

@JsonIgnore
public Id getId() {
Id id = new Id();
id.setReceiverAccountId(receiverAccountId);
id.setSenderAccountId(senderAccountId);
id.setSerialNumber(serialNumber);
id.setTokenId(tokenId);
return id;
}

@Data
public static class Id implements Serializable {
@Serial
private static final long serialVersionUID = -8165098238647325621L;

private long receiverAccountId;
private long senderAccountId;
private long serialNumber;
private long tokenId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (C) 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.
*/

package com.hedera.mirror.common.domain.token;

import jakarta.persistence.Entity;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@Entity
@NoArgsConstructor
@SuperBuilder(toBuilder = true)
public class TokenAirdrop extends AbstractTokenAirdrop {
// Only the parent class should contain fields so that they're shared with both the history and non-history tables.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (C) 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.
*/

package com.hedera.mirror.common.domain.token;

import jakarta.persistence.Entity;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@Entity
@NoArgsConstructor
@SuperBuilder(toBuilder = true)
public class TokenAirdropHistory extends AbstractTokenAirdrop {
// Only the parent class should contain fields so that they're shared with both the history and non-history tables.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (C) 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.
*/

package com.hedera.mirror.common.domain.token;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum TokenAirdropStateEnum {
CANCELLED(0),
CLAIMED(1),
PENDING(2);

private final int id;
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
import com.hedera.mirror.common.domain.token.Token;
import com.hedera.mirror.common.domain.token.TokenAccount;
import com.hedera.mirror.common.domain.token.TokenAccountHistory;
import com.hedera.mirror.common.domain.token.TokenAirdrop;
import com.hedera.mirror.common.domain.token.TokenAirdropHistory;
import com.hedera.mirror.common.domain.token.TokenAirdropStateEnum;
import com.hedera.mirror.common.domain.token.TokenFreezeStatusEnum;
import com.hedera.mirror.common.domain.token.TokenHistory;
import com.hedera.mirror.common.domain.token.TokenKycStatusEnum;
Expand Down Expand Up @@ -926,6 +929,42 @@ public DomainWrapper<SidecarFile, SidecarFile.SidecarFileBuilder> sidecarFile()
return new DomainWrapperImpl<>(builder, builder::build);
}

public DomainWrapper<TokenAirdrop, TokenAirdrop.TokenAirdropBuilder<?, ?>> tokenAirdrop(TokenTypeEnum type) {
long timestamp = timestamp();
var builder = TokenAirdrop.builder()
.receiverAccountId(id())
.senderAccountId(id())
.state(TokenAirdropStateEnum.PENDING)
.timestampRange(Range.atLeast(timestamp))
.tokenId(id());
if (type == TokenTypeEnum.NON_FUNGIBLE_UNIQUE) {
builder.serialNumber(number());
} else {
long amount = number() + 1000;
builder.amount(amount);
}

return new DomainWrapperImpl<>(builder, builder::build);
}

public DomainWrapper<TokenAirdropHistory, TokenAirdropHistory.TokenAirdropHistoryBuilder<?, ?>> tokenAirdropHistory(
TokenTypeEnum type) {
long timestamp = timestamp();
var builder = TokenAirdropHistory.builder()
.receiverAccountId(id())
.senderAccountId(id())
.state(TokenAirdropStateEnum.PENDING)
.timestampRange(Range.closedOpen(timestamp, timestamp + 10))
.tokenId(id());
if (type == TokenTypeEnum.NON_FUNGIBLE_UNIQUE) {
builder.serialNumber(number());
} else {
long amount = number() + 1000;
builder.amount(amount);
}
return new DomainWrapperImpl<>(builder, builder::build);
}

public DomainWrapper<TokenAllowance, TokenAllowance.TokenAllowanceBuilder<?, ?>> tokenAllowance() {
long amount = number() + 1000;
var spender = entityId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.hedera.mirror.common.domain.token.Nft;
import com.hedera.mirror.common.domain.token.Token;
import com.hedera.mirror.common.domain.token.TokenAccount;
import com.hedera.mirror.common.domain.token.TokenAirdrop;
import com.hedera.mirror.common.domain.token.TokenTransfer;
import com.hedera.mirror.common.domain.topic.TopicMessage;
import com.hedera.mirror.common.domain.transaction.AssessedCustomFee;
Expand Down Expand Up @@ -203,6 +204,11 @@ public void onTokenAccount(TokenAccount tokenAccount) throws ImporterException {
onEach(EntityListener::onTokenAccount, tokenAccount);
}

@Override
public void onTokenAirdrop(TokenAirdrop tokenAirdrop) throws ImporterException {
onEach(EntityListener::onTokenAirdrop, tokenAirdrop);
}

@Override
public void onTokenAllowance(TokenAllowance tokenAllowance) throws ImporterException {
onEach(EntityListener::onTokenAllowance, tokenAllowance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.hedera.mirror.common.domain.token.Nft;
import com.hedera.mirror.common.domain.token.Token;
import com.hedera.mirror.common.domain.token.TokenAccount;
import com.hedera.mirror.common.domain.token.TokenAirdrop;
import com.hedera.mirror.common.domain.token.TokenTransfer;
import com.hedera.mirror.common.domain.topic.TopicMessage;
import com.hedera.mirror.common.domain.transaction.AssessedCustomFee;
Expand Down Expand Up @@ -111,6 +112,8 @@ default void onToken(Token token) throws ImporterException {}

default void onTokenAccount(TokenAccount tokenAccount) throws ImporterException {}

default void onTokenAirdrop(TokenAirdrop tokenAirdrop) {}

default void onTokenAllowance(TokenAllowance tokenAllowance) {}

default void onTokenTransfer(TokenTransfer tokenTransfer) throws ImporterException {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public static class PersistProperties {

private boolean tokens = true;

private boolean tokenAirdrops = true;

private boolean topics = true;

private boolean topicMessageLookups = false;
Expand Down Expand Up @@ -108,6 +110,10 @@ public static class PersistProperties {
@NotNull
private Set<TransactionType> transactionSignatures = EnumSet.of(SCHEDULECREATE, SCHEDULESIGN);

public boolean isTokenAirdrops() {
return tokenAirdrops && tokens;
}

public boolean shouldPersistEntityTransaction(EntityId entityId) {
return entityTransactions && !EntityId.isEmpty(entityId) && !entityTransactionExclusion.contains(entityId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.hedera.mirror.common.domain.token.NftTransfer;
import com.hedera.mirror.common.domain.token.Token;
import com.hedera.mirror.common.domain.token.TokenAccount;
import com.hedera.mirror.common.domain.token.TokenAirdrop;
import com.hedera.mirror.common.domain.token.TokenTransfer;
import com.hedera.mirror.common.domain.topic.TopicMessage;
import com.hedera.mirror.common.domain.transaction.AssessedCustomFee;
Expand Down Expand Up @@ -295,6 +296,13 @@ public void onTokenAccount(TokenAccount tokenAccount) throws ImporterException {
context.merge(tokenAccount.getId(), tokenAccount, this::mergeTokenAccount);
}

@Override
public void onTokenAirdrop(TokenAirdrop tokenAirdrop) {
if (entityProperties.getPersist().isTokenAirdrops()) {
context.merge(tokenAirdrop.getId(), tokenAirdrop, this::mergeTokenAirdrop);
}
}

@Override
public void onTokenAllowance(TokenAllowance tokenAllowance) {
context.merge(tokenAllowance.getId(), tokenAllowance, this::mergeFungibleAllowance);
Expand Down Expand Up @@ -768,6 +776,16 @@ private TokenAccount mergeTokenAccountBalance(TokenAccount lastTokenAccount, Tok
return lastTokenAccount;
}

private TokenAirdrop mergeTokenAirdrop(TokenAirdrop previous, TokenAirdrop current) {
if (previous.getAmount() != null && current.getAmount() == null) {
// Cancel or claim do not contain an amount so set the amount here so as not to override it with null
current.setAmount(previous.getAmount());
}

previous.setTimestampUpper(current.getTimestampLower());
return current;
}

private void onNftTransferList(Transaction transaction) {
var nftTransferList = transaction.getNftTransfer();
if (CollectionUtils.isEmpty(nftTransferList)) {
Expand Down
Loading

0 comments on commit b990a26

Please sign in to comment.