Skip to content

Commit

Permalink
Add PDAs (#28)
Browse files Browse the repository at this point in the history
* add temporary salt impl

* program interface + pda

* switch to lib + cleanup

* more cleanup

* refactor

* remove print
  • Loading branch information
Funkatronics authored Jun 13, 2024
1 parent c083872 commit 0934d1d
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 3 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref =
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
multimult = { group = "io.github.funkatronics", name = "multimult", version = "0.2.3" }
rpc-core = { group = "com.solanamobile", name = "rpc-core", version = "0.2.5" }
salkt = { group = "io.github.funkatronics", name = "salkt", version = "0.1.0" }

[plugins]
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
Expand Down
1 change: 1 addition & 0 deletions solana/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ kotlin {
implementation(libs.kotlinx.serialization.json)
implementation(libs.borsh)
implementation(libs.multimult)
implementation(libs.salkt)
}
}
val commonTest by getting {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.solana.transaction.AccountMeta
import com.solana.transaction.TransactionInstruction
import kotlin.jvm.JvmStatic

object MemoProgram {
object MemoProgram : Program {
@JvmStatic
val PROGRAM_ID = SolanaPublicKey.from("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")

Expand All @@ -15,4 +15,6 @@ object MemoProgram {
listOf(AccountMeta(account, true, true)),
memo.encodeToByteArray()
)

override val programId = PROGRAM_ID
}
41 changes: 41 additions & 0 deletions solana/src/commonMain/kotlin/com/solana/programs/Program.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.solana.programs

import com.funkatronics.hash.Sha256
import com.funkatronics.salt.isOnCurve
import com.solana.publickey.ProgramDerivedAddress
import com.solana.publickey.PublicKey
import com.solana.publickey.SolanaPublicKey
import kotlin.jvm.JvmStatic

interface Program {
val programId: SolanaPublicKey

suspend fun createDerivedAddress(seeds: List<ByteArray>) =
createDerivedAddress(seeds, programId)

suspend fun findDerivedAddress(seeds: List<ByteArray>) =
findDerivedAddress(seeds, programId)

companion object {
@JvmStatic
suspend fun findDerivedAddress(seeds: List<ByteArray>, programId: PublicKey) =
ProgramDerivedAddress.find(seeds, programId)

@JvmStatic
suspend fun createDerivedAddress(seeds: List<ByteArray>, programId: PublicKey): Result<SolanaPublicKey> {
val address = Sha256.hash(
seeds.foldIndexed(ByteArray(0)) { i, a, s ->
require(s.size <= 32) { "Seed length must be <= 32 bytes" }; a + s
} + programId.bytes + "ProgramDerivedAddress".encodeToByteArray()
)

if (address.isOnCurve()) {
return Result.failure(
IllegalArgumentException("Invalid seeds, address must fall off curve")
)
}

return Result.success(SolanaPublicKey(address))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.solana.transaction.TransactionInstruction
import kotlinx.serialization.builtins.ByteArraySerializer
import kotlin.jvm.JvmStatic

object SystemProgram {
object SystemProgram : Program {
@JvmStatic
val PROGRAM_ID = SolanaPublicKey.from("11111111111111111111111111111111")

Expand Down Expand Up @@ -51,4 +51,6 @@ object SystemProgram {
encodeSerializableValue(ByteArraySerializer(), programId.bytes)
}.borshEncodedBytes
)

override val programId = PROGRAM_ID
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.solana.publickey

import com.funkatronics.salt.isOnCurve

suspend fun PublicKey.isOnCurve() = bytes.isOnCurve()
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.solana.publickey

import com.funkatronics.salt.isOnCurve
import com.solana.programs.Program
import kotlin.jvm.JvmStatic

class ProgramDerivedAddress private constructor(bytes: ByteArray, val nonce: UByte) : SolanaPublicKey(bytes) {

private constructor(publicKey: PublicKey, nonce: UByte) : this(publicKey.bytes, nonce)

companion object {
@JvmStatic
suspend fun find(seeds: List<ByteArray>, programId: PublicKey): Result<ProgramDerivedAddress> {
for (bump in 255 downTo 0) {
val result = Program.createDerivedAddress(seeds + byteArrayOf(bump.toByte()), programId)
if (result.isSuccess) return result.map { ProgramDerivedAddress(it, bump.toUByte()) }
}
return Result.failure(Error("Unable to find valid derived address for provided seeds"))
}

@JvmStatic
suspend fun create(bytes: ByteArray, nonce: UByte): ProgramDerivedAddress {
require(!bytes.isOnCurve()) { "Provided public key is not a PDA, address must be off Ed25519 curve" }
return ProgramDerivedAddress(bytes, nonce)
}

@JvmStatic
suspend fun create(publicKey: PublicKey, nonce: UByte) =
ProgramDerivedAddress(publicKey.bytes, nonce)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Serializable(with=SolanaPublicKeySerializer::class)
open class SolanaPublicKey(override val bytes: ByteArray) : PublicKey {
open class SolanaPublicKey(final override val bytes: ByteArray) : PublicKey {

init {
check (bytes.size == PUBLIC_KEY_LENGTH)
Expand All @@ -28,6 +28,10 @@ open class SolanaPublicKey(override val bytes: ByteArray) : PublicKey {
override fun equals(other: Any?): Boolean {
return (other is PublicKey) && this.bytes.contentEquals(other.bytes)
}

override fun hashCode(): Int = bytes.contentHashCode()

override fun toString() = base58()
}

object SolanaPublicKeySerializer : KSerializer<SolanaPublicKey> {
Expand Down
80 changes: 80 additions & 0 deletions solana/src/commonTest/kotlin/com/solana/programs/ProgramTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.solana.programs

import com.solana.publickey.SolanaPublicKey
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class ProgramTests {

@Test
fun `createProgramAddress returns failure for on chain pubkey`() = runTest {
// given
val seeds = listOf("helloWorld".encodeToByteArray(), byteArrayOf(255.toByte()))
val program = object : Program {
override val programId = SolanaPublicKey.from("11111111111111111111111111111111")
}

// when
val result = program.createDerivedAddress(seeds)

// then
assertTrue { result.isFailure }
}

@Test
fun `createProgramAddress returns expected pubkey for nonce`() = runTest {
// given
val seeds = listOf("helloWorld".encodeToByteArray(), byteArrayOf(252.toByte()))
val expectedPublicKey = SolanaPublicKey.from("THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHH")
val program = object : Program {
override val programId = SolanaPublicKey.from("11111111111111111111111111111111")
}

// when
val result = program.createDerivedAddress(seeds)

// then
assertTrue { result.isSuccess }
assertEquals(expectedPublicKey, result.getOrNull()!!)
}

@Test
fun `findProgramAddress returns expected pubkey and nonce`() = runTest {
// given
val seeds = listOf<ByteArray>()
val expectedBump = 255.toUByte()
val expectedPublicKey = SolanaPublicKey.from("Cu7NwqCXSmsR5vgGA3Vw9uYVViPi3kQvkbKByVQ8nPY9")
val program = object : Program {
override val programId = SolanaPublicKey.from("11111111111111111111111111111111")
}

// when
val result = program.findDerivedAddress(seeds)

// then
assertTrue { result.isSuccess }
assertEquals(expectedPublicKey, result.getOrNull()!!)
assertEquals(expectedBump, result.getOrNull()!!.nonce)
}

@Test
fun `findProgramAddress returns expected pubkey and nonce for seeds`() = runTest {
// given
val seeds = listOf("helloWorld".encodeToByteArray())
val expectedBump = 254.toUByte()
val expectedPublicKey = SolanaPublicKey.from("46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X")
val program = object : Program {
override val programId = SolanaPublicKey.from("11111111111111111111111111111111")
}

// when
val result = program.findDerivedAddress(seeds)

// then
assertTrue { result.isSuccess }
assertEquals(expectedPublicKey, result.getOrNull()!!)
assertEquals(expectedBump, result.getOrNull()!!.nonce)
}
}

0 comments on commit 0934d1d

Please sign in to comment.