Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Javascript target #76

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
/captures
.externalNativeBuild
.cxx
/.kotlin/
588 changes: 588 additions & 0 deletions kotlin-js-store/yarn.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,22 @@ coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", ver
coroutinesJdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "kotlinxCoroutines" }
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
kotlinLogging = { module = "io.github.oshai:kotlin-logging", version = "6.0.9" }
ktorClientContentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktorClientCio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktorClientCore = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktorClientDarwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktorClientLogging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktorClientOkHttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktorClientWinHttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" }
ktorClientJs = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
ktorSerializationKotlinxJson = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktorUtils = { module = "io.ktor:ktor-utils", version.ref = "ktor" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttpLoggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.13" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version = "2.0.13" }
serializationCore = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" }
serializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
skie-configurationAnnotations = { module = "co.touchlab.skie:configuration-annotations", version.ref = "skie" }
Expand Down
40 changes: 39 additions & 1 deletion solana-kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import co.touchlab.skie.configuration.ClassInterop
import co.touchlab.skie.configuration.DefaultArgumentInterop
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType

plugins {
alias(libs.plugins.kotlinMultiplatform)
Expand Down Expand Up @@ -41,14 +42,44 @@ kotlin {
mingwX64()
linuxX64()

js(KotlinJsCompilerType.IR) {
browser {
compilations.all {
kotlinOptions {
sourceMap = true
sourceMapEmbedSources = "always"
}
}
testTask {
useMocha {
timeout = "60s"
}
}
}
nodejs {
compilations.all {
kotlinOptions {
sourceMap = true
sourceMapEmbedSources = "always"
}
}
}
generateTypeScriptDefinitions()
}

sourceSets {
val jvmMain by getting {
dependencies {
implementation(libs.ktorClientOkHttp)
implementation(libs.bouncyCastle)
implementation(libs.slf4j.api)
}
}
val jvmTest by getting {
dependencies {
implementation(libs.slf4j.simple)
}
}
val jvmTest by getting

val commonMain by getting {
dependencies {
Expand All @@ -62,6 +93,7 @@ kotlin {
implementation(libs.kermit)
implementation(libs.okio)
implementation(libs.skie.configurationAnnotations)
implementation(libs.kotlinLogging)
}
}
val commonTest by getting {
Expand Down Expand Up @@ -93,6 +125,12 @@ kotlin {
implementation(libs.ktorClientWinHttp)
}
}

val jsMain by getting {
dependencies {
implementation(libs.ktorClientJs)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class AssociatedTokenProgramTest {
fun testFindProgramAddress() {
for (i in 0..1000) {
val programId = randomKey().publicKey
println("programId: $programId")

val seeds = listOf(
"Lil'".encodeToByteArray(),
Expand All @@ -21,15 +20,12 @@ class AssociatedTokenProgramTest {
seeds = seeds,
programId = programId,
)
println("address: $address")

val created = Program.createProgramAddress(
seeds = seeds + byteArrayOf(address.nonce.toByte()),
programId = programId,
)

println("created: $created")

assertEquals(address.address, created)
}
}
Expand All @@ -46,7 +42,6 @@ class AssociatedTokenProgramTest {
val wallet = PublicKey.fromBase58(wallet)

val associated = wallet.associatedTokenAddress(mint)
println(associated)

val expectedAssociated = PublicKey.fromBase58(expectedAssociated)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,128 @@
package net.avianlabs.solana.domain.program

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.test.runTest
import net.avianlabs.solana.SolanaClient
import net.avianlabs.solana.client.RpcKtorClient
import net.avianlabs.solana.domain.core.Commitment
import net.avianlabs.solana.domain.core.Transaction
import net.avianlabs.solana.domain.core.TransactionBuilder
import net.avianlabs.solana.domain.core.decode
import net.avianlabs.solana.methods.*
import net.avianlabs.solana.tweetnacl.TweetNaCl
import net.avianlabs.solana.tweetnacl.ed25519.Ed25519Keypair
import kotlin.random.Random
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.time.Duration.Companion.seconds

private val logger = KotlinLogging.logger { }

class SystemProgramTest {

@Test
@Ignore
fun testCreateDurableNonceAccount() = runBlocking {
@Test
fun testCreateDurableNonceAccount() = runTest {
val client = SolanaClient(client = RpcKtorClient("http://localhost:8899"))
val owner = TweetNaCl.Signature.generateKey(Random.nextBytes(32))
logger.info { "Keypair: ${owner.publicKey}" }
val nonce1 = TweetNaCl.Signature.generateKey(Random.nextBytes(32))
logger.info { "Nonce account 1: ${nonce1.publicKey}" }
val nonce2 = TweetNaCl.Signature.generateKey(Random.nextBytes(32))
logger.info { "Nonce account 2: ${nonce2.publicKey}" }
val destination = TweetNaCl.Signature.generateKey(Random.nextBytes(32))
logger.info { "Destination: ${destination.publicKey}" }

val airdrop = client.requestAirdrop(owner.publicKey, 2_000_000_000)
val airdrop2 = client.requestAirdrop(destination.publicKey, 2_000_000_000)
client.waitForTransaction(airdrop, airdrop2)

val balance = client.getBalance(owner.publicKey)
logger.info { "Balance: $balance" }

val nonce1tx = client.createNonceAccountSigned(
keypair = owner,
nonceAccount = nonce1,
)
val init1Sig = client.sendTransaction(nonce1tx)

logger.info { "Initialized nonce account 1: $init1Sig" }

val nonce2tx = client.createNonceAccountSigned(
keypair = owner,
nonceAccount = nonce2,
)

val init2Sig = client.sendTransaction(nonce2tx)

logger.info { "Initialized nonce account 2: $init2Sig" }

client.waitForTransaction(init1Sig, init2Sig)

val keypair = TweetNaCl.Signature.generateKey(Random.nextBytes(32))
println("Keypair: ${keypair.publicKey}")
val nonceAccount = TweetNaCl.Signature.generateKey(Random.nextBytes(32))
println("Nonce account: ${nonceAccount.publicKey}")
val nonceAccount1 = client.getNonce(nonce1.publicKey, Commitment.Confirmed)
logger.info { "Nonce account 1 info: $nonceAccount1" }

client.requestAirdrop(keypair.publicKey, 2_000_000_000)
delay(15.seconds)
val balance = client.getBalance(keypair.publicKey)
println("Balance: $balance")
val tx1 = transferTransaction(
nonceKeypair = nonce1,
owner = owner,
destination = destination,
nonceAccount = nonceAccount1,
)

val rentExempt = client.getMinimumBalanceForRentExemption(SystemProgram.NONCE_ACCOUNT_LENGTH)
val nonceAccount2 = client.getNonce(nonce2.publicKey, Commitment.Confirmed)
logger.info { "Nonce account 2 info: $nonceAccount2" }

val tx2 = transferTransaction(
nonceKeypair = nonce2,
owner = owner,
destination = destination,
nonceAccount = nonceAccount2,
)

val tx2Sig = client.sendTransaction(tx2)

client.waitForTransaction(tx2Sig)

val tx1Sig = client.sendTransaction(tx1)
logger.info { "Advanced nonce account: $tx1Sig" }

client.waitForTransaction(tx1Sig)

val newNonce = client.getNonce(nonce1.publicKey, Commitment.Confirmed)
logger.info { "New nonce: ${newNonce?.nonce}" }
}

private fun transferTransaction(
nonceKeypair: Ed25519Keypair,
owner: Ed25519Keypair,
destination: Ed25519Keypair,
nonceAccount: NonceAccount?
) = TransactionBuilder()
.addInstruction(
SystemProgram.nonceAdvance(
nonceAccount = nonceKeypair.publicKey,
authorized = owner.publicKey,
)
)
.addInstruction(
SystemProgram.transfer(
fromPublicKey = owner.publicKey,
toPublicKey = destination.publicKey,
lamports = 1_000,
)
)
.setRecentBlockHash(nonceAccount!!.nonce)
.build()
.sign(owner)

val blockhash = client.getRecentBlockhash()
private suspend fun SolanaClient.createNonceAccountSigned(
keypair: Ed25519Keypair,
nonceAccount: Ed25519Keypair
): Transaction {
val rentExempt = getMinimumBalanceForRentExemption(SystemProgram.NONCE_ACCOUNT_LENGTH)

val blockhash = getRecentBlockhash()

val initTransaction = Transaction()
.addInstruction(
Expand All @@ -53,46 +141,21 @@ class SystemProgramTest {
)
.setRecentBlockHash(blockhash.blockhash)
.sign(listOf(keypair, nonceAccount))
return initTransaction
}


val initSignature = client.sendTransaction(initTransaction)

println("Initialized nonce account: $initSignature")
delay(15.seconds)

val lamportsPerSignature = client.getFeeForMessage(initTransaction.message.serialize())
println("Lamports per signature: $lamportsPerSignature")

val nonce = client.getNonce(nonceAccount.publicKey, Commitment.Processed)
println("Nonce account info: $nonce")

val testTransaction = TransactionBuilder()
.addInstruction(
SystemProgram.nonceAdvance(
nonceAccount = nonceAccount.publicKey,
authorized = keypair.publicKey,
)
)
.addInstruction(
SystemProgram.transfer(
fromPublicKey = keypair.publicKey,
toPublicKey = nonceAccount.publicKey,
lamports = 1_000_000_000,
)
)
.setRecentBlockHash(nonce!!.nonce)
.build()
.sign(keypair)

val testSignature = client.sendTransaction(testTransaction)
println("Advanced nonce account: $testSignature")

delay(15.seconds)

val testTxInfo = client.getTransaction(testSignature, Commitment.Confirmed)
println("Transaction info: ${testTxInfo?.decode()}")

val newNonce = client.getNonce(nonceAccount.publicKey, Commitment.Processed)
println("New nonce account info: $newNonce")
private suspend fun SolanaClient.waitForTransaction(
vararg signatures: String,
commitment: Commitment = Commitment.Finalized,
) = coroutineScope {
signatures.map { signature ->
async {
var transactionResponse: TransactionResponse?
do {
transactionResponse = getTransaction(signature, commitment)
} while (transactionResponse == null)
logger.info { "Transaction $commitment $signature" }
}
}.awaitAll()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import net.avianlabs.solana.domain.program.ProgramDerivedAddress
import net.avianlabs.solana.domain.program.associatedTokenAddress
import net.avianlabs.solana.domain.randomKey
import net.avianlabs.solana.tweetnacl.ed25519.PublicKey
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class IsOnCurveTest {
@Test
fun testIsOnCurve() {
val offCurve = PublicKey.fromBase58("12rqwuEgBYiGhBrDJStCiqEtzQpTTiZbh7teNVLuYcFA")
assertFalse(offCurve.isOnCurve())

val onCurve = randomKey().publicKey
assertTrue(onCurve.isOnCurve())
}

@Test
fun testParallelAssociatedAddress() = runTest {
val x = PublicKey.fromBase58("4rZoSK72jVaAW1ayZLrefdMPAAStRVhCfH1PSundaoNt")
Expand Down
27 changes: 27 additions & 0 deletions solana-kotlin/src/commonTest/resources/simplelogger.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# SLF4J's SimpleLogger configuration file
# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err.
# Default logging detail level for all instances of SimpleLogger.
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, defaults to "info".
org.slf4j.simpleLogger.defaultLogLevel=debug
# Logging detail level for a SimpleLogger instance named "xxxxx".
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, the default logging detail level is used.
#org.slf4j.simpleLogger.log.xxxxx=
# Set to true if you want the current date and time to be included in output messages.
# Default is false, and will output the number of milliseconds elapsed since startup.
org.slf4j.simpleLogger.showDateTime=true
# The date and time format to be used in the output messages.
# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat.
# If the format is not specified or is invalid, the default format is used.
# The default format is yyyy-MM-dd HH:mm:ss:SSS Z.
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z
# Set to true if you want to output the current thread name.
# Defaults to true.
org.slf4j.simpleLogger.showThreadName=false
# Set to true if you want the Logger instance name to be included in output messages.
# Defaults to true.
org.slf4j.simpleLogger.showLogName=false
# Set to true if you want the last component of the name to be included in output messages.
# Defaults to false.
#org.slf4j.simpleLogger.showShortLogName=false
Loading
Loading