Skip to content

Commit

Permalink
feat(HIP-991): Permissionless revenue topics (#2233)
Browse files Browse the repository at this point in the history
Signed-off-by: Ivan Ivanov <[email protected]>
Signed-off-by: Naydenov <[email protected]>
Co-authored-by: Ivan Ivanov <[email protected]>
  • Loading branch information
naydenovn and 0xivanov authored Feb 21, 2025
1 parent 76cd64e commit 4b9a0ad
Show file tree
Hide file tree
Showing 19 changed files with 1,507 additions and 17 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ jobs:
run: ./gradlew qualityCheck :examples:qualityCheck --continue --rerun-tasks

- name: Start Local Node
run: npx @hashgraph/hedera-local start -d --network local --network-tag=0.57.0
run: npx @hashgraph/hedera-local start -d --network local

- name: Run Unit and Integration Tests
env:
Expand Down Expand Up @@ -244,7 +244,7 @@ jobs:
run: ./gradlew -p example-android assemble

- name: Start the local node
run: npx @hashgraph/hedera-local start -d --network local --network-tag=0.57.0
run: npx @hashgraph/hedera-local start -d --network local

- name: Prepare .env for Examples
run: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import com.hedera.hashgraph.sdk.AccountId;
import com.hedera.hashgraph.sdk.Client;
import com.hedera.hashgraph.sdk.Hbar;
import java.util.HashMap;
import java.util.List;

Expand All @@ -25,7 +24,7 @@ public static Client forName(String network) throws InterruptedException {
} else {
client = Client.forName(network);
}
return client.setDefaultMaxTransactionFee(new Hbar(50));
return client;
}

public static Client forLocalNetwork() throws InterruptedException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// SPDX-License-Identifier: Apache-2.0
package com.hedera.hashgraph.sdk.examples;

