fix(wallet): replace text-marker hack with proper raw event API (room.sendRaw + MsgLikeKind.Other)
- Add Timeline.sendRaw() to send custom Matrix events - Add CustomEventContent type for receiving custom events - Update TimelineEventContentMapper to handle MsgLikeKind.Other - Update TimelineItemContentFactory to intercept payment events - Rewrite DefaultPaymentEventSender to use sendRaw instead of text markers - Update TimelineItemContentPaymentFactory to parse raw JSON - Remove text-marker detection from TimelineItemContentMessageFactory - Update tests to use raw event API - Mark raw event SDK blocker as RESOLVED in BLOCKERS.md Event type: co.sulkta.payment.request (reverse-domain format) Status updates: co.sulkta.payment.status Benefits: - Proper Matrix protocol compliance - No JSON embedded in text messages - Events won't be indexed by search - Clean separation from regular messages
This commit is contained in:
parent
adee67cf0d
commit
f2b95d6b8a
10 changed files with 264 additions and 189 deletions
|
|
@ -21,14 +21,10 @@ import kotlinx.serialization.json.Json
|
|||
/**
|
||||
* Default implementation of [PaymentEventSender].
|
||||
*
|
||||
* Sends payment events as specially formatted text messages that can be
|
||||
* parsed by wallet-enabled clients while remaining readable for others.
|
||||
* Sends payment events as custom Matrix events using the raw event API.
|
||||
*
|
||||
* Message format:
|
||||
* ```
|
||||
* [cardano-payment:v1]{"amount_lovelace":...,"to_address":"...","from_address":"...","tx_hash":"...","status":"...","network":"..."}
|
||||
* 💰 Sent X ADA
|
||||
* ```
|
||||
* Event type: co.sulkta.payment.request
|
||||
* Event content: JSON-serialized [PaymentEventData]
|
||||
*/
|
||||
@Inject
|
||||
@ContributesBinding(SessionScope::class)
|
||||
|
|
@ -53,13 +49,11 @@ class DefaultPaymentEventSender : PaymentEventSender {
|
|||
network = network,
|
||||
)
|
||||
|
||||
val fallbackText = "💰 Sent ${TimelineItemPaymentContent.formatAda(signedTx.actualAmount)}"
|
||||
val messageBody = formatPaymentMessage(paymentData, fallbackText)
|
||||
val content = json.encodeToString(paymentData)
|
||||
|
||||
return timeline.sendMessage(
|
||||
body = messageBody,
|
||||
htmlBody = formatPaymentHtml(paymentData, fallbackText),
|
||||
intentionalMentions = emptyList(),
|
||||
return timeline.sendRaw(
|
||||
eventType = PAYMENT_EVENT_TYPE,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -69,44 +63,25 @@ class DefaultPaymentEventSender : PaymentEventSender {
|
|||
newStatus: String,
|
||||
network: String,
|
||||
): Result<Unit> {
|
||||
// Status updates use m.relates_to with m.replace relation
|
||||
// Since the SDK doesn't expose raw event editing, we send a new event
|
||||
// with a reference to the original transaction
|
||||
val statusData = PaymentStatusUpdateData(
|
||||
txHash = txHash,
|
||||
status = newStatus,
|
||||
network = network,
|
||||
)
|
||||
|
||||
val statusText = when (newStatus.lowercase()) {
|
||||
"confirmed" -> "✅ Payment confirmed"
|
||||
"failed" -> "❌ Payment failed"
|
||||
else -> "⏳ Payment $newStatus"
|
||||
}
|
||||
val content = json.encodeToString(statusData)
|
||||
|
||||
val messageBody = "[cardano-payment-status:v1]${json.encodeToString(statusData)}\n$statusText (tx: ${txHash.take(8)}...)"
|
||||
|
||||
return timeline.sendMessage(
|
||||
body = messageBody,
|
||||
htmlBody = null,
|
||||
intentionalMentions = emptyList(),
|
||||
return timeline.sendRaw(
|
||||
eventType = STATUS_UPDATE_EVENT_TYPE,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatPaymentMessage(data: PaymentEventData, fallbackText: String): String {
|
||||
val jsonPayload = json.encodeToString(data)
|
||||
return "$PAYMENT_MARKER$jsonPayload\n$fallbackText"
|
||||
}
|
||||
|
||||
private fun formatPaymentHtml(data: PaymentEventData, fallbackText: String): String {
|
||||
val jsonPayload = json.encodeToString(data)
|
||||
// Embed payment data in a data attribute for potential future parsing
|
||||
return """<span data-mx-payment="$PAYMENT_MARKER" data-payment-json='$jsonPayload'>$fallbackText</span>"""
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PAYMENT_MARKER = "[cardano-payment:v1]"
|
||||
const val STATUS_MARKER = "[cardano-payment-status:v1]"
|
||||
/** Custom event type for Cardano payment requests (reverse-domain format) */
|
||||
const val PAYMENT_EVENT_TYPE = "co.sulkta.payment.request"
|
||||
/** Custom event type for payment status updates */
|
||||
const val STATUS_UPDATE_EVENT_TYPE = "co.sulkta.payment.status"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,20 @@ package io.element.android.features.wallet.impl.timeline
|
|||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.wallet.api.PaymentCardStatus
|
||||
import io.element.android.features.wallet.api.timeline.TimelineItemPaymentContent
|
||||
import io.element.android.features.wallet.impl.payment.DefaultPaymentEventSender
|
||||
import io.element.android.features.wallet.impl.payment.PaymentEventData
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Factory for creating [TimelineItemPaymentContent] from payment event messages.
|
||||
* Factory for creating [TimelineItemPaymentContent] from raw payment events.
|
||||
*
|
||||
* Parses messages that start with the payment marker and extracts the JSON payload.
|
||||
* Parses custom events with type "co.sulkta.payment.request" and extracts the payment data.
|
||||
*/
|
||||
@Inject
|
||||
class TimelineItemContentPaymentFactory {
|
||||
|
|
@ -26,41 +32,49 @@ class TimelineItemContentPaymentFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if a message body contains a payment event.
|
||||
* Check if an event type is a payment event.
|
||||
*/
|
||||
fun isPaymentEvent(body: String): Boolean {
|
||||
return body.startsWith(PAYMENT_MARKER)
|
||||
fun isPaymentEventType(eventType: String): Boolean {
|
||||
return eventType == DefaultPaymentEventSender.PAYMENT_EVENT_TYPE
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message body contains a payment status update.
|
||||
* Check if an event type is a payment status update.
|
||||
*/
|
||||
fun isPaymentStatusUpdate(body: String): Boolean {
|
||||
return body.startsWith(STATUS_MARKER)
|
||||
fun isStatusUpdateEventType(eventType: String): Boolean {
|
||||
return eventType == DefaultPaymentEventSender.STATUS_UPDATE_EVENT_TYPE
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [TimelineItemPaymentContent] from a payment message body.
|
||||
* Create a [TimelineItemPaymentContent] from raw JSON event content.
|
||||
*
|
||||
* @param body The message body starting with [PAYMENT_MARKER]
|
||||
* @param isSentByMe Whether the current user sent this message
|
||||
* @param rawJson The raw JSON content from the Matrix event
|
||||
* @param isSentByMe Whether the current user sent this event
|
||||
* @return The parsed payment content, or null if parsing failed
|
||||
*/
|
||||
fun createFromBody(body: String, isSentByMe: Boolean): TimelineItemPaymentContent? {
|
||||
fun createFromRaw(rawJson: String, isSentByMe: Boolean): TimelineItemPaymentContent? {
|
||||
return try {
|
||||
val jsonStart = body.indexOf(PAYMENT_MARKER) + PAYMENT_MARKER.length
|
||||
val jsonEnd = body.indexOf('\n', jsonStart).takeIf { it != -1 } ?: body.length
|
||||
val jsonPayload = body.substring(jsonStart, jsonEnd)
|
||||
val fallbackText = if (jsonEnd < body.length) {
|
||||
body.substring(jsonEnd + 1).trim()
|
||||
// Try to parse the content field from the raw event JSON
|
||||
val eventJson = json.parseToJsonElement(rawJson).jsonObject
|
||||
val content = eventJson["content"]?.jsonObject ?: eventJson
|
||||
|
||||
val data = parsePaymentData(content)
|
||||
if (data != null) {
|
||||
TimelineItemPaymentContent(
|
||||
amountLovelace = data.amountLovelace,
|
||||
toAddress = data.toAddress,
|
||||
fromAddress = data.fromAddress,
|
||||
txHash = data.txHash,
|
||||
status = parseStatus(data.status),
|
||||
network = data.network,
|
||||
isSentByMe = isSentByMe,
|
||||
fallbackText = "💰 ${TimelineItemPaymentContent.formatAda(data.amountLovelace)}",
|
||||
)
|
||||
} else {
|
||||
"Payment"
|
||||
null
|
||||
}
|
||||
|
||||
val data = json.decodeFromString<PaymentEventData>(jsonPayload)
|
||||
createFromData(data, isSentByMe, fallbackText)
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Failed to parse payment event from body")
|
||||
Timber.w(e, "Failed to parse payment event from raw JSON")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -71,7 +85,6 @@ class TimelineItemContentPaymentFactory {
|
|||
fun createFromData(
|
||||
data: PaymentEventData,
|
||||
isSentByMe: Boolean,
|
||||
fallbackText: String,
|
||||
): TimelineItemPaymentContent {
|
||||
return TimelineItemPaymentContent(
|
||||
amountLovelace = data.amountLovelace,
|
||||
|
|
@ -81,35 +94,40 @@ class TimelineItemContentPaymentFactory {
|
|||
status = parseStatus(data.status),
|
||||
network = data.network,
|
||||
isSentByMe = isSentByMe,
|
||||
fallbackText = fallbackText,
|
||||
fallbackText = "💰 ${TimelineItemPaymentContent.formatAda(data.amountLovelace)}",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [TimelineItemPaymentContent] from raw JSON.
|
||||
*
|
||||
* This is the method called from TimelineItemContentFactory when
|
||||
* handling UnknownContent (if we had access to raw JSON).
|
||||
*
|
||||
* @param rawJson The raw JSON content
|
||||
* @param isSentByMe Whether the current user sent this event
|
||||
* @return The parsed payment content, or null if parsing failed
|
||||
*/
|
||||
fun createFromRaw(rawJson: String, isSentByMe: Boolean): TimelineItemPaymentContent? {
|
||||
private fun parsePaymentData(content: JsonObject): PaymentEventData? {
|
||||
return try {
|
||||
val data = json.decodeFromString<PaymentEventData>(rawJson)
|
||||
TimelineItemPaymentContent(
|
||||
amountLovelace = data.amountLovelace,
|
||||
toAddress = data.toAddress,
|
||||
fromAddress = data.fromAddress,
|
||||
txHash = data.txHash,
|
||||
status = parseStatus(data.status),
|
||||
network = data.network,
|
||||
isSentByMe = isSentByMe,
|
||||
fallbackText = "💰 ${TimelineItemPaymentContent.formatAda(data.amountLovelace)}",
|
||||
val amountLovelace = content["amount_lovelace"]?.jsonPrimitive?.longOrNull
|
||||
?: content["amountLovelace"]?.jsonPrimitive?.longOrNull
|
||||
?: return null
|
||||
|
||||
val toAddress = content["to_address"]?.jsonPrimitive?.content
|
||||
?: content["toAddress"]?.jsonPrimitive?.content
|
||||
?: return null
|
||||
|
||||
val fromAddress = content["from_address"]?.jsonPrimitive?.content
|
||||
?: content["fromAddress"]?.jsonPrimitive?.content
|
||||
?: return null
|
||||
|
||||
val txHash = content["tx_hash"]?.jsonPrimitive?.content
|
||||
?: content["txHash"]?.jsonPrimitive?.content
|
||||
|
||||
val status = content["status"]?.jsonPrimitive?.content ?: "pending"
|
||||
val network = content["network"]?.jsonPrimitive?.content ?: "mainnet"
|
||||
|
||||
PaymentEventData(
|
||||
amountLovelace = amountLovelace,
|
||||
toAddress = toAddress,
|
||||
fromAddress = fromAddress,
|
||||
txHash = txHash,
|
||||
status = status,
|
||||
network = network,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Failed to parse payment event from raw JSON")
|
||||
Timber.w(e, "Failed to parse payment data from JSON object")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -124,7 +142,9 @@ class TimelineItemContentPaymentFactory {
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val PAYMENT_MARKER = "[cardano-payment:v1]"
|
||||
const val STATUS_MARKER = "[cardano-payment-status:v1]"
|
||||
/** Custom event type for Cardano payment requests */
|
||||
const val PAYMENT_EVENT_TYPE = DefaultPaymentEventSender.PAYMENT_EVENT_TYPE
|
||||
/** Custom event type for payment status updates */
|
||||
const val STATUS_UPDATE_EVENT_TYPE = DefaultPaymentEventSender.STATUS_UPDATE_EVENT_TYPE
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,34 +8,60 @@ package io.element.android.features.wallet.impl.timeline
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.wallet.api.PaymentCardStatus
|
||||
import io.element.android.features.wallet.impl.payment.DefaultPaymentEventSender
|
||||
import org.junit.Test
|
||||
|
||||
class TimelineItemContentPaymentFactoryTest {
|
||||
private val factory = TimelineItemContentPaymentFactory()
|
||||
|
||||
@Test
|
||||
fun `isPaymentEvent returns true for valid payment marker`() {
|
||||
val body = "[cardano-payment:v1]{\"amount_lovelace\":10000000}\n💰 Sent 10 ADA"
|
||||
assertThat(factory.isPaymentEvent(body)).isTrue()
|
||||
fun `isPaymentEventType returns true for payment event type`() {
|
||||
assertThat(factory.isPaymentEventType(DefaultPaymentEventSender.PAYMENT_EVENT_TYPE)).isTrue()
|
||||
assertThat(factory.isPaymentEventType("co.sulkta.payment.request")).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPaymentEvent returns false for regular message`() {
|
||||
val body = "Hello, this is a regular message"
|
||||
assertThat(factory.isPaymentEvent(body)).isFalse()
|
||||
fun `isPaymentEventType returns false for other event types`() {
|
||||
assertThat(factory.isPaymentEventType("m.room.message")).isFalse()
|
||||
assertThat(factory.isPaymentEventType("m.room.member")).isFalse()
|
||||
assertThat(factory.isPaymentEventType("co.other.event")).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPaymentEvent returns false for empty string`() {
|
||||
assertThat(factory.isPaymentEvent("")).isFalse()
|
||||
fun `isStatusUpdateEventType returns true for status update event type`() {
|
||||
assertThat(factory.isStatusUpdateEventType(DefaultPaymentEventSender.STATUS_UPDATE_EVENT_TYPE)).isTrue()
|
||||
assertThat(factory.isStatusUpdateEventType("co.sulkta.payment.status")).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createFromBody parses valid payment event`() {
|
||||
val body = """[cardano-payment:v1]{"amount_lovelace":10000000,"to_address":"addr_test1abc","from_address":"addr_test1xyz","tx_hash":"hash123","status":"pending","network":"testnet"}
|
||||
💰 Sent 10 ADA"""
|
||||
fun `isStatusUpdateEventType returns false for other event types`() {
|
||||
assertThat(factory.isStatusUpdateEventType("m.room.message")).isFalse()
|
||||
assertThat(factory.isStatusUpdateEventType("co.sulkta.payment.request")).isFalse()
|
||||
}
|
||||
|
||||
val result = factory.createFromBody(body, isSentByMe = true)
|
||||
@Test
|
||||
fun `createFromRaw parses valid payment JSON`() {
|
||||
val json = """{"amountLovelace":25000000,"toAddress":"addr1","fromAddress":"addr2","txHash":"abc123","status":"confirmed","network":"mainnet"}"""
|
||||
|
||||
val result = factory.createFromRaw(json, isSentByMe = false)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.amountLovelace).isEqualTo(25_000_000)
|
||||
assertThat(result.amountAda).isEqualTo("25 ADA")
|
||||
assertThat(result.toAddress).isEqualTo("addr1")
|
||||
assertThat(result.fromAddress).isEqualTo("addr2")
|
||||
assertThat(result.txHash).isEqualTo("abc123")
|
||||
assertThat(result.status).isEqualTo(PaymentCardStatus.CONFIRMED)
|
||||
assertThat(result.network).isEqualTo("mainnet")
|
||||
assertThat(result.isTestnet).isFalse()
|
||||
assertThat(result.isSentByMe).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createFromRaw parses snake_case field names`() {
|
||||
val json = """{"amount_lovelace":10000000,"to_address":"addr_test1abc","from_address":"addr_test1xyz","tx_hash":"hash123","status":"pending","network":"testnet"}"""
|
||||
|
||||
val result = factory.createFromRaw(json, isSentByMe = true)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.amountLovelace).isEqualTo(10_000_000)
|
||||
|
|
@ -45,25 +71,34 @@ class TimelineItemContentPaymentFactoryTest {
|
|||
assertThat(result.status).isEqualTo(PaymentCardStatus.PENDING)
|
||||
assertThat(result.network).isEqualTo("testnet")
|
||||
assertThat(result.isSentByMe).isTrue()
|
||||
assertThat(result.fallbackText).isEqualTo("💰 Sent 10 ADA")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createFromBody parses confirmed status`() {
|
||||
val body = """[cardano-payment:v1]{"amount_lovelace":5000000,"to_address":"addr","from_address":"addr2","tx_hash":"hash","status":"confirmed","network":"mainnet"}"""
|
||||
fun `createFromRaw parses wrapped event JSON with content field`() {
|
||||
val json = """{"type":"co.sulkta.payment.request","content":{"amountLovelace":5000000,"toAddress":"addr","fromAddress":"addr2","txHash":"hash","status":"confirmed","network":"mainnet"}}"""
|
||||
|
||||
val result = factory.createFromBody(body, isSentByMe = false)
|
||||
val result = factory.createFromRaw(json, isSentByMe = false)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.amountLovelace).isEqualTo(5_000_000)
|
||||
assertThat(result.status).isEqualTo(PaymentCardStatus.CONFIRMED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createFromRaw parses confirmed status`() {
|
||||
val json = """{"amountLovelace":5000000,"toAddress":"addr","fromAddress":"addr2","txHash":"hash","status":"confirmed","network":"mainnet"}"""
|
||||
|
||||
val result = factory.createFromRaw(json, isSentByMe = false)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.status).isEqualTo(PaymentCardStatus.CONFIRMED)
|
||||
assertThat(result.isSentByMe).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createFromBody parses failed status`() {
|
||||
val body = """[cardano-payment:v1]{"amount_lovelace":1000000,"to_address":"a","from_address":"b","tx_hash":null,"status":"failed","network":"testnet"}"""
|
||||
fun `createFromRaw parses failed status`() {
|
||||
val json = """{"amountLovelace":1000000,"toAddress":"a","fromAddress":"b","txHash":null,"status":"failed","network":"testnet"}"""
|
||||
|
||||
val result = factory.createFromBody(body, isSentByMe = true)
|
||||
val result = factory.createFromRaw(json, isSentByMe = true)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.status).isEqualTo(PaymentCardStatus.FAILED)
|
||||
|
|
@ -71,34 +106,13 @@ class TimelineItemContentPaymentFactoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `createFromBody returns null for malformed JSON`() {
|
||||
val body = "[cardano-payment:v1]{not valid json}\n💰 Sent 10 ADA"
|
||||
fun `createFromRaw defaults to pending for unknown status`() {
|
||||
val json = """{"amountLovelace":1000000,"toAddress":"a","fromAddress":"b","status":"unknown_status","network":"mainnet"}"""
|
||||
|
||||
val result = factory.createFromBody(body, isSentByMe = true)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createFromBody returns null for missing marker`() {
|
||||
val body = """{"amount_lovelace":10000000,"to_address":"addr","from_address":"addr2","status":"pending","network":"testnet"}"""
|
||||
|
||||
val result = factory.createFromBody(body, isSentByMe = true)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createFromRaw parses valid JSON`() {
|
||||
val json = """{"amount_lovelace":25000000,"to_address":"addr1","from_address":"addr2","tx_hash":"abc123","status":"confirmed","network":"mainnet"}"""
|
||||
|
||||
val result = factory.createFromRaw(json, isSentByMe = false)
|
||||
val result = factory.createFromRaw(json, isSentByMe = true)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.amountLovelace).isEqualTo(25_000_000)
|
||||
assertThat(result.amountAda).isEqualTo("25 ADA")
|
||||
assertThat(result.status).isEqualTo(PaymentCardStatus.CONFIRMED)
|
||||
assertThat(result.isTestnet).isFalse()
|
||||
assertThat(result!!.status).isEqualTo(PaymentCardStatus.PENDING)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -111,13 +125,30 @@ class TimelineItemContentPaymentFactoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `isPaymentStatusUpdate returns true for valid status marker`() {
|
||||
val body = "[cardano-payment-status:v1]{\"tx_hash\":\"abc\"}\n✅ Payment confirmed"
|
||||
assertThat(factory.isPaymentStatusUpdate(body)).isTrue()
|
||||
fun `createFromRaw returns null for missing required fields`() {
|
||||
val json = """{"amountLovelace":1000000}"""
|
||||
|
||||
val result = factory.createFromRaw(json, isSentByMe = true)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPaymentStatusUpdate returns false for regular message`() {
|
||||
assertThat(factory.isPaymentStatusUpdate("Hello")).isFalse()
|
||||
fun `createFromRaw returns null for empty JSON`() {
|
||||
val json = "{}"
|
||||
|
||||
val result = factory.createFromRaw(json, isSentByMe = true)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createFromRaw formats fallback text correctly`() {
|
||||
val json = """{"amountLovelace":1500000,"toAddress":"a","fromAddress":"b","status":"pending","network":"mainnet"}"""
|
||||
|
||||
val result = factory.createFromRaw(json, isSentByMe = true)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.fallbackText).isEqualTo("💰 1.5 ADA")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue