Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ allprojects {
}

group = "org.onflow.flow"
val defaultVersion = "0.0.24"
val defaultVersion = "0.0.25"
version = System.getenv("GITHUB_REF")?.split('/')?.last() ?: defaultVersion
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ package org.onflow.flow.infrastructure
object FixedPointFormatter {
fun format(num: String, precision: ULong): String? {
return try {
// Handle empty or invalid input
if (num.isEmpty() || num.isBlank()) {
return null
}

val parts = num.split(".")
if (parts.size == 1) {
num
// For whole numbers (including "0"), add decimal point with appropriate precision
"$num.0"
} else {
val integerPart = parts[0]
val decimalPart = parts[1].take(precision.toInt())
Expand Down
175 changes: 168 additions & 7 deletions flow/src/commonTest/kotlin/org/onflow/flow/FlowTransactionTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class FlowTransactionTests {
api.waitForSeal(result.id!!)
val finalResult = api.getTransactionResult(result.id!!)

assertEquals(TransactionStatus.SEALED, finalResult.status)
assertEquals(TransactionStatus.EXECUTED, finalResult.status)
println("✅ Gas sponsored transaction sealed successfully")
}

Expand Down Expand Up @@ -139,7 +139,7 @@ class FlowTransactionTests {
api.waitForSeal(result.id!!)
val finalResult = api.getTransactionResult(result.id!!)

assertEquals(TransactionStatus.SEALED, finalResult.status)
assertEquals(TransactionStatus.EXECUTED, finalResult.status)
println("✅ Single signer transaction sealed successfully")
}

Expand Down Expand Up @@ -193,7 +193,7 @@ class FlowTransactionTests {
api.waitForSeal(result.id!!)
val finalResult = api.getTransactionResult(result.id!!)

assertEquals(TransactionStatus.SEALED, finalResult.status)
assertEquals(TransactionStatus.EXECUTED, finalResult.status)
println("✅ Proposer solo transaction sealed successfully")
}

Expand Down Expand Up @@ -247,7 +247,7 @@ class FlowTransactionTests {
api.waitForSeal(result.id!!)
val finalResult = api.getTransactionResult(result.id!!)

assertEquals(TransactionStatus.SEALED, finalResult.status)
assertEquals(TransactionStatus.EXECUTED, finalResult.status)
println("✅ Payer solo transaction sealed successfully")
}

Expand Down Expand Up @@ -324,7 +324,7 @@ class FlowTransactionTests {
api.waitForSeal(result.id!!)
val finalResult = api.getTransactionResult(result.id!!)

assertEquals(TransactionStatus.SEALED, finalResult.status)
assertEquals(TransactionStatus.EXECUTED, finalResult.status)
println("✅ Manual step-by-step transaction sealed successfully")
}

Expand Down Expand Up @@ -557,7 +557,7 @@ class FlowTransactionTests {
api.waitForSeal(result.id!!)
val finalResult = api.getTransactionResult(result.id!!)

assertEquals(TransactionStatus.SEALED, finalResult.status)
assertEquals(TransactionStatus.EXECUTED, finalResult.status)

// Verify no errors occurred - this would previously fail with UFix64 encoding error
assertTrue(finalResult.errorMessage.isNullOrEmpty(), "Transaction should not have any errors")
Expand All @@ -567,6 +567,167 @@ class FlowTransactionTests {
println(" Transaction ID: ${result.id}")
}

/**
* Test UFix64 zero value handling to prevent "missing decimal point" error
* This test specifically addresses the issue where NFT transfers with zero amounts
* were failing with "invalid UFix64: missing decimal point" errors.
*/
@Test
fun testUFix64ZeroValueTransaction() = runBlocking {
// ===== SETUP =====
val accountAddress = "c6de0d94160377cd"
val privateKey = "c9c0f04adddf7674d265c395de300a65a777d3ec412bba5bfdfd12cffbbb78d9"

// Get on-chain account information
val account = api.getAccount(accountAddress)
assertNotNull(account, "Account should exist")
val accountKey = account.keys!!.first { it.index.toInt() == 0 }

// Create signer with on-chain algorithms
val signer = Crypto.getSigner(
Crypto.decodePrivateKey(privateKey, accountKey.signingAlgorithm),
accountKey.hashingAlgorithm
).apply {
address = accountAddress
keyIndex = 0
}

val latestBlock = api.getBlockHeader()

// ===== TEST ZERO VALUE =====
val zeroValue = 0.0 // This is the problematic case for NFT transfers

val transaction = TransactionBuilder(
script = """
transaction(amount: UFix64) {
prepare(signer: auth(Storage) &Account) {
log("Testing UFix64 zero value: ".concat(amount.toString()))

// Verify the amount is exactly zero
assert(amount == 0.0, message: "Amount should be 0.0")

log("UFix64 zero value test passed!")
}
}
""".trimIndent(),
arguments = listOf(Cadence.ufix64(zeroValue))
)
.withProposalKey(accountAddress, 0, accountKey.sequenceNumber.toBigInteger())
.withPayer(accountAddress)
.withAuthorizers(listOf(accountAddress))
.withReferenceBlockId(latestBlock.id)
.build()

// ===== SIGNING =====
val signedTransaction = transaction.sign(listOf(signer))

// ===== SUBMISSION AND VERIFICATION =====
val result = api.sendTransaction(signedTransaction)
assertNotNull(result.id, "Transaction ID should not be null")

api.waitForSeal(result.id!!)
val finalResult = api.getTransactionResult(result.id!!)

assertEquals(TransactionStatus.EXECUTED, finalResult.status)

// This is the key test: verify no "missing decimal point" error occurs
assertTrue(finalResult.errorMessage.isNullOrEmpty(),
"Transaction should not have 'missing decimal point' error. Error: ${finalResult.errorMessage}")

println("✅ UFix64 zero value transaction sealed successfully")
println(" Zero value: $zeroValue")
println(" Transaction ID: ${result.id}")
println(" This confirms zero values no longer cause 'missing decimal point' errors")
}

/**
* Test callContract scenario with zero amount (simulating NFT transfer)
* This test mimics the exact scenario that was failing: callContract cadence
* with amount=0 for NFT transfers from COA to COA/EOA.
*/
@Test
fun testCallContractWithZeroAmountTransaction() = runBlocking {
// ===== SETUP =====
val accountAddress = "c6de0d94160377cd"
val privateKey = "c9c0f04adddf7674d265c395de300a65a777d3ec412bba5bfdfd12cffbbb78d9"

// Get on-chain account information
val account = api.getAccount(accountAddress)
assertNotNull(account, "Account should exist")
val accountKey = account.keys!!.first { it.index.toInt() == 0 }

// Create signer with on-chain algorithms
val signer = Crypto.getSigner(
Crypto.decodePrivateKey(privateKey, accountKey.signingAlgorithm),
accountKey.hashingAlgorithm
).apply {
address = accountAddress
keyIndex = 0
}

val latestBlock = api.getBlockHeader()

// ===== TEST CALL CONTRACT WITH ZERO AMOUNT =====
// This simulates the exact callContract scenario for NFT transfers
val zeroAmount = 0.0 // Amount assigned when sending NFT (no Flow tokens)
val contractAddress = "0x1234567890abcdef" // Mock contract address
val callData = "0x00" // Mock call data

val transaction = TransactionBuilder(
script = """
transaction(contractAddress: String, amount: UFix64, callData: String) {
prepare(signer: auth(Storage) &Account) {
log("Testing callContract with zero amount")
log("Contract: ".concat(contractAddress))
log("Amount: ".concat(amount.toString()))
log("CallData: ".concat(callData))

// Verify the amount is exactly zero (NFT transfer case)
assert(amount == 0.0, message: "Amount should be 0.0 for NFT transfers")

// Verify other parameters are present
assert(contractAddress != "", message: "Contract address should not be empty")
assert(callData != "", message: "Call data should not be empty")

log("CallContract with zero amount test passed!")
}
}
""".trimIndent(),
arguments = listOf(
Cadence.string(contractAddress),
Cadence.ufix64(zeroAmount), // This was causing the "missing decimal point" error
Cadence.string(callData)
)
)
.withProposalKey(accountAddress, 0, accountKey.sequenceNumber.toBigInteger())
.withPayer(accountAddress)
.withAuthorizers(listOf(accountAddress))
.withReferenceBlockId(latestBlock.id)
.build()

// ===== SIGNING =====
val signedTransaction = transaction.sign(listOf(signer))

// ===== SUBMISSION AND VERIFICATION =====
val result = api.sendTransaction(signedTransaction)
assertNotNull(result.id, "Transaction ID should not be null")

api.waitForSeal(result.id!!)
val finalResult = api.getTransactionResult(result.id!!)

assertEquals(TransactionStatus.EXECUTED, finalResult.status)

// This is the critical test: verify no UFix64 decimal point error occurs
assertTrue(finalResult.errorMessage.isNullOrEmpty(),
"CallContract with zero amount should not fail with UFix64 error. Error: ${finalResult.errorMessage}")

println("✅ CallContract with zero amount transaction sealed successfully")
println(" Zero amount: $zeroAmount")
println(" Contract: $contractAddress")
println(" Transaction ID: ${result.id}")
println(" This confirms NFT transfers with zero amounts no longer fail")
}

/**
* Test multiple UFix64 values including edge cases
*/
Expand Down Expand Up @@ -636,7 +797,7 @@ class FlowTransactionTests {
api.waitForSeal(result.id!!)
val finalResult = api.getTransactionResult(result.id!!)

assertEquals(TransactionStatus.SEALED, finalResult.status)
assertEquals(TransactionStatus.EXECUTED, finalResult.status)
assertTrue(finalResult.errorMessage.isNullOrEmpty(), "Transaction should not have any errors")

println("✅ Multiple UFix64 values transaction sealed successfully")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ class DoubleCadenceSerializerTest {

val json = Json.encodeToString(ufixValue)

assertTrue("Should contain zero") {
json.contains("\"0\"")
assertTrue("Should contain zero with decimal point") {
json.contains("\"0.0\"")
}
}

Expand All @@ -94,6 +94,56 @@ class DoubleCadenceSerializerTest {
}
}

@Test
fun testUFix64ZeroValueSerialization() {
// Test the exact scenario that was failing: UFix64 with zero value
val zeroValue = 0.0
val ufixValue = Cadence.ufix64(zeroValue)

// Serialize to JSON
val json = Json.encodeToString(ufixValue)
println("Zero value JSON: $json")

// Parse the JSON to extract the value
val jsonElement = Json.parseToJsonElement(json)
val valueString = jsonElement.jsonObject["value"]?.jsonPrimitive?.content ?: ""

// The critical test: verify zero value has decimal point
assertEquals("0.0", valueString, "Zero UFix64 value must be serialized as '0.0' not '0'")

// Verify it can be deserialized back
val deserializedValue = Json.decodeFromString<Cadence.Value.UFix64Value>(json)
assertEquals(zeroValue, deserializedValue.value, "Deserialized value should match original")

println("✅ UFix64 zero value serialization test passed")
println(" Serialized as: $valueString")
}

@Test
fun testUFix64WholeNumbersSerialization() {
// Test that whole numbers also get proper decimal formatting
val wholeNumbers = listOf(1.0, 5.0, 100.0, 1000.0)

for (wholeNumber in wholeNumbers) {
val ufixValue = Cadence.ufix64(wholeNumber)
val json = Json.encodeToString(ufixValue)

val jsonElement = Json.parseToJsonElement(json)
val valueString = jsonElement.jsonObject["value"]?.jsonPrimitive?.content ?: ""

// Verify whole numbers include decimal point
assertTrue("Whole number $wholeNumber should have decimal point in serialized form: $valueString") {
valueString.contains(".")
}

// Verify it deserializes correctly
val deserializedValue = Json.decodeFromString<Cadence.Value.UFix64Value>(json)
assertEquals(wholeNumber, deserializedValue.value, "Deserialized value should match original")
}

println("✅ UFix64 whole numbers serialization test passed")
}

@Test
fun testDirectConversion() {
// Test the conversion logic directly
Expand Down
Loading
Loading