-
Notifications
You must be signed in to change notification settings - Fork 123
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(HIP-991): Permissionless revenue topics (#2233)
Signed-off-by: Ivan Ivanov <[email protected]> Signed-off-by: Naydenov <[email protected]> Co-authored-by: Ivan Ivanov <[email protected]>
- Loading branch information
Showing
19 changed files
with
1,507 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
239 changes: 239 additions & 0 deletions
239
examples/src/main/java/com/hedera/hashgraph/sdk/examples/CreateTopicWithRevenueExample.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
81
sdk/src/main/java/com/hedera/hashgraph/sdk/CustomFeeLimit.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.