import com.hedera.hashgraph.sdk.*;
import io.github.cdimascio.dotenv.Dotenv;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
* Demonstrates the creation of a revenue-generating topic with HBAR and token-based custom fees,
* account creation, and fee exemptions using the Hedera SDK.
*/
public class CreateTopicWithRevenueExample {

/**
* Operator's account ID used to sign and pay for transactions on Hedera.
*/
private static final AccountId OPERATOR_ID =
AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID")));

/**
* Operator's private key for signing transactions.
*/
private static final PrivateKey OPERATOR_KEY =
PrivateKey.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_KEY")));

/**
* Hedera network (localhost, testnet, previewnet, or mainnet).
*/
private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet");

public static void main(String[] args) throws Exception {
System.out.println("Starting Hedera Custom Fees Example...");

// Step 0: Initialize client and set the operator.

try (Client client = ClientHelper.forName(HEDERA_NETWORK).setOperator(OPERATOR_ID, OPERATOR_KEY)) {
/**
* Step 1: Create an account for Alice with an initial balance of 5 HBAR.
*/
System.out.println("Creating Alice's account...");
PrivateKey aliceKey = PrivateKey.generateECDSA();

var aliceAccountId = new AccountCreateTransaction()
.setKeyWithoutAlias(aliceKey)
.setMaxAutomaticTokenAssociations(1)
.setInitialBalance(Hbar.from(2))
.execute(client)
.getReceipt(client)
.accountId;
Objects.requireNonNull(aliceAccountId);

System.out.println("Alice's Account ID: " + aliceAccountId);

/**
* Step 2: Create a topic with an HBAR custom fee.
*/
System.out.println("Creating a topic with HBAR custom fee...");

var customFee =
new CustomFixedFee().setAmount(new Hbar(1).toTinybars()).setFeeCollectorAccountId(OPERATOR_ID);

var topicId = new TopicCreateTransaction()
.setAdminKey(OPERATOR_KEY)
.setFeeScheduleKey(OPERATOR_KEY)
.setCustomFees(Collections.singletonList(customFee))
.execute(client)
.getReceipt(client)
.topicId;

System.out.println("Created Topic ID: " + topicId);

/**
* Step 3: Submit a message to the topic, paid by Alice, with a custom fee limit.
*/
System.out.println("Submitting a message as Alice to the topic...");

var aliceBalanceBefore =
new AccountBalanceQuery().setAccountId(aliceAccountId).execute(client).hbars;

var feeCollectorBalanceBefore =
new AccountBalanceQuery().setAccountId(OPERATOR_ID).execute(client).hbars;

var customFeeLimit = new CustomFeeLimit()
.setPayerId(aliceAccountId)
.setCustomFees(
List.of(new CustomFixedFee().setAmount(Hbar.from(2).toTinybars())));

client.setOperator(aliceAccountId, aliceKey);

new TopicMessageSubmitTransaction()
.setCustomFeeLimits(List.of(customFeeLimit))
.setTopicId(topicId)
.setMessage("Hello, Hedera™ hashgraph!")
.execute(client)
.getReceipt(client);

System.out.println("Message submitted successfully.");

/**
* Step 4: Verify Alice's and fee collector's balance after the transaction.
*/
client.setOperator(OPERATOR_ID, OPERATOR_KEY);

var aliceBalanceAfter =
new AccountBalanceQuery().setAccountId(aliceAccountId).execute(client).hbars;

var feeCollectorBalanceAfter =
new AccountBalanceQuery().setAccountId(OPERATOR_ID).execute(client).hbars;

System.out.println("Alice's balance before: " + aliceBalanceBefore + ", after: " + aliceBalanceAfter);
System.out.println("Fee collector's balance before: " + feeCollectorBalanceBefore + ", after: "
+ feeCollectorBalanceAfter);

/**
* Step 5: Create a fungible token and transfer it to Alice.
*/
System.out.println("Creating a token and transferring it to Alice...");

var tokenId = new TokenCreateTransaction()
.setTokenName("revenue-generating token")
.setTokenSymbol("RGT")
.setTreasuryAccountId(client.getOperatorAccountId())
.setDecimals(8)
.setInitialSupply(100)
.execute(client)
.getReceipt(client)
.tokenId;

new TransferTransaction()
.addTokenTransfer(tokenId, client.getOperatorAccountId(), -1)
.addTokenTransfer(tokenId, aliceAccountId, 1)
.execute(client)
.getReceipt(client);

/**
* Step 6: Update the topic to charge a token-based fee.
*/
System.out.println("Updating the topic to charge a token-based fee...");

var customFeeToken = new CustomFixedFee()
.setAmount(1)
.setFeeCollectorAccountId(OPERATOR_ID)
.setDenominatingTokenId(tokenId);

new TopicUpdateTransaction()
.setTopicId(topicId)
.setCustomFees(List.of(customFeeToken))
.execute(client)
.getReceipt(client);

/**
* Step 7: Submit another message without specifying a custom fee limit.
*/
System.out.println("Submitting another message without custom fee limit...");

client.setOperator(aliceAccountId, aliceKey);

new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage("Another message!")
.execute(client)
.getReceipt(client);

client.setOperator(OPERATOR_ID, OPERATOR_KEY);

/**
* Step 8: Verify Alice's token balance and the fee collector's token balance after the transaction.
*/
var aliceTokenBalanceAfter = new AccountBalanceQuery()
.setAccountId(aliceAccountId)
.execute(client)
.tokens
.get(tokenId);

var feeCollectorTokenBalanceAfter = new AccountBalanceQuery()
.setAccountId(OPERATOR_ID)
.execute(client)
.tokens
.get(tokenId);

System.out.println("Alice's token balance: " + aliceTokenBalanceAfter);
System.out.println("Fee collector's token balance: " + feeCollectorTokenBalanceAfter);

/**
* Step 9: Create Bob's account with 10 HBAR.
*/
System.out.println("Creating Bob's account...");
Hbar initialBalance = new Hbar(10);
PrivateKey bobKey = PrivateKey.generateECDSA();
var bobAccountId = new AccountCreateTransaction()
.setKey(bobKey)
.setInitialBalance(initialBalance)
.setMaxAutomaticTokenAssociations(100)
.execute(client)
.getReceipt(client)
.accountId;

System.out.println("Bob's Account ID: " + bobAccountId);

/**
* Step 10: Exempt Bob from paying topic fees.
*/
System.out.println("Updating topic to add Bob as a fee-exempt key...");

new TopicUpdateTransaction()
.setTopicId(topicId)
.addFeeExemptKey(bobKey)
.execute(client)
.getReceipt(client);

/**
* Step 11: Bob submits a message to the topic without paying the fee.
*/
client.setOperator(bobAccountId, bobKey);

new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage("Hello from Bob!")
.execute(client)
.getReceipt(client);

System.out.println("Message submitted successfully by Bob without being charged.");

/**
* Step 12: Verify Bob's balance should be almost the same as the initial
*/
var bobBalanceAfter =
new AccountBalanceQuery().setAccountId(bobAccountId).execute(client).hbars;
System.out.println("Bob's initial balance: " + initialBalance + ", after: " + bobBalanceAfter);

} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("Example execution completed.");
}
}
}
81 changes: 81 additions & 0 deletions sdk/src/main/java/com/hedera/hashgraph/sdk/CustomFeeLimit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: Apache-2.0
package com.hedera.hashgraph.sdk;

