mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-12-25 23:49:28 -05:00
lib: basic address parser and currency formatter
This commit is contained in:
parent
36be1d209a
commit
46d387ae37
@ -10,21 +10,24 @@ class WalletNativeTest {
|
||||
@Test
|
||||
fun keyGenerationIsDeterministic() {
|
||||
assertThat(
|
||||
WalletNative.fullNode(MoneroNetwork.Mainnet.id) {
|
||||
secretSpendKey(SecretKey("d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706".parseHex()))
|
||||
}.publicAddress
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = SecretKey("d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706".parseHex())
|
||||
).primaryAccountAddress
|
||||
).isEqualTo("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T")
|
||||
|
||||
assertThat(
|
||||
WalletNative.fullNode(MoneroNetwork.Testnet.id) {
|
||||
secretSpendKey(SecretKey("48a35268bc33227eea43ac1ecfd144d51efc023c115c26ca68a01cc6201e9900".parseHex()))
|
||||
}.publicAddress
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Testnet.id,
|
||||
secretSpendKey = SecretKey("48a35268bc33227eea43ac1ecfd144d51efc023c115c26ca68a01cc6201e9900".parseHex()),
|
||||
).primaryAccountAddress
|
||||
).isEqualTo("A1v6gVUcGgGE87c1uFRWB1KfPVik2qLLDJiZT3rhZ8qjF3BGA6oHzeDboD23dH8rFaFFcysyqwF6DBj8WUTBWwEhESB7nZz")
|
||||
|
||||
assertThat(
|
||||
WalletNative.fullNode(MoneroNetwork.Stagenet.id) {
|
||||
secretSpendKey(SecretKey("561a8d4e121ffca7321a7dc6af79679ceb4cdc8c0dcb0ef588b574586c5fac04".parseHex()))
|
||||
}.publicAddress
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Stagenet.id,
|
||||
secretSpendKey = SecretKey("561a8d4e121ffca7321a7dc6af79679ceb4cdc8c0dcb0ef588b574586c5fac04".parseHex()),
|
||||
).primaryAccountAddress
|
||||
).isEqualTo("54kPaUhYgGNBT72N8Bv2DFMqstLGJCEcWg1EAjwpxABkKL3uBtBLAh4VAPKvhWBdaD4ZpiftA8YWFLAxnWL4aQ9TD4vhY4W")
|
||||
}
|
||||
|
||||
@ -32,14 +35,16 @@ class WalletNativeTest {
|
||||
@Test
|
||||
fun publicAddressesAreDistinct() {
|
||||
val publicAddress =
|
||||
WalletNative.fullNode(MoneroNetwork.Mainnet.id) {
|
||||
secretSpendKey(randomSecretKey())
|
||||
}.publicAddress
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).primaryAccountAddress
|
||||
|
||||
val anotherPublicAddress =
|
||||
WalletNative.fullNode(MoneroNetwork.Mainnet.id) {
|
||||
secretSpendKey(randomSecretKey())
|
||||
}.publicAddress
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).primaryAccountAddress
|
||||
|
||||
assertThat(publicAddress).isNotEqualTo(anotherPublicAddress)
|
||||
}
|
||||
@ -47,9 +52,10 @@ class WalletNativeTest {
|
||||
@Test
|
||||
fun atGenesisBalanceIsZero() {
|
||||
with(
|
||||
WalletNative.fullNode(MoneroNetwork.Mainnet.id) {
|
||||
secretSpendKey(randomSecretKey())
|
||||
}.currentBalance
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).currentBalance
|
||||
) {
|
||||
assertThat(totalAmount).isEqualTo(0.toAtomicAmount())
|
||||
assertThat(totalAmountUnlockedAt(1)).isEqualTo(0.toAtomicAmount())
|
||||
|
@ -2,7 +2,6 @@ package im.molly.monero;
|
||||
|
||||
import im.molly.monero.IBalanceListener;
|
||||
import im.molly.monero.IRefreshCallback;
|
||||
import im.molly.monero.PublicAddress;
|
||||
|
||||
interface IWallet {
|
||||
String getPrimaryAccountAddress();
|
||||
|
@ -1,35 +0,0 @@
|
||||
package im.molly.monero
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JvmInline
|
||||
@Parcelize
|
||||
value class Amount private constructor(
|
||||
val atomic: Long,
|
||||
) : Parcelable {
|
||||
|
||||
init {
|
||||
require(atomic >= 0) { "XMR amount cannot be negative" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun atomic(atomic: Long) = Amount(atomic)
|
||||
|
||||
fun pico(pico: Long) = atomic(pico)
|
||||
}
|
||||
|
||||
operator fun plus(other: Amount) = Amount(Math.addExact(this.atomic, other.atomic))
|
||||
}
|
||||
|
||||
fun Long.toAtomicAmount(): Amount = Amount.atomic(this)
|
||||
|
||||
fun Int.toAtomicAmount(): Amount = Amount.atomic(this.toLong())
|
||||
|
||||
inline fun <T> Iterable<T>.sumOf(selector: (T) -> Amount): Amount {
|
||||
var sum: Amount = 0L.toAtomicAmount()
|
||||
for (element in this) {
|
||||
sum += selector(element)
|
||||
}
|
||||
return sum
|
||||
}
|
24
lib/android/src/main/kotlin/im/molly/monero/AtomicAmount.kt
Normal file
24
lib/android/src/main/kotlin/im/molly/monero/AtomicAmount.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package im.molly.monero
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JvmInline
|
||||
@Parcelize
|
||||
value class AtomicAmount(val value: Long) : Parcelable {
|
||||
operator fun plus(other: AtomicAmount) = AtomicAmount(Math.addExact(this.value, other.value))
|
||||
|
||||
operator fun compareTo(other: Int): Int = value.compareTo(other)
|
||||
}
|
||||
|
||||
fun Long.toAtomicAmount(): AtomicAmount = AtomicAmount(this)
|
||||
|
||||
fun Int.toAtomicAmount(): AtomicAmount = AtomicAmount(this.toLong())
|
||||
|
||||
inline fun <T> Iterable<T>.sumOf(selector: (T) -> AtomicAmount): AtomicAmount {
|
||||
var sum: AtomicAmount = 0L.toAtomicAmount()
|
||||
for (element in this) {
|
||||
sum += selector(element)
|
||||
}
|
||||
return sum
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
package im.molly.monero
|
||||
|
||||
data class Balance(
|
||||
private val uTxOuts: Set<OwnedTxOut>,
|
||||
private val spendableTxOuts: Set<OwnedTxOut>,
|
||||
) {
|
||||
val totalAmount: Amount = uTxOuts.sumOf { it.amount }
|
||||
val totalAmount: AtomicAmount = spendableTxOuts.sumOf { it.amount }
|
||||
|
||||
fun totalAmountUnlockedAt(
|
||||
blockHeight: Long,
|
||||
timestampMillis: Long = System.currentTimeMillis()
|
||||
): Amount {
|
||||
// TODO: Create Timelock class
|
||||
): AtomicAmount {
|
||||
require(blockHeight > 0)
|
||||
require(timestampMillis >= 0)
|
||||
TODO()
|
||||
@ -16,7 +17,7 @@ data class Balance(
|
||||
|
||||
companion object {
|
||||
fun of(txOuts: List<OwnedTxOut>) = Balance(
|
||||
uTxOuts = txOuts.filter { it.notSpent }.toSet(),
|
||||
spendableTxOuts = txOuts.filter { it.notSpent }.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ package im.molly.monero
|
||||
|
||||
data class Ledger constructor(
|
||||
val publicAddress: String,
|
||||
val txOuts: List<OwnedTxOut>,
|
||||
val receivedOutputs: List<OwnedTxOut>,
|
||||
val checkedAtBlockHeight: Long,
|
||||
) {
|
||||
val balance = Balance.of(txOuts)
|
||||
val balance = Balance.of(receivedOutputs)
|
||||
|
||||
// companion object {
|
||||
// fun fromProto(proto: LedgerProto) = Ledger(
|
||||
|
@ -0,0 +1,20 @@
|
||||
package im.molly.monero
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
object MoneroCurrency {
|
||||
const val symbol = "XMR"
|
||||
|
||||
fun format(atomicAmount: AtomicAmount, formatter: NumberFormat = DefaultFormatter): String =
|
||||
formatter.format(BigDecimal.valueOf(atomicAmount.value, 12))
|
||||
|
||||
fun parse(source: String, formatter: NumberFormat = DefaultFormatter): AtomicAmount {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
val DefaultFormatter: NumberFormat = NumberFormat.getInstance(Locale.US).apply {
|
||||
minimumFractionDigits = 5
|
||||
}
|
@ -14,13 +14,14 @@ data class OwnedTxOut
|
||||
@CalledByNative("wallet.cc")
|
||||
constructor(
|
||||
val txId: ByteArray,
|
||||
val amount: Amount,
|
||||
val amount: AtomicAmount,
|
||||
val blockHeight: Long,
|
||||
val spentInBlockHeight: Long,
|
||||
) : Parcelable {
|
||||
|
||||
init {
|
||||
require(blockHeight <= spentInBlockHeight)
|
||||
require(amount >= 0) { "TX amount cannot be negative" }
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
@ -55,7 +56,7 @@ constructor(
|
||||
|
||||
fun proto(): OwnedTxOutProto = OwnedTxOutProto.newBuilder()
|
||||
.setTxId(ByteString.copyFrom(txId))
|
||||
.setAmount(amount.atomic)
|
||||
.setAmount(amount.value)
|
||||
.setBlockHeight(blockHeight)
|
||||
.setSpentHeight(spentInBlockHeight)
|
||||
.build()
|
||||
|
@ -1,19 +1,82 @@
|
||||
package im.molly.monero
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import im.molly.monero.util.decodeBase58
|
||||
|
||||
interface PublicAddress {
|
||||
val network: MoneroNetwork
|
||||
val subAddress: Boolean
|
||||
// viewPublicKey: ByteArray
|
||||
// spendPublicKey: ByteArray
|
||||
|
||||
//@Parcelize
|
||||
data class PublicAddress(
|
||||
val network: MoneroNetwork,
|
||||
// // Address type
|
||||
// // viewPublicKey: ByteArray
|
||||
// // spendPublicKey: ByteArray
|
||||
// // Checksum
|
||||
) {
|
||||
companion object {
|
||||
fun parse(publicAddress: String): PublicAddress {
|
||||
return PublicAddress(network = MoneroNetwork.Mainnet) // FIXME
|
||||
val decoded = try {
|
||||
publicAddress.decodeBase58()
|
||||
} catch (t: IllegalArgumentException) {
|
||||
throw InvalidAddress("Base58 decoding error", t)
|
||||
}
|
||||
if (decoded.size <= 4) {
|
||||
throw InvalidAddress("Address too short")
|
||||
}
|
||||
|
||||
val prefix = decoded[0].toLong()
|
||||
|
||||
StandardAddress.prefixes[prefix]?.let { network ->
|
||||
return StandardAddress(network)
|
||||
}
|
||||
SubAddress.prefixes[prefix]?.let { network ->
|
||||
TODO()
|
||||
}
|
||||
IntegratedAddress.prefixes[prefix]?.let { network ->
|
||||
TODO()
|
||||
}
|
||||
|
||||
throw InvalidAddress("Unrecognized address prefix")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidAddress(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||
|
||||
data class StandardAddress(
|
||||
override val network: MoneroNetwork,
|
||||
) : PublicAddress {
|
||||
override val subAddress = false
|
||||
|
||||
companion object {
|
||||
val prefixes = mapOf(
|
||||
18L to MoneroNetwork.Mainnet,
|
||||
53L to MoneroNetwork.Testnet,
|
||||
24L to MoneroNetwork.Stagenet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class SubAddress(
|
||||
override val network: MoneroNetwork,
|
||||
) : PublicAddress {
|
||||
override val subAddress = true
|
||||
|
||||
companion object {
|
||||
val prefixes = mapOf(
|
||||
42L to MoneroNetwork.Mainnet,
|
||||
64L to MoneroNetwork.Testnet,
|
||||
36L to MoneroNetwork.Stagenet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class IntegratedAddress(
|
||||
override val network: MoneroNetwork,
|
||||
val paymentId: Long,
|
||||
) : PublicAddress {
|
||||
override val subAddress = false
|
||||
|
||||
companion object {
|
||||
val prefixes = mapOf(
|
||||
19L to MoneroNetwork.Mainnet,
|
||||
54L to MoneroNetwork.Testnet,
|
||||
25L to MoneroNetwork.Stagenet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package im.molly.monero.util
|
||||
|
||||
fun CharSequence.parseHex(): ByteArray {
|
||||
check(length % 2 == 0) { "Must have an even length" }
|
||||
|
||||
return ByteArray(length / 2) {
|
||||
Integer.parseInt(substring(it * 2, (it + 1) * 2), 16).toByte()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user