Skip to content
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
6 changes: 6 additions & 0 deletions .idea/copilot.data.migration.ask2agent.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 0 additions & 18 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/markdown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

187 changes: 187 additions & 0 deletions core/src/main/java/com/altude/core/Programs/AttestationProgram.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package com.altude.core.Programs

import com.altude.core.Programs.Utility.SYSTEM_PROGRAM_ID
import foundation.metaplex.solana.transactions.AccountMeta
import foundation.metaplex.solana.transactions.TransactionInstruction
import foundation.metaplex.solanapublickeys.PublicKey
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.charset.StandardCharsets

/**
* Solana Attestation Service (SAS) Program instructions.
*
* Program ID: 22zoJMtdu5rJOmEMCqaYzDVLEqJa4FcFBGmrnEcUhbCr
* Reference: https://github.com/solana-attestation-service
*
* SAS uses an Anchor 8-byte discriminator derived from the instruction name.
* Discriminators are precomputed sha256("global:<instruction_name>")[0..8].
*/
object AttestationProgram {

/** On-chain program address for the Solana Attestation Service. */
val PROGRAM_ID = PublicKey("22zoJMtdu5rJOmEMCqaYzDVLEqJa4FcFBGmrnEcUhbCr")

// ── Anchor discriminators (sha256("global:<name>")[0..8] little-endian) ───
// Pre-computed from the SAS IDL.
private val DISC_CREATE_SCHEMA = byteArrayOf(102.toByte(), 82.toByte(), 205.toByte(), 109.toByte(), 56.toByte(), 206.toByte(), 253.toByte(), 189.toByte())
private val DISC_CREATE_ATTESTATION = byteArrayOf( 71.toByte(), 149.toByte(), 77.toByte(), 206.toByte(), 166.toByte(), 86.toByte(), 252.toByte(), 185.toByte())
private val DISC_REVOKE_ATTESTATION = byteArrayOf(243.toByte(), 175.toByte(), 179.toByte(), 26.toByte(), 67.toByte(), 213.toByte(), 154.toByte(), 97.toByte())

// ── PDA seeds ─────────────────────────────────────────────────────────────

/** Derives the Schema PDA: seeds = ["schema", authority, name] */
suspend fun deriveSchemaAddress(authority: PublicKey, name: String): PublicKey {
val seeds = listOf(
"schema".toByteArray(StandardCharsets.UTF_8),
authority.toByteArray(),
name.toByteArray(StandardCharsets.UTF_8)
)
return PublicKey.findProgramAddress(seeds, PROGRAM_ID).address
}

/**
* Derives the Attestation PDA.
* seeds = ["attestation", schema, attester, recipient, nonce]
*/
suspend fun deriveAttestationAddress(
schema: PublicKey,
attester: PublicKey,
recipient: PublicKey,
nonce: String = ""
): PublicKey {
val seeds = mutableListOf(
"attestation".toByteArray(StandardCharsets.UTF_8),
schema.toByteArray(),
attester.toByteArray(),
recipient.toByteArray()
)
if (nonce.isNotBlank()) seeds += nonce.toByteArray(StandardCharsets.UTF_8)
return PublicKey.findProgramAddress(seeds, PROGRAM_ID).address
}

// ── Instruction builders ──────────────────────────────────────────────────

/**
* Creates a `createSchema` instruction.
*
* Accounts (order matches SAS IDL):
* 0. schema [writable] — PDA, derived by [deriveSchemaAddress]
* 1. authority [writable, signer] — Schema authority
* 2. feePayer [writable, signer] — Rent / fee payer
* 3. systemProgram [read-only] — 11111…
*/
suspend fun createSchema(
authority: PublicKey,
feePayer: PublicKey,
name: String,
description: String,
fieldNames: List<String>,
isRevocable: Boolean
): TransactionInstruction {
val schemaPda = deriveSchemaAddress(authority, name)

val nameBytes = name.toByteArray(StandardCharsets.UTF_8)
val descBytes = description.toByteArray(StandardCharsets.UTF_8)
val fieldNamesBytes = fieldNames.map { it.toByteArray(StandardCharsets.UTF_8) }

// Layout: discriminator(8) + name_len(4) + name + desc_len(4) + desc
// + field_count(4) + [field_len(4) + field]* + is_revocable(1)
val fieldSection = fieldNamesBytes.fold(ByteArray(4).also {
ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN).putInt(fieldNamesBytes.size)
}) { acc, fb ->
val lenBuf = ByteArray(4).also { b -> ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).putInt(fb.size) }
acc + lenBuf + fb
}

val data = DISC_CREATE_SCHEMA +
encode4(nameBytes.size) + nameBytes +
encode4(descBytes.size) + descBytes +
fieldSection +
byteArrayOf(if (isRevocable) 1 else 0)

val accounts = listOf(
AccountMeta(schemaPda, isSigner = false, isWritable = true),
AccountMeta(authority, isSigner = true, isWritable = true),
AccountMeta(feePayer, isSigner = true, isWritable = true),
AccountMeta(SYSTEM_PROGRAM_ID, isSigner = false, isWritable = false)
)
return TransactionInstruction(programId = PROGRAM_ID, keys = accounts, data = data)
}