import com.hedera.hashgraph.sdk.proto.CustomFee;
import com.hedera.hashgraph.sdk.proto.FixedFee;
import java.util.List;
import java.util.stream.Collectors;

/**
* A maximum custom fee that the user is willing to pay.
* <p>
* This message is used to specify the maximum custom fee that given user is
* willing to pay.
*/
public class CustomFeeLimit {

private AccountId payerId;

private List<CustomFixedFee> customFees;

/**
* Constructor
*/
public CustomFeeLimit() {}

/**
* Extracts the payer accountId
* @return payerId
*/
public AccountId getPayerId() {
return payerId;
}

/**
* A payer account identifier.
*/
public CustomFeeLimit setPayerId(AccountId payerId) {
this.payerId = payerId;
return this;
}

/**
* Extracts a list of CustomFixedFee
* @return
*/
public List<CustomFixedFee> getCustomFees() {
return customFees;
}

/**
* The maximum fees that the user is willing to pay for the message.
*/
public CustomFeeLimit setCustomFees(List<CustomFixedFee> customFees) {
this.customFees = customFees;
return this;
}

static CustomFeeLimit fromProtobuf(com.hedera.hashgraph.sdk.proto.CustomFeeLimit customFeeLimit) {
return new CustomFeeLimit()
.setPayerId(AccountId.fromProtobuf(customFeeLimit.getAccountId()))
.setCustomFees(customFeeLimit.getFeesList().stream()
.map(CustomFixedFee::fromProtobuf)
.collect(Collectors.toList()));
}

com.hedera.hashgraph.sdk.proto.CustomFeeLimit toProtobuf() {
com.hedera.hashgraph.sdk.proto.CustomFeeLimit.Builder builder =
com.hedera.hashgraph.sdk.proto.CustomFeeLimit.newBuilder();

builder.setAccountId(payerId.toProtobuf());

List<FixedFee> protoFixedFees = customFees.stream()
.map(CustomFixedFee::toProtobuf)
.map(CustomFee::getFixedFee)
.collect(Collectors.toList());

builder.addAllFees(protoFixedFees);

return builder.build();
}
}
18 changes: 18 additions & 0 deletions sdk/src/main/java/com/hedera/hashgraph/sdk/CustomFixedFee.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
package com.hedera.hashgraph.sdk;

import com.hedera.hashgraph.sdk.proto.FixedCustomFee;
import com.hedera.hashgraph.sdk.proto.FixedFee;
import javax.annotation.Nullable;

Expand Down Expand Up @@ -35,6 +36,23 @@ static CustomFixedFee fromProtobuf(FixedFee fixedFee) {
return returnFee;
}

FixedCustomFee toTopicFeeProtobuf() {
var builder = FixedCustomFee.newBuilder();
var fixedFeeBuilder = FixedFee.newBuilder();

if (feeCollectorAccountId != null) {
builder.setFeeCollectorAccountId(feeCollectorAccountId.toProtobuf());
}

builder.setFixedFee(fixedFeeBuilder.setAmount(amount));

if (denominatingTokenId != null) {
builder.setFixedFee(fixedFeeBuilder.setDenominatingTokenId(denominatingTokenId.toProtobuf()));
}

return builder.build();
}

@Override
CustomFixedFee deepCloneSubclass() {
return new CustomFixedFee()
Expand Down
Loading

0 comments on commit 4b9a0ad

Please sign in to comment.