Skip to content

Lets you write Kotlin apps which interact with the Internet Computer

Notifications You must be signed in to change notification settings

seniorjoinu/candid-kt

Repository files navigation

Release

Candid-kt

Generates client code for your canisters

Usage

Use the gradle plugin to generate Kotlin code out of candid code.

For example, this candid code

type Phone = nat;
type Name = text;
type Entry = 
 record {
   description: text;
   name: Name;
   phone: Phone;
 };
service : {
  insert: (Name, text, Phone) -> ();
  lookup: (Name) -> (opt Entry) query;
}

would generate this Kotlin code

typealias Phone = BigInteger

val PhoneValueSer: ValueSer<BigInteger> = NatValueSer

typealias Name = String

val NameValueSer: ValueSer<String> = TextValueSer

data class Entry(
    val name: Name,
    val description: String,
    val phone: Phone
)

object EntryValueSer : ValueSer<Entry> {
    val nameValueSer: ValueSer<Name> = NameValueSer

    val descriptionValueSer: ValueSer<String> = TextValueSer

    val phoneValueSer: ValueSer<Phone> = PhoneValueSer

    override fun calcSizeBytes(value: Entry): Int = this.nameValueSer.calcSizeBytes(value.name) +
            this.descriptionValueSer.calcSizeBytes(value.description) +
            this.phoneValueSer.calcSizeBytes(value.phone)

    override fun ser(buf: ByteBuffer, value: Entry) {
        this.nameValueSer.ser(buf, value.name)
        this.descriptionValueSer.ser(buf, value.description)
        this.phoneValueSer.ser(buf, value.phone)
    }

    override fun deser(buf: ByteBuffer): Entry = Entry(this.nameValueSer.deser(buf),
        this.descriptionValueSer.deser(buf), this.phoneValueSer.deser(buf))

    override fun poetize(): String = Code.of("%T", EntryValueSer::class)
}

typealias PhonebookServiceValueSer = ServiceValueSer

typealias AnonFunc0ValueSer = FuncValueSer

class AnonFunc0(
    funcName: String?,
    service: SimpleIDLService?
) : SimpleIDLFunc(funcName, service) {
    suspend operator fun invoke(
        arg0: Name,
        arg1: String,
        arg2: Phone
    ) {
        val arg0ValueSer = NameValueSer
        val arg1ValueSer = senior.joinu.candid.serialize.TextValueSer
        val arg2ValueSer = PhoneValueSer
        val valueSizeBytes = 0 + arg0ValueSer.calcSizeBytes(arg0) + arg1ValueSer.calcSizeBytes(arg1) +
                arg2ValueSer.calcSizeBytes(arg2)
        val sendBuf = ByteBuffer.allocate(staticPayload.size + valueSizeBytes)
        sendBuf.order(ByteOrder.LITTLE_ENDIAN)
        sendBuf.put(staticPayload)
        arg0ValueSer.ser(sendBuf, arg0)
        arg1ValueSer.ser(sendBuf, arg1)
        arg2ValueSer.ser(sendBuf, arg2)
        val sendBytes = sendBuf.array()

        val receiveBytes = this.service!!.call(this.funcName!!, sendBytes)
        val receiveBuf = ByteBuffer.wrap(receiveBytes)
        receiveBuf.order(ByteOrder.LITTLE_ENDIAN)
        receiveBuf.rewind()
        val deserContext = TypeDeser.deserUntilM(receiveBuf)
    }

    companion object {
        val staticPayload: ByteArray = Base64.getDecoder().decode("RElETAADcXF9")
    }
}

typealias AnonFunc1ValueSer = FuncValueSer

class AnonFunc1(
    funcName: String?,
    service: SimpleIDLService?
) : SimpleIDLFunc(funcName, service) {
    suspend operator fun invoke(arg0: Name): Entry? {
        val arg0ValueSer = NameValueSer
        val valueSizeBytes = 0 + arg0ValueSer.calcSizeBytes(arg0)
        val sendBuf = ByteBuffer.allocate(staticPayload.size + valueSizeBytes)
        sendBuf.order(ByteOrder.LITTLE_ENDIAN)
        sendBuf.put(staticPayload)
        arg0ValueSer.ser(sendBuf, arg0)
        val sendBytes = sendBuf.array()

        val receiveBytes = this.service!!.query(this.funcName!!, sendBytes)
        val receiveBuf = ByteBuffer.wrap(receiveBytes)
        receiveBuf.order(ByteOrder.LITTLE_ENDIAN)
        receiveBuf.rewind()
        val deserContext = TypeDeser.deserUntilM(receiveBuf)
        return senior.joinu.candid.serialize.OptValueSer( EntryValueSer ).deser(receiveBuf) as Entry?
    }

    companion object {
        val staticPayload: ByteArray = Base64.getDecoder().decode("RElETAABcQ==")
    }
}

class PhonebookService(
    host: String,
    canisterId: SimpleIDLPrincipal?,
    keyPair: EdDSAKeyPair?,
    apiVersion: String = "v1"
) : SimpleIDLService(host, canisterId, keyPair, apiVersion) {
    val insert: AnonFunc0 = AnonFunc0("insert", this)

    val lookup: AnonFunc1 = AnonFunc1("lookup", this)
}

which we then can use to interact with our deployed canister

val host = "http://localhost:8000"
val keys = EdDSAKeyPair.generateInsecure()
val canisterId = "75hes-oqbaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-q"

val phonebook = PhonebookService(host, SimpleIDLPrincipal.fromText(canisterId), keys)

phonebook.insert("test", "test desc", BigInteger("12345"))
val entry = phonebook.lookup("test")

check(entry != null) { "Entry not found" } 

Pros

  • Idiomatic Kotlin
  • Complete type-safety
  • Asynchronous io with coroutines
  • Reflectionless single-allocation (de)serialization

Cons

  • Unstable

Type conversion rules

IDL Kotlin
type T = "existing type" typealias T = "existing type"
int, nat BigInteger
int8, nat8 Byte
int16, nat16 Short
int32, nat32 Int
int64, nat64 Long
float32 Float
float64 Double
bool Boolean
text String
null Null object
reserved Reserved object
empty Empty object
opt T T?
vec T List<T>
type T = record { a: T1, b: T2 } data class T(val a: T1, val b: T2)
type T = variant { A, B: T1 } sealed class T { data class A: T(); data class B(val value: T1): T() }
type T = func (T1) -> T2 class T { suspend operator fun invoke(arg0: T1): T2 }
type T = service { a: SomeFunc } class T { val a: SomeFunc }
principal Principal class
Unnamed IDL types are transpiled into anonymous Kotlin types.