mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-12-26 07:59:35 -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
|
@Test
|
||||||
fun keyGenerationIsDeterministic() {
|
fun keyGenerationIsDeterministic() {
|
||||||
assertThat(
|
assertThat(
|
||||||
WalletNative.fullNode(MoneroNetwork.Mainnet.id) {
|
WalletNative.fullNode(
|
||||||
secretSpendKey(SecretKey("d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706".parseHex()))
|
networkId = MoneroNetwork.Mainnet.id,
|
||||||
}.publicAddress
|
secretSpendKey = SecretKey("d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706".parseHex())
|
||||||
|
).primaryAccountAddress
|
||||||
).isEqualTo("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T")
|
).isEqualTo("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T")
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
WalletNative.fullNode(MoneroNetwork.Testnet.id) {
|
WalletNative.fullNode(
|
||||||
secretSpendKey(SecretKey("48a35268bc33227eea43ac1ecfd144d51efc023c115c26ca68a01cc6201e9900".parseHex()))
|
networkId = MoneroNetwork.Testnet.id,
|
||||||
}.publicAddress
|
secretSpendKey = SecretKey("48a35268bc33227eea43ac1ecfd144d51efc023c115c26ca68a01cc6201e9900".parseHex()),
|
||||||
|
).primaryAccountAddress
|
||||||
).isEqualTo("A1v6gVUcGgGE87c1uFRWB1KfPVik2qLLDJiZT3rhZ8qjF3BGA6oHzeDboD23dH8rFaFFcysyqwF6DBj8WUTBWwEhESB7nZz")
|
).isEqualTo("A1v6gVUcGgGE87c1uFRWB1KfPVik2qLLDJiZT3rhZ8qjF3BGA6oHzeDboD23dH8rFaFFcysyqwF6DBj8WUTBWwEhESB7nZz")
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
WalletNative.fullNode(MoneroNetwork.Stagenet.id) {
|
WalletNative.fullNode(
|
||||||
secretSpendKey(SecretKey("561a8d4e121ffca7321a7dc6af79679ceb4cdc8c0dcb0ef588b574586c5fac04".parseHex()))
|
networkId = MoneroNetwork.Stagenet.id,
|
||||||
}.publicAddress
|
secretSpendKey = SecretKey("561a8d4e121ffca7321a7dc6af79679ceb4cdc8c0dcb0ef588b574586c5fac04".parseHex()),
|
||||||
|
).primaryAccountAddress
|
||||||
).isEqualTo("54kPaUhYgGNBT72N8Bv2DFMqstLGJCEcWg1EAjwpxABkKL3uBtBLAh4VAPKvhWBdaD4ZpiftA8YWFLAxnWL4aQ9TD4vhY4W")
|
).isEqualTo("54kPaUhYgGNBT72N8Bv2DFMqstLGJCEcWg1EAjwpxABkKL3uBtBLAh4VAPKvhWBdaD4ZpiftA8YWFLAxnWL4aQ9TD4vhY4W")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,14 +35,16 @@ class WalletNativeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun publicAddressesAreDistinct() {
|
fun publicAddressesAreDistinct() {
|
||||||
val publicAddress =
|
val publicAddress =
|
||||||
WalletNative.fullNode(MoneroNetwork.Mainnet.id) {
|
WalletNative.fullNode(
|
||||||
secretSpendKey(randomSecretKey())
|
networkId = MoneroNetwork.Mainnet.id,
|
||||||
}.publicAddress
|
secretSpendKey = randomSecretKey(),
|
||||||
|
).primaryAccountAddress
|
||||||
|
|
||||||
val anotherPublicAddress =
|
val anotherPublicAddress =
|
||||||
WalletNative.fullNode(MoneroNetwork.Mainnet.id) {
|
WalletNative.fullNode(
|
||||||
secretSpendKey(randomSecretKey())
|
networkId = MoneroNetwork.Mainnet.id,
|
||||||
}.publicAddress
|
secretSpendKey = randomSecretKey(),
|
||||||
|
).primaryAccountAddress
|
||||||
|
|
||||||
assertThat(publicAddress).isNotEqualTo(anotherPublicAddress)
|
assertThat(publicAddress).isNotEqualTo(anotherPublicAddress)
|
||||||
}
|
}
|
||||||
@ -47,9 +52,10 @@ class WalletNativeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun atGenesisBalanceIsZero() {
|
fun atGenesisBalanceIsZero() {
|
||||||
with(
|
with(
|
||||||
WalletNative.fullNode(MoneroNetwork.Mainnet.id) {
|
WalletNative.fullNode(
|
||||||
secretSpendKey(randomSecretKey())
|
networkId = MoneroNetwork.Mainnet.id,
|
||||||
}.currentBalance
|
secretSpendKey = randomSecretKey(),
|
||||||
|
).currentBalance
|
||||||
) {
|
) {
|
||||||
assertThat(totalAmount).isEqualTo(0.toAtomicAmount())
|
assertThat(totalAmount).isEqualTo(0.toAtomicAmount())
|
||||||
assertThat(totalAmountUnlockedAt(1)).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.IBalanceListener;
|
||||||
import im.molly.monero.IRefreshCallback;
|
import im.molly.monero.IRefreshCallback;
|
||||||
import im.molly.monero.PublicAddress;
|
|
||||||
|
|
||||||
interface IWallet {
|
interface IWallet {
|
||||||
String getPrimaryAccountAddress();
|
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
|
package im.molly.monero
|
||||||
|
|
||||||
data class Balance(
|
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(
|
fun totalAmountUnlockedAt(
|
||||||
blockHeight: Long,
|
blockHeight: Long,
|
||||||
timestampMillis: Long = System.currentTimeMillis()
|
timestampMillis: Long = System.currentTimeMillis()
|
||||||
): Amount {
|
// TODO: Create Timelock class
|
||||||
|
): AtomicAmount {
|
||||||
require(blockHeight > 0)
|
require(blockHeight > 0)
|
||||||
require(timestampMillis >= 0)
|
require(timestampMillis >= 0)
|
||||||
TODO()
|
TODO()
|
||||||
@ -16,7 +17,7 @@ data class Balance(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun of(txOuts: List<OwnedTxOut>) = Balance(
|
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(
|
data class Ledger constructor(
|
||||||
val publicAddress: String,
|
val publicAddress: String,
|
||||||
val txOuts: List<OwnedTxOut>,
|
val receivedOutputs: List<OwnedTxOut>,
|
||||||
val checkedAtBlockHeight: Long,
|
val checkedAtBlockHeight: Long,
|
||||||
) {
|
) {
|
||||||
val balance = Balance.of(txOuts)
|
val balance = Balance.of(receivedOutputs)
|
||||||
|
|
||||||
// companion object {
|
// companion object {
|
||||||
// fun fromProto(proto: LedgerProto) = Ledger(
|
// 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")
|
@CalledByNative("wallet.cc")
|
||||||
constructor(
|
constructor(
|
||||||
val txId: ByteArray,
|
val txId: ByteArray,
|
||||||
val amount: Amount,
|
val amount: AtomicAmount,
|
||||||
val blockHeight: Long,
|
val blockHeight: Long,
|
||||||
val spentInBlockHeight: Long,
|
val spentInBlockHeight: Long,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(blockHeight <= spentInBlockHeight)
|
require(blockHeight <= spentInBlockHeight)
|
||||||
|
require(amount >= 0) { "TX amount cannot be negative" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
@ -55,7 +56,7 @@ constructor(
|
|||||||
|
|
||||||
fun proto(): OwnedTxOutProto = OwnedTxOutProto.newBuilder()
|
fun proto(): OwnedTxOutProto = OwnedTxOutProto.newBuilder()
|
||||||
.setTxId(ByteString.copyFrom(txId))
|
.setTxId(ByteString.copyFrom(txId))
|
||||||
.setAmount(amount.atomic)
|
.setAmount(amount.value)
|
||||||
.setBlockHeight(blockHeight)
|
.setBlockHeight(blockHeight)
|
||||||
.setSpentHeight(spentInBlockHeight)
|
.setSpentHeight(spentInBlockHeight)
|
||||||
.build()
|
.build()
|
||||||
|
@ -1,19 +1,82 @@
|
|||||||
package im.molly.monero
|
package im.molly.monero
|
||||||
|
|
||||||
import android.os.Parcelable
|
import im.molly.monero.util.decodeBase58
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
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 {
|
companion object {
|
||||||
fun parse(publicAddress: String): PublicAddress {
|
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