Skip to content

Commit

Permalink
Airdrop functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
RickyLB authored Oct 9, 2024
1 parent 5b160d8 commit 89d96b5
Show file tree
Hide file tree
Showing 157 changed files with 4,719 additions and 2,655 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/swift-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ permissions:

jobs:
format:
runs-on: macos-12
runs-on: macos-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
Expand Down
294 changes: 294 additions & 0 deletions Examples/TokenAirdrop/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
/*
* ‌
* Hedera Swift SDK
* ​
* Copyright (C) 2022 - 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 Foundation
import Hedera
import SwiftDotenv

@main
internal enum Program {
internal static func main() async throws {
let env = try Dotenv.load()
let client = try Client.forName(env.networkName)

client.setOperator(env.operatorAccountId, env.operatorKey)

let privateKey1 = PrivateKey.generateEcdsa()
let aliceId = try await AccountCreateTransaction()
.key(.single(privateKey1.publicKey))
.initialBalance(Hbar(10))
.maxAutomaticTokenAssociations(-1)
.execute(client)
.getReceipt(client)
.accountId!

let privateKey2 = PrivateKey.generateEcdsa()
let bobId = try await AccountCreateTransaction()
.key(.single(privateKey2.publicKey))
.maxAutomaticTokenAssociations(1)
.execute(client)
.getReceipt(client)
.accountId!

let privateKey3 = PrivateKey.generateEcdsa()
let carolId = try await AccountCreateTransaction()
.key(.single(privateKey3.publicKey))
.maxAutomaticTokenAssociations(0)
.execute(client)
.getReceipt(client)
.accountId!

let treasuryKey = PrivateKey.generateEcdsa()
let treasuryAccountId = try await AccountCreateTransaction()
.key(.single(treasuryKey.publicKey))
.initialBalance(Hbar(10))
.execute(client)
.getReceipt(client)
.accountId!

// Create FT and NFT and mint
let tokenId = try await TokenCreateTransaction()
.name("ffff")
.symbol("F")
.decimals(3)
.initialSupply(100)
.maxSupply(100)
.treasuryAccountId(treasuryAccountId)
.tokenSupplyType(.finite)
.adminKey(.single(env.operatorKey.publicKey))
.freezeKey(.single(env.operatorKey.publicKey))
.supplyKey(.single(env.operatorKey.publicKey))
.pauseKey(.single(env.operatorKey.publicKey))
.expirationTime(.now + .hours(2))
.freezeWith(client)
.sign(treasuryKey)
.execute(client)
.getReceipt(client)
.tokenId!

let nftId = try await TokenCreateTransaction()
.name("example NFT")
.symbol("F")
.maxSupply(10)
.treasuryAccountId(treasuryAccountId)
.tokenSupplyType(.finite)
.tokenType(.nonFungibleUnique)
.adminKey(.single(env.operatorKey.publicKey))
.freezeKey(.single(env.operatorKey.publicKey))
.supplyKey(.single(env.operatorKey.publicKey))
.pauseKey(.single(env.operatorKey.publicKey))
.expirationTime(.now + .hours(2))
.freezeWith(client)
.sign(treasuryKey)
.execute(client)
.getReceipt(client)
.tokenId!

_ = try await TokenMintTransaction()
.tokenId(nftId)
.metadata(Array(repeating: Data([9, 1, 6]), count: 4))
.execute(client)
.getReceipt(client)

// Airdrop fungible tokens to all 3 accounts
print("Airdropping tokens to all accounts")

let txRecord = try await TokenAirdropTransaction()
.tokenTransfer(tokenId, aliceId, 10)
.tokenTransfer(tokenId, treasuryAccountId, -10)
.tokenTransfer(tokenId, bobId, 10)
.tokenTransfer(tokenId, treasuryAccountId, -10)
.tokenTransfer(tokenId, carolId, 10)
.tokenTransfer(tokenId, treasuryAccountId, -10)
.freezeWith(client)
.sign(treasuryKey)
.execute(client)
.getRecord(client)

// Get the transaction record and see one pending airdrop (for carol)
print("Pending airdrop length: \(txRecord.pendingAirdropRecords.count)")
print("Pending airdrops: \(txRecord.pendingAirdropRecords.first!)")

// Query to verify alice and bob received the airdrops and carol did not
let aliceBalance = try await AccountBalanceQuery()
.accountId(aliceId)
.execute(client)

let bobBalance = try await AccountBalanceQuery()
.accountId(bobId)
.execute(client)

let carolBalance = try await AccountBalanceQuery()
.accountId(carolId)
.execute(client)

print("Alice ft balance after airdrop: \(aliceBalance.tokenBalances[tokenId]!)")
print("Bob ft balance after airdrop: \(bobBalance.tokenBalances[tokenId]!)")
print("Carol ft balance after airdrop: \(String(describing:carolBalance.tokenBalances[tokenId]))")

// Claim the airdrop for carol
print("Claiming ft with Carol")

_ = try await TokenClaimAirdropTransaction()
.addPendingAirdropId(txRecord.pendingAirdropRecords[0].pendingAirdropId)
.freezeWith(client)
.sign(privateKey3)
.execute(client)
.getReceipt(client)

let carolBalanceAfterClaim = try await AccountBalanceQuery()
.accountId(carolId)
.execute(client)

print("Carol ft balance after airdrop: \(carolBalanceAfterClaim.tokenBalances[tokenId]!)")

// Airdrop the NFTs to all three accounts
print("Airdropping nfts")
let nftTxRecord = try await TokenAirdropTransaction()
.nftTransfer(nftId.nft(1), treasuryAccountId, aliceId)
.nftTransfer(nftId.nft(2), treasuryAccountId, bobId)
.nftTransfer(nftId.nft(3), treasuryAccountId, carolId)
.freezeWith(client)
.sign(treasuryKey)
.execute(client)
.getRecord(client)

// Get the transaction record and verify two pending airdrops (for bob & carol)
print("Pending airdrops length: \(nftTxRecord.pendingAirdropRecords.count)")
print("Pending airdrops for Bob: \(nftTxRecord.pendingAirdropRecords[0])")
print("Pending airdrops for Carol: \(nftTxRecord.pendingAirdropRecords[1])")

// Query to verify alice received the airdrop and bob and carol did not
let aliceNftBalance = try await AccountBalanceQuery()
.accountId(aliceId)
.execute(client)

let bobNftBalance = try await AccountBalanceQuery()
.accountId(bobId)
.execute(client)

let carolNftBalance = try await AccountBalanceQuery()
.accountId(carolId)
.execute(client)

print("Alice nft balance after airdrop: \(aliceNftBalance.tokenBalances[nftId]!)")
print("Bob nft balance after airdrop: \(String(describing:bobNftBalance.tokenBalances[nftId]))")
print("Carol nft balance after airdrop: \(String(describing:carolNftBalance.tokenBalances[nftId]))")

// Claim the airdrop for bob
print("Claiming nft with Bob")
_ = try await TokenClaimAirdropTransaction()
.addPendingAirdropId(nftTxRecord.pendingAirdropRecords[0].pendingAirdropId)
.freezeWith(client)
.sign(privateKey2)
.execute(client)
.getReceipt(client)

let bobNftBalanceAfterClaim = try await AccountBalanceQuery()
.accountId(bobId)
.execute(client)

print("Bob nft balance after claim: \(bobNftBalanceAfterClaim.tokenBalances[nftId]!)")

// Cancel the airdrop for carol
print("Cancelling nft for Carol")
_ = try await TokenCancelAirdropTransaction()
.addPendingAirdropId(nftTxRecord.pendingAirdropRecords[1].pendingAirdropId)
.freezeWith(client)
.sign(treasuryKey)
.execute(client)
.getReceipt(client)

let carolNftBalanceAfterCancel = try await AccountBalanceQuery()
.accountId(carolId)
.execute(client)

print("Carol nft balance after cancel: \(String(describing:carolNftBalanceAfterCancel.tokenBalances[nftId]))")

// Reject the NFT for bob
print("Rejecting nft with Bob")
_ = try await TokenRejectTransaction()
.owner(bobId)
.addNftId(nftId.nft(2))
.freezeWith(client)
.sign(privateKey2)
.execute(client)
.getReceipt(client)

// Query to verify bob no longer has the NFT
let bobNftBalanceAfterReject = try await AccountBalanceQuery()
.accountId(bobId)
.execute(client)

print("Bob nft balance after reject: \(bobNftBalanceAfterReject.tokenBalances[nftId]!)")

// Query to verify the NFT was returned to the Treasury
let treasuryNftBalance = try await AccountBalanceQuery()
.accountId(treasuryAccountId)
.execute(client)

print("Treasury nft balance after reject: \(treasuryNftBalance.tokenBalances[nftId]!)")

// Reject the Fungible token for carol
print("Rejecting ft with Carol")
_ = try await TokenRejectTransaction()
.owner(carolId)
.addTokenId(tokenId)
.freezeWith(client)
.sign(privateKey3)
.execute(client)
.getReceipt(client)

// Query to verify Carol no longer has the fungible tokens
let carolFtBalanceAfterReject = try await AccountBalanceQuery()
.accountId(carolId)
.execute(client)

print("Carol ft balance after reject: \(carolFtBalanceAfterReject.tokenBalances[tokenId]!)")

// Query to verify Treasury received the rejected fungible tokens
let treasuryFtBalance = try await AccountBalanceQuery()
.accountId(treasuryAccountId)
.execute(client)

print("Treasury ft balance after reject: \(treasuryFtBalance.tokenBalances[tokenId]!)")

print("Token airdrop example completed successfully")
}
}

extension Environment {
/// Account ID for the operator to use in this example.
internal var operatorAccountId: AccountId {
AccountId(self["OPERATOR_ID"]!.stringValue)!
}

/// Private key for the operator to use in this example.
internal var operatorKey: PrivateKey {
PrivateKey(self["OPERATOR_KEY"]!.stringValue)!
}

/// The name of the hedera network this example should be ran against.
///
/// Testnet by default.
internal var networkName: String {
self["HEDERA_NETWORK"]?.stringValue ?? "testnet"
}
}
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ let exampleTargets = [
"ValidateChecksum",
"TokenUpdateMetadata",
"NftUpdateMetadata",
"TokenAirdrop",
].map { name in
Target.executableTarget(
name: "\(name)Example",
Expand Down
21 changes: 18 additions & 3 deletions Sources/Hedera/AnyTransaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ internal enum ServicesTransactionDataList {
case nodeCreate([Com_Hedera_Hapi_Node_Addressbook_NodeCreateTransactionBody])
case nodeUpdate([Com_Hedera_Hapi_Node_Addressbook_NodeUpdateTransactionBody])
case nodeDelete([Com_Hedera_Hapi_Node_Addressbook_NodeDeleteTransactionBody])
case tokenAirdrop([Proto_TokenAirdropTransactionBody])
case tokenClaimAirdrop([Proto_TokenClaimAirdropTransactionBody])
case tokenCancelAirdrop([Proto_TokenCancelAirdropTransactionBody])

internal mutating func append(_ transaction: Proto_TransactionBody.OneOf_Data) throws {
switch (self, transaction) {
Expand Down Expand Up @@ -255,6 +258,18 @@ internal enum ServicesTransactionDataList {
array.append(data)
self = .nodeDelete(array)

case (.tokenAirdrop(var array), .tokenAirdrop(let data)):
array.append(data)
self = .tokenAirdrop(array)

case (.tokenClaimAirdrop(var array), .tokenClaimAirdrop(let data)):
array.append(data)
self = .tokenClaimAirdrop(array)

case (.tokenCancelAirdrop(var array), .tokenCancelAirdrop(let data)):
array.append(data)
self = .tokenCancelAirdrop(array)

default:
throw HError.fromProtobuf("mismatched transaction types")
}
Expand Down Expand Up @@ -323,9 +338,9 @@ extension ServicesTransactionDataList: TryFromProtobuf {
case .nodeUpdate(let data): value = .nodeUpdate([data])
case .nodeDelete(let data): value = .nodeDelete([data])
case .tokenReject(let data): value = .tokenReject([data])
case .tokenAirdrop: throw HError.fromProtobuf("Unsupported transaction `TokenAirdropTransaction`")
case .tokenCancelAirdrop: throw HError.fromProtobuf("Unsupported transaction `TokenCancelAirdropTransaction`")
case .tokenClaimAirdrop: throw HError.fromProtobuf("Unsupported transaction `TokenClaimAirdropTransaction`")
case .tokenAirdrop(let data): value = .tokenAirdrop([data])
case .tokenCancelAirdrop(let data): value = .tokenCancelAirdrop([data])
case .tokenClaimAirdrop(let data): value = .tokenClaimAirdrop([data])
}

for transaction in iter {
Expand Down
Loading

0 comments on commit 89d96b5

Please sign in to comment.