/**
* Creates an `attest` instruction.
*
* Accounts:
* 0. attestation [writable] — PDA, derived by [deriveAttestationAddress]
* 1. schema [read-only] — Schema PDA
* 2. attester [writable, signer]
* 3. recipient [read-only]
* 4. feePayer [writable, signer]
* 5. systemProgram
*/
suspend fun createAttestation(
attester: PublicKey,
feePayer: PublicKey,
schemaPda: PublicKey,
recipient: PublicKey,
attestationData: ByteArray,
expireAt: Long = 0L,
nonce: String = ""
): TransactionInstruction {
val attestationPda = deriveAttestationAddress(schemaPda, attester, recipient, nonce)

// Layout: discriminator(8) + data_len(4) + data + expire_at(8) + nonce_len(4) + nonce
val nonceBytes = nonce.toByteArray(StandardCharsets.UTF_8)
val data = DISC_CREATE_ATTESTATION +
encode4(attestationData.size) + attestationData +
encode8(expireAt) +
encode4(nonceBytes.size) + nonceBytes

val accounts = listOf(
AccountMeta(attestationPda, isSigner = false, isWritable = true),
AccountMeta(schemaPda, isSigner = false, isWritable = false),
AccountMeta(attester, isSigner = true, isWritable = true),
AccountMeta(recipient, isSigner = false, isWritable = false),
AccountMeta(feePayer, isSigner = true, isWritable = true),
AccountMeta(SYSTEM_PROGRAM_ID, isSigner = false, isWritable = false)
)
return TransactionInstruction(programId = PROGRAM_ID, keys = accounts, data = data)
}

/**
* Creates a `revokeAttestation` instruction.
*
* Accounts:
* 0. attestation [writable] — The Attestation PDA to revoke
* 1. attester [writable, signer]
* 2. feePayer [writable, signer]
* 3. systemProgram
*/
fun revokeAttestation(
attester: PublicKey,
feePayer: PublicKey,
attestationPda: PublicKey
): TransactionInstruction {
val accounts = listOf(
AccountMeta(attestationPda, isSigner = false, isWritable = true),
AccountMeta(attester, isSigner = true, isWritable = true),
AccountMeta(feePayer, isSigner = true, isWritable = true),
AccountMeta(SYSTEM_PROGRAM_ID, isSigner = false, isWritable = false)
)
return TransactionInstruction(
programId = PROGRAM_ID,
keys = accounts,
data = DISC_REVOKE_ATTESTATION
)
}

// ── Helpers ───────────────────────────────────────────────────────────────

private fun encode4(value: Int): ByteArray =
ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(value).array()

private fun encode8(value: Long): ByteArray =
ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).array()
}

20 changes: 20 additions & 0 deletions core/src/main/java/com/altude/core/api/TransactionService.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.altude.core.api

import com.altude.core.data.AttestRequest
import com.altude.core.data.BatchTransactionRequest
import com.altude.core.data.CreateSchemaRequest
import com.altude.core.data.MintData
import com.altude.core.data.RevokeAttestationRequest
import com.altude.core.data.SendTransactionRequest
import com.altude.core.data.SwapTransactionRequest
import kotlinx.serialization.Contextual
Expand Down Expand Up @@ -111,6 +114,23 @@ interface TransactionService {
@Body body: ISendTransactionRequest
): Call<JsonElement>

// ── Solana Attestation Service (SAS) ─────────────────────────────────────

@POST("api/attestation/createSchema")
fun createSchema(
@Body body: CreateSchemaRequest
): Call<JsonElement>

@POST("api/attestation/attest")
fun attest(
@Body body: AttestRequest
): Call<JsonElement>

@POST("api/attestation/revoke")
fun revokeAttestation(
@Body body: RevokeAttestationRequest
): Call<JsonElement>

@GET("api/transaction/config")
fun getConfig(): Call<ConfigResponse>
}
Expand Down
28 changes: 28 additions & 0 deletions core/src/main/java/com/altude/core/data/AttestationRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.altude.core.data

import kotlinx.serialization.Serializable

/**
* Request body for POST api/attestation/createSchema
*/
@Serializable
data class CreateSchemaRequest(
val SignedTransaction: String
)

/**
* Request body for POST api/attestation/attest
*/
@Serializable
data class AttestRequest(
val SignedTransaction: String
)

/**
* Request body for POST api/attestation/revoke
*/
@Serializable
data class RevokeAttestationRequest(
val SignedTransaction: String
)

7 changes: 7 additions & 0 deletions core/src/main/java/com/altude/core/service/StorageService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ object StorageService {
fun init(context : Context) {
appContext = context.applicationContext // prevents memory leaks
}

/**
* Exposes the application [Context] to other SDK modules (e.g. provenance).
* Safe to call after [init] has been invoked.
*/
fun getContext(): Context = appContext

private val ANDROID_KEYSTORE = "AndroidKeyStore"

private fun getKeyAlias(): String = "altude_store"
Expand Down
1 change: 1 addition & 0 deletions gasstation/src/main/java/com/altude/gasstation/Altude.kt
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ object Altude {
}
}


suspend fun generateKeyPair(): SolanaKeypair {
val keypair = KeyPair.generate()
return SolanaKeypair(keypair.publicKey,keypair.secretKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,4 +559,5 @@ object GaslessManager {
}



}
Loading
Loading