Skip to content

Commit

Permalink
feat(kotlin): setup initial kotlin SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
mehcode committed Jun 9, 2022
1 parent 143ed31 commit ec79c78
Show file tree
Hide file tree
Showing 15 changed files with 368 additions and 6 deletions.
8 changes: 3 additions & 5 deletions sdk/kotlin/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# Ignore Gradle project-specific cache directory
.gradle

# Ignore Gradle build output directory
build
.gradle/
build/
src/main/resources/com/hedera/hashgraph/sdk/native
4 changes: 4 additions & 0 deletions sdk/kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ repositories {
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("com.github.jnr:jnr-ffi:2.2.12")
implementation("com.fasterxml.jackson.core:jackson-core:2.13.3")
implementation("com.fasterxml.jackson.core:jackson-annotations:2.13.3")
implementation("com.fasterxml.jackson.core:jackson-databind:2.13.3")
}
31 changes: 31 additions & 0 deletions sdk/kotlin/examples/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
plugins {
`java-library`
}

repositories {
mavenCentral()
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

dependencies {
implementation(rootProject)
implementation("com.fasterxml.jackson.core:jackson-databind:2.13.3")
}

tasks.addRule("Pattern: run<Example>: Runs an example.") {
val taskPattern = this

if (taskPattern.startsWith("run")) {
val taskName = taskPattern.removePrefix("run") + "Example"

task<JavaExec>(taskPattern) {
mainClass.set(taskName)
classpath = sourceSets["main"].runtimeClasspath
standardInput = System.`in`
}
}
}
16 changes: 16 additions & 0 deletions sdk/kotlin/examples/src/main/java/GetAccountBalanceExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import com.hedera.hashgraph.sdk.AccountBalanceQuery;
import com.hedera.hashgraph.sdk.AccountId;
import com.hedera.hashgraph.sdk.Client;

class GetAccountBalanceExample {
public static void main(String[] args) {
var client = Client.forTestnet();

var query = new AccountBalanceQuery();
query.setAccountId(AccountId.parse("0.0.1001"));

var response = query.execute(client);

System.out.printf("balance = %s\n", response.balance);
}
}
1 change: 1 addition & 0 deletions sdk/kotlin/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
rootProject.name = "hedera-sdk-kotlin"
include("examples")
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.hedera.hashgraph.sdk

/**
* Either `AccountId` or `AccountAlias`. Some transactions and queries
* accept `AccountAddress` as an input. All transactions and queries
* return only `AccountId` as an output, however.
*/
sealed class AccountAddress constructor(val shard: Long, val realm: Long)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.hedera.hashgraph.sdk

import com.fasterxml.jackson.annotation.JsonValue

/**
* The unique identifier for a cryptocurrency account represented with an
* alias instead of an account number.
*/
class AccountAlias(shard: Long, realm: Long, val alias: PublicKey?) : AccountAddress(shard, realm) {
constructor(alias: PublicKey?) : this(0, 0, alias)

@JsonValue
override fun toString(): String {
return String.format("%d.%d.%s", shard, realm, alias)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.hedera.hashgraph.sdk

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonTypeName

/**
* Get the balance of a cryptocurrency account.
*
* This returns only the balance, so it is a smaller reply
* than `AccountInfoQuery`, which returns the balance plus
* additional information.
*/
@JsonTypeName("accountBalance")
class AccountBalanceQuery : Query<AccountBalanceResponse>(AccountBalanceResponse::class.java) {
/**
* The account ID for which information is requested.
*/
@JsonProperty
var accountId: AccountAddress? = null

/**
* The contract ID for which information is requested.
*/
@JsonProperty
var contractId: AccountAddress? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.hedera.hashgraph.sdk

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty

// TODO: Hbar
// TODO: tokens
@JsonIgnoreProperties("\$type")
class AccountBalanceResponse {
@JsonProperty
@JvmField
val accountId: AccountId? = null

@JsonProperty
@JvmField
val balance: Long = 0
}
35 changes: 35 additions & 0 deletions sdk/kotlin/src/main/kotlin/com/hedera/hashgraph/sdk/AccountId.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.hedera.hashgraph.sdk

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonValue
import jnr.ffi.byref.NativeLongByReference

/**
* The unique identifier for a cryptocurrency account on Hedera.
*/
class AccountId(shard: Long, realm: Long, val num: Long) : AccountAddress(shard, realm) {
constructor(num: Long) : this(0, 0, num)

@JsonValue
override fun toString(): String {
return String.format("%d.%d.%d", shard, realm, num)
}

companion object {
@JvmStatic
@JsonCreator
fun parse(s: String?): AccountId {
val shard = NativeLongByReference()
val realm = NativeLongByReference()
val num = NativeLongByReference()

val err = CHedera.instance.hedera_entity_id_from_string(s, shard, realm, num)

if (err != CHedera.Error.OK) {
throw RuntimeException("oh no")
}

return AccountId(shard.toLong(), realm.toLong(), num.toLong())
}
}
}
128 changes: 128 additions & 0 deletions sdk/kotlin/src/main/kotlin/com/hedera/hashgraph/sdk/CHedera.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.hedera.hashgraph.sdk

import jnr.ffi.LibraryLoader
import jnr.ffi.LibraryOption
import jnr.ffi.Pointer
import jnr.ffi.annotations.Delegate
import jnr.ffi.annotations.In
import jnr.ffi.annotations.Out
import jnr.ffi.byref.NativeLongByReference
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.*

internal open class CHedera {
companion object {
val instance: LibHedera

init {
var os = System.getProperty("os.name").lowercase(Locale.getDefault())
val arch = System.getProperty("os.arch").lowercase(Locale.getDefault())
var libraryName: String

if (os.contains("win")) {
os = "windows"
libraryName = "hedera.dll"
} else if (os.contains("mac")) {
os = "macos"
libraryName = "libhedera.dylib"
} else {
os = "linux"
libraryName = "libhedera.so"
}

val resourceName = String.format("com/hedera/hashgraph/sdk/native/%s/%s/%s", os, arch, libraryName)

try {
ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName).use { stream ->
if (stream == null) {
throw RuntimeException(String.format("unsupported platform, os: %s, arch: %s", os, arch))
}

val temporaryFile = File.createTempFile("chedera", libraryName)
temporaryFile.deleteOnExit()

Files.copy(stream, temporaryFile.toPath(), StandardCopyOption.REPLACE_EXISTING)

instance = LibraryLoader.create(LibHedera::class.java)
.option(LibraryOption.LoadNow, true)
.option(LibraryOption.IgnoreError, true)
.failImmediately()
.load(temporaryFile.absolutePath)
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

/**
* Represents any possible result from a fallible function in the Hedera SDK.
*/
enum class Error {
OK,
TIMED_OUT,
GRPC_STATUS,
FROM_PROTOBUF,
TRANSACTION_PRE_CHECK_STATUS,
TRANSACTION_NO_ID_PRE_CHECK_STATUS,
QUERY_PRE_CHECK_STATUS,
QUERY_PAYMENT_PRE_CHECK_STATUS,
QUERY_NO_PAYMENT_PRE_CHECK_STATUS,
BASIC_PARSE,
KEY_PARSE,
NO_PAYER_ACCOUNT_OR_TRANSACTION_ID,
MAX_QUERY_PAYMENT_EXCEEDED,
NODE_ACCOUNT_UNKNOWN,
RESPONSE_STATUS_UNRECOGNIZED,
RECEIPT_STATUS,
SIGNATURE,
REQUEST_PARSE
}

fun interface Callback {
@Delegate
operator fun invoke(context: Pointer?, error: Error?, response: String?)
}

@Suppress("FunctionName")
interface LibHedera {
/**
* Construct a Hedera client pre-configured for testnet access.
*/
fun hedera_client_for_testnet(): Pointer?

/**
* Release memory associated with the previously-opened Hedera client.
*/
fun hedera_client_free(@In client: Pointer?)

/**
* Parse a Hedera `EntityId` from the passed string.
*/
fun hedera_entity_id_from_string(
@In s: String?,
@Out shard: NativeLongByReference?,
@Out realm: NativeLongByReference?,
@Out num: NativeLongByReference?
): Error?

/**
* Returns English-language text that describes the last error.
* Undefined if there has been no last error.
*/
fun hedera_error_message(): String?

/**
* Execute this request against the provided client of the Hedera network.
*/
fun hedera_execute(
@In client: Pointer?,
@In request: String?,
@In context: Pointer?,
@In callback: Callback?
): Error?
}
}
12 changes: 12 additions & 0 deletions sdk/kotlin/src/main/kotlin/com/hedera/hashgraph/sdk/Client.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hedera.hashgraph.sdk

import jnr.ffi.Pointer

class Client private constructor(val ptr: Pointer) {
companion object {
@JvmStatic
fun forTestnet(): Client {
return Client(CHedera.instance.hedera_client_for_testnet()!!)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package com.hedera.hashgraph.sdk;
/**
* A public key on the Hedera network.
*/
public final class PublicKey {
class PublicKey {
}
60 changes: 60 additions & 0 deletions sdk/kotlin/src/main/kotlin/com/hedera/hashgraph/sdk/Query.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.hedera.hashgraph.sdk

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException

/**
* A query that can be executed on the Hedera network.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "\$type")
open class Query<Response> protected constructor(private val responseClass: Class<Response>) {
fun execute(client: Client): Response {
val objectMapper = ObjectMapper()
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)

val request: String = try {
objectMapper.writeValueAsString(this)
} catch (e: JsonProcessingException) {
// BUG: should never happen if our serialization configuration is sane
throw RuntimeException(e)
}

val completableFuture = CompletableFuture<String?>()

val executeErr = CHedera.instance.hedera_execute(client.ptr, request, null) { _, responseErr, response ->
if (responseErr !== CHedera.Error.OK) {
// TODO: translate error to exception
System.out.printf("ERROR hedera_execute callback invoked with error, %s\n", responseErr)

// TODO: completableFuture.completeExceptionally();
return@hedera_execute
}

completableFuture.complete(response)
}

if (executeErr !== CHedera.Error.OK) {
// TODO: translate error to exception
System.out.printf("ERROR hedera_execute returned with error, %s\n", executeErr)
throw RuntimeException()
}

val response: String? = try {
completableFuture.get()
} catch (e: InterruptedException) {
throw RuntimeException(e)
} catch (e: ExecutionException) {
throw RuntimeException(e)
}

return try {
objectMapper.readValue(response, responseClass)
} catch (e: JsonProcessingException) {
throw RuntimeException(e)
}
}
}
Loading

0 comments on commit ec79c78

Please sign in to comment.