Skip to content

Commit

Permalink
AND-8609: Add CasperAddressService
Browse files Browse the repository at this point in the history
  • Loading branch information
nzeeei committed Oct 15, 2024
1 parent 40fbe43 commit faadeb1
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.tangem.blockchain.blockchains.casper

import com.tangem.blockchain.blockchains.casper.cashaddr.CasperAddressUtils.checksum
import com.tangem.blockchain.common.address.AddressService
import com.tangem.blockchain.extensions.isSameCase
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes

internal class CasperAddressService : AddressService() {
override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String {
val prefix = CasperConstants.getAddressPrefix(curve!!)
val bytes = prefix.hexToBytes() + walletPublicKey
return bytes.checksum()
}

override fun validate(address: String): Boolean {
val isCorrectEd25519Address = address.length == CasperConstants.ED25519_LENGTH &&
address.startsWith(CasperConstants.ED25519_PREFIX)
val isCorrectSecp256k1Address = address.length == CasperConstants.SECP256K1_LENGTH &&
address.startsWith(CasperConstants.SECP256K1_PREFIX)

if (!isCorrectEd25519Address && !isCorrectSecp256k1Address) {
return false
}

// don't check checksum if it's not mixed case
if (address.isSameCase()) {
return true
}

return address == address.hexToBytes().checksum()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.tangem.blockchain.blockchains.casper

import com.tangem.common.card.EllipticCurve

internal object CasperConstants {
const val ED25519_PREFIX = "01"
const val ED25519_LENGTH = 66

const val SECP256K1_PREFIX = "02"
const val SECP256K1_LENGTH = 68

fun getAddressPrefix(curve: EllipticCurve) = when (curve) {
EllipticCurve.Ed25519,
EllipticCurve.Ed25519Slip0010,
-> ED25519_PREFIX
EllipticCurve.Secp256k1,
-> SECP256K1_PREFIX
// added as unsupported for now, need to research
EllipticCurve.Secp256r1,
EllipticCurve.Bls12381G2,
EllipticCurve.Bls12381G2Aug,
EllipticCurve.Bls12381G2Pop,
EllipticCurve.Bip0340,
-> error("${curve.curve} is not supported")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.tangem.blockchain.blockchains.casper.cashaddr

import org.bouncycastle.jcajce.provider.digest.Blake2b

/**
* @see <a href="https://github.com/casper-ecosystem/casper-js-sdk/blob/dev/src/lib/ChecksummedHex.ts">Source</a>
*/
object CasperAddressUtils {
// Ed25519: encode([0x01]) + encode(<public key bytes>)
// or
// Secp256k1: encode([0x02]) + encode(<public key bytes>)
fun ByteArray.checksum(): String = encode(byteArrayOf(first())) + encode(drop(1).toByteArray())

// Separate bytes inside ByteArray to nibbles
// E.g. [0x01, 0x55, 0xFF, ...] -> [0x00, 0x01, 0x50, 0x05, 0xF0, 0x0F, ...]
@Suppress("MagicNumber")
private fun bytesToNibbles(bytes: ByteArray): ByteArray = bytes
.flatMap { byte ->
listOf(
(byte.toInt() and 0xff shr 4).toByte(),
(byte.toInt() and 0x0f).toByte(),
)
}.toByteArray()

// Separate bytes inside ByteArray to bits array
// E.g. [0x01, ...] -> [false, false, false, false, false, false, false, true, ...]
// E.g. [0xAA, ...] -> [true, false, true, false, true, false, true, false, ...]
@Suppress("MagicNumber")
private fun ByteArray.toBitArray(): BooleanArray = this
.flatMap { byte ->
List(8) { i ->
byte.toInt() shr i and 0x01 == 0x01
}
}.toBooleanArray()

private fun byteHash(bytes: ByteArray): ByteArray = Blake2b.Blake2b256().digest(bytes)

private fun encode(input: ByteArray): String {
val inputNibbles = bytesToNibbles(input)
val hash = byteHash(input)
val hashBits = hash.toBitArray()
val hashBitsValues = hashBits.iterator()
return inputNibbles.fold(StringBuilder()) { accum, nibble ->
val c = "%x".format(nibble)

if (Regex("^[a-zA-Z()]+\$").matches(c) && hashBitsValues.hasNext() && hashBitsValues.next()) {
accum.append(c.uppercase())
} else {
accum.append(c.lowercase())
}
accum
}.toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.tangem.blockchain.blockchains.binance.BinanceAddressService
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressService
import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashAddressService
import com.tangem.blockchain.blockchains.cardano.CardanoAddressServiceFacade
import com.tangem.blockchain.blockchains.casper.CasperAddressService
import com.tangem.blockchain.blockchains.chia.ChiaAddressService
import com.tangem.blockchain.blockchains.decimal.DecimalAddressService
import com.tangem.blockchain.blockchains.ethereum.Chain
Expand Down Expand Up @@ -377,7 +378,7 @@ enum class Blockchain(
Nexa, NexaTestnet -> NexaAddressService(this.isTestnet())
Koinos, KoinosTestnet -> KoinosAddressService()
Radiant -> RadiantAddressService()
Casper, CasperTestnet -> TODO() // AND-8609
Casper, CasperTestnet -> CasperAddressService()
Unknown -> error("unsupported blockchain")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ fun String?.toBigDecimalOrDefault(default: BigDecimal = BigDecimal.ZERO): BigDec

fun String.isValidHex(): Boolean = this.all { it.isAscii() }

fun String.isSameCase(): Boolean = this.lowercase() == this || this.uppercase() == this

inline fun <R> String?.letNotBlank(block: (String) -> R): R? {
if (isNullOrBlank()) return null

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.tangem.blockchain.blockchains.casper

import com.google.common.truth.Truth
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes
import org.junit.Test

class CasperAddressTest {

private val addressService = CasperAddressService()

@Test
fun makeAddressFromCorrectEd25519PublicKey() {
val walletPublicKey = "98C07D7E72D89A681D7227A7AF8A6FD5F22FE0105C8741D55A95DF415454B82E".hexToBytes()
val expected = "0198c07D7e72D89A681d7227a7Af8A6fd5F22fe0105c8741d55A95dF415454b82E"

Truth.assertThat(addressService.makeAddress(walletPublicKey, EllipticCurve.Ed25519)).isEqualTo(expected)
}

@Test
fun validateCorrectEd25519Address() {
val address = "0198c07D7e72D89A681d7227a7Af8A6fd5F22fe0105c8741d55A95dF415454b82E"

Truth.assertThat(addressService.validate(address)).isTrue()
}

@Test
fun makeAddressFromCorrectSecp256k1PublicKey() {
val walletPublicKey = "021F997DFBBFD32817C0E110EAEE26BCBD2BB70B4640C515D9721C9664312EACD8".hexToBytes()
val expected = "02021f997DfbbFd32817C0E110EAeE26BCbD2BB70b4640C515D9721c9664312eaCd8"

Truth.assertThat(addressService.makeAddress(walletPublicKey, EllipticCurve.Secp256k1)).isEqualTo(expected)
}

@Test
fun validateCorrectSecp256k1Address() {
val address = "02021f997DfbbFd32817C0E110EAeE26BCbD2BB70b4640C515D9721c9664312eaCd8"

Truth.assertThat(addressService.validate(address)).isTrue()
}
}

0 comments on commit faadeb1

Please sign in to comment.