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 11, 2024
1 parent b074f52 commit 89188cb
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.tangem.blockchain.blockchains.casper

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

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
}

val addressChecksum = address.hexToBytes().checksum()
return addressChecksum == addressChecksum.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

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,53 @@
package com.tangem.blockchain.blockchains.casper.cashaddr

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

/**
* Casper checksummed hex encoding following an [EIP-55][1]-like scheme
*
* @see <a href="https://github.com/casper-ecosystem/casper-js-sdk/blob/dev/src/lib/ChecksummedHex.ts">Source</a>
*/
object CasperCashAddr {
fun ByteArray.checksum(): String = encode(sliceArray(0..0)) + encode(sliceArray(1 until size))

// 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 bytesToBitsCycle(bytes: ByteArray): BooleanArray = bytes
.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 hashBits = bytesToBitsCycle(byteHash(input))
val hashBitsValues = hashBits.iterator()
val hexOutputString = 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()
return hexOutputString
}
}
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 @@ -11,6 +11,8 @@ import org.spongycastle.crypto.util.DigestFactory
import java.math.BigInteger
import java.nio.ByteBuffer

fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }

fun ByteArray.encodeBase58(checked: Boolean = false): String {
return if (checked) Base58Check.bytesToBase58(this) else Base58.encode(this)
}
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 89188cb

Please sign in to comment.