Initial commit: Pezkuwi Wallet Android

Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
@@ -0,0 +1,26 @@
package android.util;
// Replace not mockable Log.xxx functions
public class Log {
public static int d(String tag, String msg) {
System.out.println("DEBUG: " + tag + ": " + msg);
return 0;
}
public static int i(String tag, String msg) {
System.out.println("INFO: " + tag + ": " + msg);
return 0;
}
public static int w(String tag, String msg) {
System.out.println("WARN: " + tag + ": " + msg);
return 0;
}
public static int e(String tag, String msg) {
System.out.println("ERROR: " + tag + ": " + msg);
return 0;
}
// add other methods if required...
}
@@ -0,0 +1,234 @@
package io.novafoundation.nova.runtime.extrinsic.visitor.impl
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicWalk
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.RealExtrinsicWalk
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.batch.BatchAllNode
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProvider
import io.novafoundation.nova.test_shared.any
import io.novafoundation.nova.test_shared.whenever
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertArrayEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
internal class BatchAllWalkTest {
@Mock
private lateinit var runtimeProvider: RuntimeProvider
@Mock
private lateinit var chainRegistry: ChainRegistry
@Mock
private lateinit var runtime: RuntimeSnapshot
@Mock
private lateinit var metadata: RuntimeMetadata
@Mock
private lateinit var utilityModule: Module
private lateinit var extrinsicWalk: ExtrinsicWalk
private val itemCompletedType = Event("ItemCompleted", index = 0 to 0, documentation = emptyList(), arguments = emptyList())
private val batchCompletedType = Event("BatchCompleted", index = 0 to 1, documentation = emptyList(), arguments = emptyList())
private val itemFailedType = Event("ItemFailed", index = 0 to 2, documentation = emptyList(), arguments = emptyList())
private val signer = byteArrayOf(0x00)
private val testModuleMocker = TestModuleMocker()
private val testEvent = testModuleMocker.testEvent
private val testInnerCall = testModuleMocker.testInnerCall
@Before
fun setup() = runBlocking {
whenever(utilityModule.events).thenReturn(
mapOf(
batchCompletedType.name to batchCompletedType,
itemCompletedType.name to itemCompletedType,
itemFailedType.name to itemFailedType
)
)
whenever(metadata.modules).thenReturn(mapOf("Utility" to utilityModule))
whenever(runtime.metadata).thenReturn(metadata)
whenever(runtimeProvider.get()).thenReturn(runtime)
whenever(chainRegistry.getRuntimeProvider(any())).thenReturn(runtimeProvider)
extrinsicWalk = RealExtrinsicWalk(chainRegistry, knownNodes = listOf(BatchAllNode()))
}
@Test
fun shouldVisitSucceededSingleBatchedCall() = runBlocking {
val innerBatchEvents = listOf(testEvent)
val events = innerBatchEvents + listOf(itemCompleted(), batchCompleted(), extrinsicSuccess())
val extrinsic = createExtrinsic(
call = batchCall(testInnerCall),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(true, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerBatchEvents, visit.events)
}
@Test
fun shouldVisitFailedSingleBatchedCall() = runBlocking {
val events = listOf(extrinsicFailed())
val extrinsic = createExtrinsic(
call = batchCall(testInnerCall),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(false, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(true, visit.events.isEmpty())
}
@Test
fun shouldVisitSucceededMultipleBatchedCalls() = runBlocking {
val innerBatchEvents = listOf(testEvent)
val events = buildList {
addAll(innerBatchEvents)
add(itemCompleted())
addAll(innerBatchEvents)
add(itemCompleted())
add(batchCompleted())
add(extrinsicSuccess())
}
val extrinsic = createExtrinsic(
call = batchCall(testInnerCall, testInnerCall),
events = events
)
val visits = extrinsicWalk.walkMultipleIgnoringBranches(extrinsic, expectedSize = 2)
visits.forEach { visit ->
assertEquals(true, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerBatchEvents, visit.events)
}
}
@Test
fun shouldVisitFailedMultipleBatchedCalls() = runBlocking {
val events = listOf(extrinsicFailed())
val extrinsic = createExtrinsic(
call = batchCall(testInnerCall, testInnerCall),
events = events
)
val visits = extrinsicWalk.walkMultipleIgnoringBranches(extrinsic, expectedSize = 2)
visits.forEach { visit ->
assertEquals(false, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(true, visit.events.isEmpty())
}
}
@Test
fun shouldVisitNestedBatches() = runBlocking {
val innerBatchEvents = listOf(testEvent)
val events = buildList {
// first level batch starts
addAll(innerBatchEvents)
add(itemCompleted())
run {
addAll(innerBatchEvents)
add(itemCompleted())
run {
addAll(innerBatchEvents)
add(itemCompleted())
addAll(innerBatchEvents)
add(itemCompleted())
add(batchCompleted())
}
add(itemCompleted())
add(batchCompleted())
}
add(itemCompleted())
addAll(innerBatchEvents)
add(itemCompleted())
// first level batch ends
add(batchCompleted())
add(extrinsicSuccess())
}
val extrinsic = createExtrinsic(
call = batchCall(
testInnerCall,
batchCall(
testInnerCall,
batchCall(
testInnerCall,
testInnerCall
)
),
testInnerCall
),
events = events
)
val visits = extrinsicWalk.walkMultipleIgnoringBranches(extrinsic, expectedSize = 5)
visits.forEach { visit ->
assertEquals(true, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerBatchEvents, visit.events)
}
}
private fun createExtrinsic(
call: GenericCall.Instance,
events: List<GenericEvent.Instance>
) = createExtrinsic(signer, call, events)
private fun itemCompleted(): GenericEvent.Instance {
return GenericEvent.Instance(utilityModule, itemCompletedType, arguments = emptyList())
}
private fun batchCompleted(): GenericEvent.Instance {
return GenericEvent.Instance(utilityModule, batchCompletedType, arguments = emptyList())
}
private fun batchCall(vararg innerCalls: GenericCall.Instance): GenericCall.Instance {
return mockCall(
moduleName = Modules.UTILITY,
callName = "batch_all",
arguments = mapOf(
"calls" to innerCalls.toList()
)
)
}
}
@@ -0,0 +1,146 @@
package io.novafoundation.nova.runtime.extrinsic.visitor.impl
import io.novafoundation.nova.common.data.network.runtime.binding.MultiAddress
import io.novafoundation.nova.common.data.network.runtime.binding.bindMultiAddress
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicWalk
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.walkToList
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents
import io.novafoundation.nova.test_shared.whenever
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
import io.novasama.substrate_sdk_android.runtime.metadata.call
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
import io.novasama.substrate_sdk_android.runtime.metadata.module.MetadataFunction
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
import org.junit.Assert
import org.mockito.Mockito
import java.math.BigInteger
fun createTestModuleWithCall(
moduleName: String,
callName: String
): Module {
return Module(
name = moduleName,
storage = null,
calls = mapOf(
callName to MetadataFunction(
name = callName,
arguments = emptyList(),
documentation = emptyList(),
index = 0 to 0
)
),
events = emptyMap(),
constants = emptyMap(),
errors = emptyMap(),
index = BigInteger.ZERO
)
}
fun createExtrinsic(
signer: AccountId,
call: GenericCall.Instance,
events: List<GenericEvent.Instance>
) = ExtrinsicWithEvents(
extrinsic = Extrinsic.Instance(
type = Extrinsic.ExtrinsicType.Signed(
accountIdentifier = bindMultiAddress(MultiAddress.Id(signer)),
signature = null,
signedExtras = emptyMap()
),
call = call,
),
extrinsicHash = "0x",
events = events
)
fun extrinsicSuccess(): GenericEvent.Instance {
return mockEvent("System", "ExtrinsicSuccess")
}
fun extrinsicFailed(): GenericEvent.Instance {
return mockEvent("System", "ExtrinsicFailed")
}
fun mockEvent(moduleName: String, eventName: String, arguments: List<Any?> = emptyList()): GenericEvent.Instance {
val module = Mockito.mock(Module::class.java)
whenever(module.name).thenReturn(moduleName)
val event = Mockito.mock(Event::class.java)
whenever(event.name).thenReturn(eventName)
return GenericEvent.Instance(
module = module,
event = event,
arguments = arguments
)
}
fun mockCall(moduleName: String, callName: String, arguments: Map<String, Any?> = emptyMap()): GenericCall.Instance {
val module = Mockito.mock(Module::class.java)
whenever(module.name).thenReturn(moduleName)
val function = Mockito.mock(MetadataFunction::class.java)
whenever(function.name).thenReturn(callName)
return GenericCall.Instance(
module = module,
function = function,
arguments = arguments
)
}
class TestModuleMocker {
val testModule = createTestModuleWithCall(moduleName = "Test", callName = "test")
val testInnerCall = GenericCall.Instance(
module = testModule,
function = testModule.call("test"),
arguments = emptyMap()
)
val testEvent = mockEvent(testModule.name, "test")
operator fun component1(): GenericCall.Instance {
return testInnerCall
}
operator fun component2():GenericEvent.Instance {
return testEvent
}
}
suspend fun ExtrinsicWalk.walkSingleIgnoringBranches(extrinsicWithEvents: ExtrinsicWithEvents): ExtrinsicVisit {
val visits = walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT).ignoreBranches()
Assert.assertEquals(1, visits.size)
return visits.single()
}
suspend fun ExtrinsicWalk.walkToList(extrinsicWithEvents: ExtrinsicWithEvents): List<ExtrinsicVisit> {
return walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT)
}
suspend fun ExtrinsicWalk.walkEmpty(extrinsicWithEvents: ExtrinsicWithEvents) {
val visits = walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT).ignoreBranches()
Assert.assertTrue(visits.isEmpty())
}
suspend fun ExtrinsicWalk.walkMultipleIgnoringBranches(extrinsicWithEvents: ExtrinsicWithEvents, expectedSize: Int): List<ExtrinsicVisit> {
val visits = walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT).ignoreBranches()
Assert.assertEquals(expectedSize, visits.size)
return visits
}
private fun List<ExtrinsicVisit>.ignoreBranches(): List<ExtrinsicVisit> {
return filterNot { it.hasRegisteredNode }
}
@@ -0,0 +1,264 @@
package io.novafoundation.nova.runtime.extrinsic.visitor.impl
import io.novafoundation.nova.common.utils.Modules
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicWalk
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.RealExtrinsicWalk
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.batch.ForceBatchNode
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProvider
import io.novafoundation.nova.test_shared.any
import io.novafoundation.nova.test_shared.whenever
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertArrayEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
internal class ForceBatchWalkTest {
@Mock
private lateinit var runtimeProvider: RuntimeProvider
@Mock
private lateinit var chainRegistry: ChainRegistry
@Mock
private lateinit var runtime: RuntimeSnapshot
@Mock
private lateinit var metadata: RuntimeMetadata
@Mock
private lateinit var utilityModule: Module
private lateinit var extrinsicWalk: ExtrinsicWalk
private val itemCompletedType = Event("ItemCompleted", index = 0 to 0, documentation = emptyList(), arguments = emptyList())
private val batchCompletedType = Event("BatchCompleted", index = 0 to 1, documentation = emptyList(), arguments = emptyList())
private val itemFailedType = Event("ItemFailed", index = 0 to 2, documentation = emptyList(), arguments = emptyList())
private val batchCompletedWithErrorsType = Event("BatchCompletedWithErrors", index = 0 to 3, documentation = emptyList(), arguments = emptyList())
private val signer = byteArrayOf(0x00)
private val testModuleMocker = TestModuleMocker()
private val testEvent = testModuleMocker.testEvent
private val testInnerCall = testModuleMocker.testInnerCall
@Before
fun setup() = runBlocking {
whenever(utilityModule.events).thenReturn(
mapOf(
batchCompletedType.name to batchCompletedType,
itemCompletedType.name to itemCompletedType,
itemFailedType.name to itemFailedType,
batchCompletedWithErrorsType.name to batchCompletedWithErrorsType
)
)
whenever(metadata.modules).thenReturn(mapOf("Utility" to utilityModule))
whenever(runtime.metadata).thenReturn(metadata)
whenever(runtimeProvider.get()).thenReturn(runtime)
whenever(chainRegistry.getRuntimeProvider(any())).thenReturn(runtimeProvider)
extrinsicWalk = RealExtrinsicWalk(chainRegistry, knownNodes = listOf(ForceBatchNode()))
}
@Test
fun shouldVisitSucceededSingleBatchedCall() = runBlocking {
val innerBatchEvents = listOf(testEvent)
val events = innerBatchEvents + listOf(itemCompleted(), batchCompleted(), extrinsicSuccess())
val extrinsic = createExtrinsic(
call = forceBatchCall(testInnerCall),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(true, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerBatchEvents, visit.events)
}
@Test
fun shouldVisitFailedSingleBatchedCall() = runBlocking {
val innerBatchEvents = listOf(testEvent)
val events = innerBatchEvents + listOf(itemFailed(), batchCompletedWithErrors(), extrinsicSuccess())
val extrinsic = createExtrinsic(
call = forceBatchCall(testInnerCall),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(false, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(true, visit.events.isEmpty())
}
@Test
fun shouldVisitSucceededMultipleBatchedCalls() = runBlocking {
val innerBatchEvents = listOf(testEvent)
val events = buildList {
addAll(innerBatchEvents)
add(itemCompleted())
addAll(innerBatchEvents)
add(itemCompleted())
add(batchCompleted())
add(extrinsicSuccess())
}
val extrinsic = createExtrinsic(
call = forceBatchCall(testInnerCall, testInnerCall),
events = events
)
val visits = extrinsicWalk.walkMultipleIgnoringBranches(extrinsic, expectedSize = 2)
visits.forEach { visit ->
assertEquals(true, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerBatchEvents, visit.events)
}
}
@Test
fun shouldVisitMixedMultipleBatchedCalls() = runBlocking {
val innerBatchEvents = listOf(testEvent)
val events = buildList {
addAll(innerBatchEvents)
add(itemCompleted())
add(itemFailed())
addAll(innerBatchEvents)
add(itemCompleted())
add(batchCompletedWithErrors())
add(extrinsicSuccess())
}
val extrinsic = createExtrinsic(
call = forceBatchCall(testInnerCall, testInnerCall, testInnerCall),
events = events
)
val visits = extrinsicWalk.walkMultipleIgnoringBranches(extrinsic, expectedSize = 3)
val expected: List<Pair<Boolean, List<GenericEvent.Instance>>> = listOf(
true to innerBatchEvents,
false to emptyList(),
true to innerBatchEvents
)
visits.zip(expected).forEach { (visit, expected) ->
val (expectedSuccess, expectedEvents) = expected
assertEquals(expectedSuccess, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(expectedEvents, visit.events)
}
}
@Test
fun shouldVisitNestedBatches() = runBlocking {
val innerBatchEvents = listOf(testEvent)
val events = buildList {
addAll(innerBatchEvents)
add(itemCompleted())
run {
addAll(innerBatchEvents)
add(itemCompleted())
run {
addAll(innerBatchEvents)
add(itemCompleted())
addAll(innerBatchEvents)
add(itemCompleted())
add(batchCompleted())
}
add(itemCompleted())
add(batchCompleted())
}
add(itemCompleted())
addAll(innerBatchEvents)
add(itemCompleted())
// first level batch ends
add(batchCompleted())
add(extrinsicSuccess())
}
val extrinsic = createExtrinsic(
call = forceBatchCall(
testInnerCall,
forceBatchCall(
testInnerCall,
forceBatchCall(
testInnerCall,
testInnerCall
)
),
testInnerCall
),
events = events
)
val visits = extrinsicWalk.walkMultipleIgnoringBranches(extrinsic, expectedSize = 5)
visits.forEach { visit ->
assertEquals(true, visit.success)
assertArrayEquals(signer, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerBatchEvents, visit.events)
}
}
private fun createExtrinsic(
call: GenericCall.Instance,
events: List<GenericEvent.Instance>
) = createExtrinsic(signer, call, events)
private fun itemCompleted(): GenericEvent.Instance {
return GenericEvent.Instance(utilityModule, itemCompletedType, arguments = emptyList())
}
private fun itemFailed(): GenericEvent.Instance {
return GenericEvent.Instance(utilityModule, itemFailedType, arguments = emptyList())
}
private fun batchCompleted(): GenericEvent.Instance {
return GenericEvent.Instance(utilityModule, batchCompletedType, arguments = emptyList())
}
private fun batchCompletedWithErrors(): GenericEvent.Instance {
return GenericEvent.Instance(utilityModule, batchCompletedWithErrorsType, arguments = emptyList())
}
private fun forceBatchCall(vararg innerCalls: GenericCall.Instance): GenericCall.Instance {
return mockCall(
moduleName = Modules.UTILITY,
callName = "force_batch",
arguments = mapOf(
"calls" to innerCalls.toList()
)
)
}
}
@@ -0,0 +1,230 @@
package io.novafoundation.nova.runtime.extrinsic.visitor.impl
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicWalk
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.RealExtrinsicWalk
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.multisig.MultisigNode
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.multisig.generateMultisigAddress
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProvider
import io.novafoundation.nova.test_shared.any
import io.novafoundation.nova.test_shared.whenever
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertArrayEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
internal class MultisigWalkTest {
@Mock
private lateinit var runtimeProvider: RuntimeProvider
@Mock
private lateinit var chainRegistry: ChainRegistry
@Mock
private lateinit var runtime: RuntimeSnapshot
@Mock
private lateinit var metadata: RuntimeMetadata
@Mock
private lateinit var multisigModule: Module
private lateinit var extrinsicWalk: ExtrinsicWalk
private val multisigExecutedEvent = Event("MultisigExecuted", index = 0 to 0, documentation = emptyList(), arguments = emptyList())
private val multisigApprovalEvent = Event("MultisigApproval", index = 0 to 1, documentation = emptyList(), arguments = emptyList())
private val multisigNewMultisigEvent = Event("NewMultisig", index = 0 to 1, documentation = emptyList(), arguments = emptyList())
private val signatory = byteArrayOf(0x00)
private val otherSignatories = listOf(byteArrayOf(0x01))
private val threshold = 2
private val multisig = generateMultisigAddress(
signatory = signatory.intoKey(),
otherSignatories = otherSignatories.map { it.intoKey() },
threshold = threshold
).value
private val testModuleMocker = TestModuleMocker()
private val testEvent = testModuleMocker.testEvent
private val testInnerCall = testModuleMocker.testInnerCall
@Before
fun setup() = runBlocking {
// Explicitly using string literals instead of accessing name property as this would result in Unfinished stubbing exception
whenever(multisigModule.events).thenReturn(
mapOf(
"MultisigExecuted" to multisigExecutedEvent,
"MultisigApproval" to multisigApprovalEvent,
"NewMultisig" to multisigNewMultisigEvent
)
)
whenever(multisigModule.name).thenReturn("Multisig")
whenever(metadata.modules).thenReturn(mapOf("Multisig" to multisigModule))
whenever(runtime.metadata).thenReturn(metadata)
whenever(runtimeProvider.get()).thenReturn(runtime)
whenever(chainRegistry.getRuntimeProvider(any())).thenReturn(runtimeProvider)
extrinsicWalk = RealExtrinsicWalk(chainRegistry, knownNodes = listOf(MultisigNode()))
}
@Test
fun shouldVisitSucceededSingleMultisigCall() = runBlocking {
val innerMultisigEvents = listOf(testEvent)
val events = innerMultisigEvents + listOf(multisigExecuted(success = true), extrinsicSuccess())
val extrinsic = createExtrinsic(
signer = signatory,
call = multisig_call(
innerCall = testInnerCall,
threshold = threshold,
otherSignatories = otherSignatories
),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(true, visit.success)
assertArrayEquals(multisig, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerMultisigEvents, visit.events)
}
@Test
fun shouldVisitFailedSingleMultisigCall() = runBlocking {
val innerMultisigEvents = emptyList<GenericEvent.Instance>()
val events = listOf(multisigExecuted(success = false), extrinsicSuccess())
val extrinsic = createExtrinsic(
signer = signatory,
call = multisig_call(
innerCall = testInnerCall,
threshold = threshold,
otherSignatories = otherSignatories
),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(false, visit.success)
assertArrayEquals(multisig, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerMultisigEvents, visit.events)
}
@Test
fun shouldVisitNewMultisigCall() = runBlocking {
val events = listOf(newMultisig(), extrinsicSuccess())
val extrinsic = createExtrinsic(
signer = signatory,
call = multisig_call(
innerCall = testInnerCall,
threshold = threshold,
otherSignatories = otherSignatories
),
events = events
)
extrinsicWalk.walkEmpty(extrinsic)
}
@Test
fun shouldVisitMultisigApprovalCall() = runBlocking {
val events = listOf(multisigApproval(), extrinsicSuccess())
val extrinsic = createExtrinsic(
signer = signatory,
call = multisig_call(
innerCall = testInnerCall,
threshold = threshold,
otherSignatories = otherSignatories
),
events = events
)
extrinsicWalk.walkEmpty(extrinsic)
}
@Test
fun shouldVisitSucceededNestedMultisigCalls() = runBlocking {
val events = listOf(
newMultisig(),
multisigExecuted(success = true),
extrinsicSuccess()
)
val otherSignatories2 = otherSignatories
val threshold2 = 1
val extrinsic = createExtrinsic(
signer = signatory,
call = multisig_call(
threshold = threshold,
otherSignatories = otherSignatories,
innerCall = multisig_call(
innerCall = testInnerCall,
threshold = threshold2,
otherSignatories = otherSignatories2
),
),
events = events
)
val visit = extrinsicWalk.walkToList(extrinsic)
assertEquals(2, visit.size)
val visit1 = visit[0]
assertEquals(true, visit1.success)
assertArrayEquals(signatory, visit1.origin)
val visit2 = visit[1]
assertEquals(true, visit2.success)
assertArrayEquals(multisig, visit2.origin)
}
private fun multisigExecuted(success: Boolean): GenericEvent.Instance {
val outcomeVariant = if (success) "Ok" else "Err"
val outcome = DictEnum.Entry(name = outcomeVariant, value = null)
return GenericEvent.Instance(multisigModule, multisigExecutedEvent, arguments = listOf(null, null, null, null, outcome))
}
private fun newMultisig(): GenericEvent.Instance {
return GenericEvent.Instance(multisigModule, multisigNewMultisigEvent, arguments = emptyList())
}
private fun multisigApproval(): GenericEvent.Instance {
return GenericEvent.Instance(multisigModule, multisigApprovalEvent, arguments = emptyList())
}
private fun multisig_call(
innerCall: GenericCall.Instance,
threshold: Int,
otherSignatories: List<ByteArray>
): GenericCall.Instance {
return mockCall(
moduleName = "Multisig",
callName = "as_multi",
arguments = mapOf(
"threshold" to threshold.toBigInteger(),
"other_signatories" to otherSignatories,
"call" to innerCall,
// other args are not relevant
)
)
}
}
@@ -0,0 +1,227 @@
package io.novafoundation.nova.runtime.extrinsic.visitor.impl
import io.novafoundation.nova.common.data.network.runtime.binding.MultiAddress
import io.novafoundation.nova.common.data.network.runtime.binding.bindMultiAddress
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicWalk
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.RealExtrinsicWalk
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.proxy.ProxyNode
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProvider
import io.novafoundation.nova.test_shared.any
import io.novafoundation.nova.test_shared.whenever
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertArrayEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
internal class ProxyWalkTest {
@Mock
private lateinit var runtimeProvider: RuntimeProvider
@Mock
private lateinit var chainRegistry: ChainRegistry
@Mock
private lateinit var runtime: RuntimeSnapshot
@Mock
private lateinit var metadata: RuntimeMetadata
@Mock
private lateinit var proxyModule: Module
private lateinit var extrinsicWalk: ExtrinsicWalk
private val proxyExecutedType = Event("ProxyExecuted", index = 0 to 0, documentation = emptyList(), arguments = emptyList())
private val proxy = byteArrayOf(0x00)
private val proxied = byteArrayOf(0x01)
private val testModuleMocker = TestModuleMocker()
private val testEvent = testModuleMocker.testEvent
private val testInnerCall = testModuleMocker.testInnerCall
@Before
fun setup() = runBlocking {
whenever(proxyModule.events).thenReturn(mapOf("ProxyExecuted" to proxyExecutedType))
whenever(metadata.modules).thenReturn(mapOf("Proxy" to proxyModule))
whenever(runtime.metadata).thenReturn(metadata)
whenever(runtimeProvider.get()).thenReturn(runtime)
whenever(chainRegistry.getRuntimeProvider(any())).thenReturn(runtimeProvider)
extrinsicWalk = RealExtrinsicWalk(chainRegistry, knownNodes = listOf(ProxyNode()))
}
@Test
fun shouldVisitSucceededSimpleCall() = runBlocking {
val events = listOf(testEvent, extrinsicSuccess())
val extrinsic = createExtrinsic(
signer = proxied,
call = testInnerCall,
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(true, visit.success)
assertArrayEquals(proxied, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(events, visit.events)
}
@Test
fun shouldVisitFailedSimpleCall() = runBlocking {
val events = listOf(extrinsicFailed())
val extrinsic = createExtrinsic(
signer = proxied,
call = testInnerCall,
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(false, visit.success)
assertArrayEquals(proxied, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(events, visit.events)
}
@Test
fun shouldVisitSucceededSingleProxyCall() = runBlocking {
val innerProxyEvents = listOf(testEvent)
val events = innerProxyEvents + listOf(proxyExecuted(success = true), extrinsicSuccess())
val extrinsic = createExtrinsic(
signer = proxy,
call = proxyCall(
real = proxied,
innerCall = testInnerCall
),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(true, visit.success)
assertArrayEquals(proxied, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerProxyEvents, visit.events)
}
@Test
fun shouldVisitFailedSingleProxyCall() = runBlocking {
val innerProxyEvents = emptyList<GenericEvent.Instance>()
val events = listOf(proxyExecuted(success = false), extrinsicSuccess())
val extrinsic = createExtrinsic(
signer = proxy,
call = proxyCall(
real = proxied,
innerCall = testInnerCall
),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(false, visit.success)
assertArrayEquals(proxied, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerProxyEvents, visit.events)
}
@Test
fun shouldVisitSucceededMultipleProxyCalls() = runBlocking {
val innerProxyEvents = listOf(testEvent)
val events = innerProxyEvents + listOf(proxyExecuted(success = true), proxyExecuted(success = true), proxyExecuted(success = true), extrinsicSuccess())
val proxy1 = byteArrayOf(0x00)
val proxy2 = byteArrayOf(0x01)
val proxy3 = byteArrayOf(0x02)
val proxied = byteArrayOf(0x10)
val extrinsic = createExtrinsic(
signer = proxy1,
call = proxyCall(
real = proxy2,
innerCall = proxyCall(
real = proxy3,
innerCall = proxyCall(
real = proxied,
innerCall = testInnerCall
)
)
),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(true, visit.success)
assertArrayEquals(proxied, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerProxyEvents, visit.events)
}
@Test
fun shouldVisitFailedMultipleProxyCalls() = runBlocking {
val innerProxyEvents = emptyList<GenericCall.Instance>()
val events = listOf(proxyExecuted(success = false), proxyExecuted(success = true), extrinsicSuccess()) // only outer-most proxy succeeded
val proxy1 = byteArrayOf(0x00)
val proxy2 = byteArrayOf(0x01)
val proxy3 = byteArrayOf(0x02)
val proxied = byteArrayOf(0x10)
val extrinsic = createExtrinsic(
signer = proxy1,
call = proxyCall(
real = proxy2,
innerCall = proxyCall(
real = proxy3,
innerCall = proxyCall(
real = proxied,
innerCall = testInnerCall
)
)
),
events = events
)
val visit = extrinsicWalk.walkSingleIgnoringBranches(extrinsic)
assertEquals(false, visit.success)
assertArrayEquals(proxied, visit.origin)
assertEquals(testInnerCall, visit.call)
assertEquals(innerProxyEvents, visit.events)
}
private fun proxyExecuted(success: Boolean): GenericEvent.Instance {
val outcomeVariant = if (success) "Ok" else "Err"
val outcome = DictEnum.Entry(name = outcomeVariant, value = null)
return GenericEvent.Instance(proxyModule, proxyExecutedType, arguments = listOf(outcome))
}
private fun proxyCall(real: AccountId, innerCall: GenericCall.Instance): GenericCall.Instance {
return mockCall(
moduleName = "Proxy",
callName = "proxy",
arguments = mapOf(
"real" to bindMultiAddress(MultiAddress.Id(real)),
"call" to innerCall,
// other args are not relevant
)
)
}
}
@@ -0,0 +1,60 @@
package io.novafoundation.nova.runtime.extrinsic.visitor.impl.multisig
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.multisig.generateMultisigAddress
import io.novasama.substrate_sdk_android.extensions.asEthereumAddress
import io.novasama.substrate_sdk_android.extensions.toAccountId
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import org.junit.Assert.assertArrayEquals
import org.junit.Test
class CommonsTest {
@Test
fun shouldGenerateSs58MultisigAddress() {
shouldGenerateMultisigAddress(
signatories = listOf(
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
"5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"
),
threshold = 2,
expected = "5DjYJStmdZ2rcqXbXGX7TW85JsrW6uG4y9MUcLq2BoPMpRA7",
)
}
@Test
fun shouldGenerateEvmMultisigAddress() {
shouldGenerateMultisigAddress(
signatories = listOf(
"0xC60eFE26b9b92380D1b2c479472323eC35F0f0aB",
"0x61d8c5647f4181f2c35996c62a6272967f5739a8",
"0xaCCaCE4056A930745218328BF086369Fbd61c212"
),
threshold =2,
expected = "0xb4e55b61678623fd5ece9c24e79d6c0532bee057",
)
}
private fun shouldGenerateMultisigAddress(
signatories: List<String>,
threshold: Int,
expected: String
) {
val accountIds = signatories.map { it.addressToAccountId() }
val expectedId = expected.addressToAccountId()
val actual = generateMultisigAddress(accountIds, threshold)
assertArrayEquals(expectedId.value, actual.value)
}
private fun String.addressToAccountId(): AccountIdKey {
return if (startsWith("0x")) {
asEthereumAddress().toAccountId().value
} else {
toAccountId()
}.intoKey()
}
}
@@ -0,0 +1,192 @@
package io.novafoundation.nova.runtime.multiNetwork.asset
import com.google.gson.Gson
import io.novafoundation.nova.core_db.dao.ChainAssetDao
import io.novafoundation.nova.core_db.dao.ChainDao
import io.novafoundation.nova.core_db.model.chain.AssetSourceLocal
import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal
import io.novafoundation.nova.runtime.multiNetwork.asset.remote.AssetFetcher
import io.novafoundation.nova.runtime.multiNetwork.asset.remote.model.EVMAssetRemote
import io.novafoundation.nova.runtime.multiNetwork.asset.remote.model.EVMInstanceRemote
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.chainAssetIdOfErc20Token
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapEVMAssetRemoteToLocalAssets
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.test_shared.emptyDiff
import io.novafoundation.nova.test_shared.insertsElement
import io.novafoundation.nova.test_shared.removesElement
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.lenient
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
class EvmErc20AssetSyncServiceTest {
private val chainId = "chainId"
private val contractAddress = "0xc748673057861a797275CD8A068AbB95A902e8de"
private val assetId = chainAssetIdOfErc20Token(contractAddress)
private val buyProviders = mapOf("transak" to mapOf("network" to "ETHEREUM"))
private val sellProviders = mapOf("transak" to mapOf("network" to "ETHEREUM"))
private val REMOTE_ASSET = EVMAssetRemote(
symbol = "USDT",
precision = 6,
priceId = "usd",
name = "USDT",
icon = "https://url.com",
instances = listOf(
EVMInstanceRemote(chainId, contractAddress, buyProviders, sellProviders)
)
)
private val gson = Gson()
private val LOCAL_ASSETS = createLocalCopy(REMOTE_ASSET)
@Mock
lateinit var dao: ChainAssetDao
@Mock
lateinit var chaindao: ChainDao
@Mock
lateinit var assetFetcher: AssetFetcher
lateinit var evmAssetSyncService: EvmAssetsSyncService
@Before
fun setup() {
evmAssetSyncService = EvmAssetsSyncService(chaindao, dao, assetFetcher, gson)
}
@Test
fun `should insert new asset`() {
runBlocking {
localHasChains(chainId)
localReturnsERC20(emptyList())
remoteReturns(listOf(REMOTE_ASSET))
evmAssetSyncService.syncUp()
verify(dao).updateAssets(
insertAsset(chainId, assetId),
)
}
}
@Test
fun `should not insert the same asset`() {
runBlocking {
localHasChains(chainId)
localReturnsERC20(LOCAL_ASSETS)
remoteReturns(listOf(REMOTE_ASSET))
evmAssetSyncService.syncUp()
verify(dao).updateAssets(emptyDiff())
}
}
@Test
fun `should update assets's own params`() {
runBlocking {
localHasChains(chainId)
localReturnsERC20(LOCAL_ASSETS)
remoteReturns(listOf(REMOTE_ASSET.copy(name = "new name")))
evmAssetSyncService.syncUp()
verify(dao).updateAssets(
insertAsset(chainId, assetId),
)
}
}
@Test
fun `should remove asset`() {
runBlocking {
localHasChains(chainId)
localReturnsERC20(LOCAL_ASSETS)
remoteReturns(emptyList())
evmAssetSyncService.syncUp()
verify(dao).updateAssets(
removeAsset(chainId, assetId),
)
}
}
@Test
fun `should not overwrite enabled state`() {
runBlocking {
localHasChains(chainId)
localReturnsERC20(LOCAL_ASSETS.map { it.copy(enabled = false) })
remoteReturns(listOf(REMOTE_ASSET))
evmAssetSyncService.syncUp()
verify(dao).updateAssets(
emptyDiff(),
)
}
}
@Test
fun `should not modify manual assets`() {
runBlocking {
localHasChains(chainId)
localReturnsERC20(LOCAL_ASSETS)
localReturnsManual(emptyList())
remoteReturns(listOf(REMOTE_ASSET))
evmAssetSyncService.syncUp()
verify(dao).updateAssets(
emptyDiff(),
)
}
}
@Test
fun `should not insert assets for non-present chain`() {
runBlocking {
localHasChains(chainId)
localReturnsERC20(emptyList())
remoteReturns(listOf(REMOTE_ASSET.copy(instances = listOf(EVMInstanceRemote("changedChainId", contractAddress, buyProviders, sellProviders)))))
evmAssetSyncService.syncUp()
verify(dao).updateAssets(emptyDiff())
}
}
private suspend fun remoteReturns(assets: List<EVMAssetRemote>) {
`when`(assetFetcher.getEVMAssets()).thenReturn(assets)
}
private suspend fun localReturnsERC20(assets: List<ChainAssetLocal>) {
`when`(dao.getAssetsBySource(AssetSourceLocal.ERC20)).thenReturn(assets)
}
private suspend fun localHasChains(vararg chainIds: ChainId) {
`when`(chaindao.getAllChainIds()).thenReturn(chainIds.toList())
}
private suspend fun localReturnsManual(assets: List<ChainAssetLocal>) {
lenient().`when`(dao.getAssetsBySource(AssetSourceLocal.MANUAL)).thenReturn(assets)
}
private fun insertAsset(chainId: String, id: Int) = insertsElement<ChainAssetLocal> { it.chainId == chainId && it.id == id }
private fun removeAsset(chainId: String, id: Int) = removesElement<ChainAssetLocal> { it.chainId == chainId && it.id == id }
private fun createLocalCopy(remote: EVMAssetRemote): List<ChainAssetLocal> {
return mapEVMAssetRemoteToLocalAssets(remote, gson)
}
}
@@ -0,0 +1,353 @@
package io.novafoundation.nova.runtime.multiNetwork.chain
import com.google.gson.Gson
import io.novafoundation.nova.core_db.dao.ChainAssetDao
import io.novafoundation.nova.core_db.dao.ChainDao
import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal
import io.novafoundation.nova.core_db.model.chain.ChainExplorerLocal
import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal
import io.novafoundation.nova.core_db.model.chain.ChainLocal
import io.novafoundation.nova.core_db.model.chain.ChainNodeLocal
import io.novafoundation.nova.core_db.model.chain.JoinedChainInfo
import io.novafoundation.nova.core_db.model.chain.NodeSelectionPreferencesLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapExternalApisToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapRemoteAssetToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapRemoteChainToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapRemoteExplorersToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapRemoteNodesToLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.ChainFetcher
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainAssetRemote
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainExplorerRemote
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainExternalApiRemote
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainNodeRemote
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainRemote
import io.novafoundation.nova.test_shared.emptyDiff
import io.novafoundation.nova.test_shared.insertsElement
import io.novafoundation.nova.test_shared.removesElement
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
class ChainSyncServiceTest {
private val assetId = 0
private val nodeUrl = "url"
private val explorerName = "explorer"
private val transferApiUrl = "url"
private val REMOTE_CHAIN = ChainRemote(
chainId = "0x00",
name = "Test",
assets = listOf(
ChainAssetRemote(
assetId = assetId,
symbol = "TEST",
precision = 10,
name = "Test",
priceId = "test",
staking = listOf("test"),
type = null,
typeExtras = null,
icon = null,
buyProviders = emptyMap(),
sellProviders = emptyMap()
)
),
nodes = listOf(
ChainNodeRemote(
url = nodeUrl,
name = "test"
)
),
icon = "test",
addressPrefix = 0,
legacyAddressPrefix = null,
types = null,
options = emptySet(),
parentId = null,
externalApi = mapOf(
"history" to listOf(
ChainExternalApiRemote(
sourceType = "subquery",
url = transferApiUrl,
parameters = null // substrate history
)
)
),
explorers = listOf(
ChainExplorerRemote(
explorerName,
"extrinsic",
"account",
"event"
)
),
additional = emptyMap(),
nodeSelectionStrategy = null
)
private val gson = Gson()
private val LOCAL_CHAIN = createLocalCopy(REMOTE_CHAIN)
@Mock
lateinit var dao: ChainDao
@Mock
lateinit var chainAssetDao: ChainAssetDao
@Mock
lateinit var chainFetcher: ChainFetcher
lateinit var chainSyncService: ChainSyncService
@Before
fun setup() {
chainSyncService = ChainSyncService(dao, chainFetcher, gson)
}
@Test
fun `should insert new chain`() {
runBlocking {
localReturns(emptyList())
remoteReturns(listOf(REMOTE_CHAIN))
chainSyncService.syncUp()
verify(dao).applyDiff(
chainDiff = insertsChainWithId(REMOTE_CHAIN.chainId),
assetsDiff = insertsAssetWithId(assetId),
nodesDiff = insertsNodeWithUrl(nodeUrl),
explorersDiff = insertsExplorerByName(explorerName),
externalApisDiff = insertsTransferApiByUrl(transferApiUrl),
nodeSelectionPreferencesDiff = insertsNodeSelectionPreferences(REMOTE_CHAIN.chainId)
)
}
}
@Test
fun `should not insert the same chain`() {
runBlocking {
localReturns(listOf(LOCAL_CHAIN))
remoteReturns(listOf(REMOTE_CHAIN))
chainSyncService.syncUp()
verify(dao).applyDiff(
emptyDiff(),
emptyDiff(),
emptyDiff(),
emptyDiff(),
emptyDiff(),
emptyDiff()
)
}
}
@Test
fun `should update chain's own params`() {
runBlocking {
localReturns(listOf(LOCAL_CHAIN))
remoteReturns(listOf(REMOTE_CHAIN.copy(name = "new name")))
chainSyncService.syncUp()
verify(dao).applyDiff(
chainDiff = insertsChainWithId(REMOTE_CHAIN.chainId),
assetsDiff = emptyDiff(),
nodesDiff = emptyDiff(),
explorersDiff = emptyDiff(),
externalApisDiff = emptyDiff(),
nodeSelectionPreferencesDiff = emptyDiff()
)
}
}
@Test
fun `should update chain's asset`() {
runBlocking {
localReturns(listOf(LOCAL_CHAIN))
remoteReturns(
listOf(
REMOTE_CHAIN.copy(
assets = listOf(
REMOTE_CHAIN.assets.first().copy(symbol = "NEW")
)
)
)
)
chainSyncService.syncUp()
verify(dao).applyDiff(
chainDiff = emptyDiff(),
assetsDiff = insertsAssetWithId(assetId),
nodesDiff = emptyDiff(),
explorersDiff = emptyDiff(),
externalApisDiff = emptyDiff(),
nodeSelectionPreferencesDiff = emptyDiff()
)
}
}
@Test
fun `should update chain's node`() {
runBlocking {
localReturns(listOf(LOCAL_CHAIN))
remoteReturns(
listOf(
REMOTE_CHAIN.copy(
nodes = listOf(
REMOTE_CHAIN.nodes.first().copy(name = "NEW")
)
)
)
)
chainSyncService.syncUp()
verify(dao).applyDiff(
chainDiff = emptyDiff(),
assetsDiff = emptyDiff(),
nodesDiff = insertsNodeWithUrl(nodeUrl),
explorersDiff = emptyDiff(),
externalApisDiff = emptyDiff(),
nodeSelectionPreferencesDiff = emptyDiff()
)
}
}
@Test
fun `should update chain's explorer`() {
runBlocking {
localReturns(listOf(LOCAL_CHAIN))
remoteReturns(
listOf(
REMOTE_CHAIN.copy(
explorers = listOf(
REMOTE_CHAIN.explorers!!.first().copy(extrinsic = "NEW")
)
)
)
)
chainSyncService.syncUp()
verify(dao).applyDiff(
chainDiff = emptyDiff(),
assetsDiff = emptyDiff(),
nodesDiff = emptyDiff(),
explorersDiff = insertsExplorerByName(explorerName),
externalApisDiff = emptyDiff(),
nodeSelectionPreferencesDiff = emptyDiff()
)
}
}
@Test
fun `should update chain's transfer apis`() {
runBlocking {
localReturns(listOf(LOCAL_CHAIN))
val currentHistoryApi = REMOTE_CHAIN.externalApi!!.getValue("history").first()
val anotherUrl = "another url"
remoteReturns(
listOf(
REMOTE_CHAIN.copy(
externalApi = mapOf(
"history" to listOf(
currentHistoryApi,
currentHistoryApi.copy(url = anotherUrl)
)
)
)
)
)
chainSyncService.syncUp()
verify(dao).applyDiff(
chainDiff = emptyDiff(),
assetsDiff = emptyDiff(),
nodesDiff = emptyDiff(),
explorersDiff = emptyDiff(),
externalApisDiff = insertsTransferApiByUrl(anotherUrl),
nodeSelectionPreferencesDiff = emptyDiff()
)
}
}
@Test
fun `should remove chain`() {
runBlocking {
localReturns(listOf(LOCAL_CHAIN))
remoteReturns(emptyList())
chainSyncService.syncUp()
verify(dao).applyDiff(
chainDiff = removesChainWithId(REMOTE_CHAIN.chainId),
assetsDiff = removesAssetWithId(assetId),
nodesDiff = removesNodeWithUrl(nodeUrl),
explorersDiff = removesExplorerByName(explorerName),
externalApisDiff = removesTransferApiByUrl(transferApiUrl),
nodeSelectionPreferencesDiff = removesNodeSelectionPreferences(REMOTE_CHAIN.chainId)
)
}
}
private suspend fun remoteReturns(chains: List<ChainRemote>) {
`when`(chainFetcher.getChains()).thenReturn(chains)
}
private suspend fun localReturns(chains: List<JoinedChainInfo>) {
`when`(dao.getJoinChainInfo()).thenReturn(chains)
}
private fun insertsChainWithId(id: String) = insertsElement<ChainLocal> { it.id == id }
private fun insertsAssetWithId(id: Int) = insertsElement<ChainAssetLocal> { it.id == id }
private fun insertsNodeWithUrl(url: String) = insertsElement<ChainNodeLocal> { it.url == url }
private fun insertsExplorerByName(name: String) = insertsElement<ChainExplorerLocal> { it.name == name }
private fun insertsTransferApiByUrl(url: String) = insertsElement<ChainExternalApiLocal> { it.url == url }
private fun insertsNodeSelectionPreferences(id: String) = insertsElement<NodeSelectionPreferencesLocal> { it.chainId == id }
private fun removesChainWithId(id: String) = removesElement<ChainLocal> { it.id == id }
private fun removesAssetWithId(id: Int) = removesElement<ChainAssetLocal> { it.id == id }
private fun removesNodeWithUrl(url: String) = removesElement<ChainNodeLocal> { it.url == url }
private fun removesExplorerByName(name: String) = removesElement<ChainExplorerLocal> { it.name == name }
private fun removesTransferApiByUrl(url: String) = removesElement<ChainExternalApiLocal> { it.url == url }
private fun removesNodeSelectionPreferences(chainId: String) = removesElement<NodeSelectionPreferencesLocal> { it.chainId == chainId }
private fun createLocalCopy(remote: ChainRemote): JoinedChainInfo {
val domain = mapRemoteChainToLocal(remote, oldChain = null, source = ChainLocal.Source.DEFAULT, gson)
val nodeSelectionPreferences = NodeSelectionPreferencesLocal(
chainId = remote.chainId,
autoBalanceEnabled = true,
selectedNodeUrl = null
)
val assets = remote.assets.map { mapRemoteAssetToLocal(remote, it, gson, true) }
val nodes = mapRemoteNodesToLocal(remote)
val explorers = mapRemoteExplorersToLocal(remote)
val transferHistoryApis = mapExternalApisToLocal(remote)
return JoinedChainInfo(
chain = domain,
nodeSelectionPreferences = nodeSelectionPreferences,
nodes = nodes,
assets = assets,
explorers = explorers,
externalApis = transferHistoryApis
)
}
}
@@ -0,0 +1,41 @@
package io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl
import org.junit.Assert.assertEquals
import org.junit.Test
class RoundRobinStrategyTest {
private val nodes = listOf(
createFakeNode("1"),
createFakeNode("2"),
createFakeNode("3")
)
private val strategy = RoundRobinGenerator(nodes)
@Test
fun `collections should have the same sequence`() {
val iterator = strategy.generateNodeSequence()
.iterator()
nodes.forEach { assertEquals(it, iterator.next()) }
}
@Test
fun `sequence should be looped`() {
val iterator = strategy.generateNodeSequence()
.iterator()
repeat(nodes.size) { iterator.next() }
assertEquals(nodes.first(), iterator.next())
}
private fun createFakeNode(id: String) = NodeWithSaturatedUrl(
node = Chain.Node(unformattedUrl = id, name = id, chainId = "test", orderId = 0, isCustom = false),
saturatedUrl = id
)
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.runtime.multiNetwork.runtime
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.test_shared.whenever
import org.mockito.Mockito
object Mocks {
fun chain(id: String) : Chain {
val chain = Mockito.mock(Chain::class.java)
whenever(chain.id).thenReturn(id)
return chain
}
}
@@ -0,0 +1,318 @@
package io.novafoundation.nova.runtime.multiNetwork.runtime
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.TypesUsage
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.BaseTypeSynchronizer
import io.novafoundation.nova.test_shared.any
import io.novafoundation.nova.test_shared.eq
import io.novafoundation.nova.test_shared.thenThrowUnsafe
import io.novafoundation.nova.test_shared.whenever
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
@Ignore("Not stable tests") // FIXME
class RuntimeProviderTest {
lateinit var baseTypeSyncFlow: MutableSharedFlow<FileHash>
lateinit var chainSyncFlow: MutableSharedFlow<SyncResult>
lateinit var chain: Chain
@Mock
lateinit var runtime: RuntimeSnapshot
@Mock
lateinit var constructedRuntime: ConstructedRuntime
@Mock
lateinit var runtimeSyncService: RuntimeSyncService
@Mock
lateinit var runtimeCache: RuntimeFilesCache
@Mock
lateinit var runtimeFactory: RuntimeFactory
@Mock
lateinit var baseTypesSynchronizer: BaseTypeSynchronizer
lateinit var runtimeProvider: RuntimeProvider
@Before
fun setup() {
runBlocking {
chain = Mocks.chain(id = "1")
baseTypeSyncFlow = MutableSharedFlow()
chainSyncFlow = MutableSharedFlow()
whenever(constructedRuntime.runtime).thenReturn(runtime)
whenever(runtimeFactory.constructRuntime(any(), any())).thenReturn(constructedRuntime)
whenever(baseTypesSynchronizer.syncStatusFlow).thenAnswer { baseTypeSyncFlow }
whenever(runtimeSyncService.syncResultFlow(eq(chain.id))).thenAnswer { chainSyncFlow }
}
}
@Test(timeout = 500)
fun `should init from cache`() {
runBlocking {
initProvider()
val returnedRuntime = runtimeProvider.get()
verify(runtimeFactory, times(1)).constructRuntime(eq(chain.id), any())
assertEquals(returnedRuntime, runtime)
}
}
@Test
fun `should not reconstruct runtime if base types has remains the same`() {
runBlocking {
initProvider()
currentBaseTypesHash("Hash")
baseTypeSyncFlow.emit("Hash")
verifyReconstructionNotStarted()
}
}
@Test
fun `should not reconstruct runtime on base types change if they are not used`() {
runBlocking {
initProvider(typesUsage = TypesUsage.OWN)
baseTypeSyncFlow.emit("Hash")
verifyReconstructionNotStarted()
}
}
@Test
fun `should reconstruct runtime if base types changes`() {
runBlocking {
initProvider()
currentBaseTypesHash("Hash")
baseTypeSyncFlow.emit("Changed Hash")
verifyReconstructionStarted()
}
}
@Test
fun `should not reconstruct runtime if chain metadata or types did not change`() {
runBlocking {
initProvider()
currentChainTypesHash("Hash")
currentMetadataHash("Hash")
chainSyncFlow.emit(SyncResult(chain.id, metadataHash = "Hash", typesHash = "Hash"))
verifyReconstructionNotStarted()
}
}
@Test
fun `should reconstruct runtime if chain metadata or types changed`() {
runBlocking {
initProvider()
currentChainTypesHash("Hash")
currentMetadataHash("Hash")
chainSyncFlow.emit(SyncResult(chain.id, metadataHash = "Hash Changed", typesHash = "Hash"))
verifyReconstructionAfterInit(1)
chainSyncFlow.emit(SyncResult(chain.id, metadataHash = "Hash Changed", typesHash = "Hash Changed"))
verifyReconstructionAfterInit(2)
}
}
@Test
fun `should reconstruct runtime on chain info sync if cache init failed`() {
runBlocking {
withRuntimeFactoryFailing {
chainSyncFlow.emit(SyncResult(chain.id, metadataHash = "Hash Changed", typesHash = "Hash"))
verifyReconstructionStarted()
}
}
}
@Test
fun `should not reconstruct runtime if types and runtime were not synced`() {
runBlocking {
initProvider()
currentChainTypesHash("Hash")
currentMetadataHash("Hash")
chainSyncFlow.emit(SyncResult(chain.id, metadataHash = null, typesHash = null))
verifyReconstructionNotStarted()
}
}
@Test
fun `should wait until current job is finished before consider reconstructing runtime on runtime sync event`() {
runBlocking {
whenever(runtimeFactory.constructRuntime(any(), any())).thenAnswer {
runBlocking { chainSyncFlow.first() } // ensure runtime wont be returned until chainSyncFlow event
constructedRuntime
}
initProvider()
currentChainTypesHash("Hash")
currentMetadataHash("Hash")
chainSyncFlow.emit(SyncResult(chain.id, metadataHash = null, typesHash = null))
verifyReconstructionNotStarted()
}
}
@Test
fun `should wait until current job is finished before consider reconstructing runtime on types sync event`() {
runBlocking {
whenever(runtimeFactory.constructRuntime(any(), any())).thenAnswer {
runBlocking { baseTypeSyncFlow.first() } // ensure runtime wont be returned until baseTypeSyncFlow event
constructedRuntime
}
initProvider()
currentChainTypesHash("Hash")
currentMetadataHash("Hash")
baseTypeSyncFlow.emit("New hash")
verifyReconstructionNotStarted()
}
}
@Test
fun `should report missing cache for base types`() {
runBlocking {
withRuntimeFactoryFailing(BaseTypesNotInCacheException) {
verify(baseTypesSynchronizer, times(1)).cacheNotFound()
verify(runtimeSyncService, times(0)).cacheNotFound(any())
}
}
}
@Test
fun `should report missing cache for chain types or metadata`() {
runBlocking {
withRuntimeFactoryFailing(ChainInfoNotInCacheException) {
verify(runtimeSyncService, times(1)).cacheNotFound(eq(chain.id))
verify(baseTypesSynchronizer, times(0)).cacheNotFound()
}
}
}
@Test
fun `should construct runtime on base types sync if cache init failed`() {
runBlocking {
withRuntimeFactoryFailing {
baseTypeSyncFlow.emit("Hash")
verifyReconstructionStarted()
}
}
}
@Test
fun `should construct runtime on type usage change`() {
runBlocking {
initProvider(typesUsage = TypesUsage.BASE)
runtimeProvider.considerUpdatingTypesUsage(TypesUsage.OWN)
verifyReconstructionStarted()
}
}
@Test
fun `should not construct runtime on same type usage`() {
runBlocking {
initProvider(typesUsage = TypesUsage.BASE)
runtimeProvider.considerUpdatingTypesUsage(TypesUsage.BASE)
verifyReconstructionNotStarted()
}
}
private suspend fun verifyReconstructionNotStarted() {
verifyReconstructionAfterInit(0)
}
private suspend fun verifyReconstructionStarted() {
verifyReconstructionAfterInit(1)
}
private suspend fun withRuntimeFactoryFailing(exception: Exception = BaseTypesNotInCacheException, block: suspend () -> Unit) {
whenever(runtimeFactory.constructRuntime(any(), any())).thenThrowUnsafe(exception)
initProvider()
delay(10)
block()
}
private suspend fun verifyReconstructionAfterInit(times: Int) {
delay(10)
// + 1 since it is called once in init (cache)
verify(runtimeFactory, times(times + 1)).constructRuntime(eq(chain.id), any())
}
private fun currentBaseTypesHash(hash: String?) {
whenever(constructedRuntime.baseTypesHash).thenReturn(hash)
}
private fun currentMetadataHash(hash: String?) {
whenever(constructedRuntime.metadataHash).thenReturn(hash)
}
private fun currentChainTypesHash(hash: String?) {
whenever(constructedRuntime.ownTypesHash).thenReturn(hash)
}
private fun initProvider(typesUsage: TypesUsage? = null) {
val types = when (typesUsage) {
TypesUsage.OWN -> Chain.Types(url = "url", overridesCommon = true)
TypesUsage.BOTH -> Chain.Types(url = "url", overridesCommon = false)
else -> null
}
whenever(chain.types).thenReturn(types)
runtimeProvider = RuntimeProvider(runtimeFactory, runtimeSyncService, baseTypesSynchronizer, runtimeCache, chain)
}
}
@@ -0,0 +1,381 @@
package io.novafoundation.nova.runtime.multiNetwork.runtime
import com.google.gson.Gson
import io.novafoundation.nova.common.utils.md5
import io.novafoundation.nova.core_db.dao.ChainDao
import io.novafoundation.nova.core_db.model.chain.ChainRuntimeInfoLocal
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.TypesFetcher
import io.novafoundation.nova.test_shared.any
import io.novafoundation.nova.test_shared.eq
import io.novafoundation.nova.test_shared.whenever
import io.novasama.substrate_sdk_android.wsrpc.SocketService
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
import io.novasama.substrate_sdk_android.wsrpc.response.RpcResponse
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.Timeout
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnitRunner
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
private const val TEST_TYPES = "Stub"
@Ignore("Flaky tests due to concurrency issues")
@RunWith(MockitoJUnitRunner::class)
class RuntimeSyncServiceTest {
private val testChain by lazy {
Mocks.chain(id = "1")
}
@Mock
private lateinit var socket: SocketService
@Mock
private lateinit var testConnection: ChainConnection
@Mock
private lateinit var typesFetcher: TypesFetcher
@Mock
private lateinit var chainDao: ChainDao
@Mock
private lateinit var runtimeFilesCache: RuntimeFilesCache
@Mock
private lateinit var runtimeMetadataFetcher: RuntimeMetadataFetcher
@Mock
private lateinit var cacheMigrator: RuntimeCacheMigrator
private lateinit var syncDispatcher: SyncChainSyncDispatcher
private lateinit var service: RuntimeSyncService
private lateinit var syncResultFlow: Flow<SyncResult>
@JvmField
@Rule
val globalTimeout: Timeout = Timeout.seconds(10)
@Before
fun setup() = runBlocking {
whenever(testConnection.socketService).thenReturn(socket)
whenever(socket.jsonMapper).thenReturn(Gson())
whenever(typesFetcher.getTypes(any())).thenReturn(TEST_TYPES)
whenever(runtimeMetadataFetcher.fetchRawMetadata(any(), any())).thenReturn(RawRuntimeMetadata(metadataContent = byteArrayOf(0), isOpaque = false))
whenever(cacheMigrator.needsMetadataFetch(anyInt())).thenReturn(false)
syncDispatcher = Mockito.spy(SyncChainSyncDispatcher())
service = RuntimeSyncService(typesFetcher, runtimeFilesCache, chainDao, runtimeMetadataFetcher, cacheMigrator, syncDispatcher)
syncResultFlow = service.syncResultFlow(testChain.id)
.shareIn(GlobalScope, started = SharingStarted.Eagerly, replay = 1)
}
@Test
fun `should not start syncing new chain`() {
service.registerChain(chain = testChain, connection = testConnection)
assertNoSyncLaunched()
}
@Test
fun `should start syncing on runtime version apply`() {
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
assertSyncLaunchedOnce()
assertSyncCancelledTimes(1)
}
@Test
fun `should not start syncing the same chain`() {
runBlocking {
chainDaoReturnsBiggerRuntimeVersion()
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
assertSyncLaunchedOnce()
service.registerChain(chain = testChain, connection = testConnection)
// No new launches
assertSyncLaunchedOnce()
assertFalse(service.isSyncing(testChain.id))
}
}
@Test
fun `should sync modified chain`() {
runBlocking {
chainDaoReturnsBiggerRuntimeVersion()
val newChain = Mockito.mock(Chain::class.java)
whenever(newChain.id).thenAnswer { testChain.id }
whenever(newChain.types).thenReturn(Chain.Types(url = "Changed", overridesCommon = false))
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
assertSyncLaunchedOnce()
chainDaoReturnsSameRuntimeInfo()
service.registerChain(chain = newChain, connection = testConnection)
assertSyncLaunchedTimes(2)
val syncResult = syncResultFlow.first()
assertNull("Metadata should not sync", syncResult.metadataHash)
}
}
@Test
fun `should sync types when url is not null`() {
runBlocking {
chainDaoReturnsSameRuntimeInfo()
whenever(testChain.types).thenReturn(Chain.Types("Stub", overridesCommon = false))
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
val result = syncResultFlow.first()
assertNotNull(result.typesHash)
}
}
@Test
fun `should not sync types when url is null`() {
runBlocking {
chainDaoReturnsBiggerRuntimeVersion()
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
val result = syncResultFlow.first()
assertNull(result.typesHash)
}
}
@Test
fun `should cancel syncing when chain is unregistered`() {
runBlocking {
chainDaoReturnsBiggerRuntimeVersion()
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
assertSyncCancelledTimes(1)
assertSyncLaunchedOnce()
service.unregisterChain(testChain.id)
assertSyncCancelledTimes(2)
}
}
@Test
fun `should broadcast sync result`() {
runBlocking {
chainDaoReturnsBiggerRuntimeVersion()
whenever(testChain.types).thenReturn(Chain.Types("testUrl", overridesCommon = false))
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
val result = syncResultFlow.first()
assertEquals(TEST_TYPES.md5(), result.typesHash)
}
}
@Test
fun `should sync bigger version of metadata`() {
runBlocking {
chainDaoReturnsBiggerRuntimeVersion()
whenever(testChain.types).thenReturn(Chain.Types("testUrl", overridesCommon = false))
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
val syncResult = syncResultFlow.first()
assertNotNull(syncResult.metadataHash)
}
}
@Test
fun `should sync lower version of metadata`() {
runBlocking {
chainDaoReturnsLowerRuntimeInfo()
whenever(testChain.types).thenReturn(Chain.Types("testUrl", overridesCommon = false))
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
val syncResult = syncResultFlow.first()
assertNotNull(syncResult.metadataHash)
}
}
@Test
fun `should always sync chain info when cache is not found`() {
runBlocking {
chainDaoReturnsSameRuntimeInfo()
whenever(testChain.types).thenReturn(Chain.Types("testUrl", overridesCommon = false))
service.registerChain(chain = testChain, connection = testConnection)
assertNoSyncLaunched()
service.cacheNotFound(testChain.id)
assertSyncLaunchedOnce()
val syncResult = syncResultFlow.first()
assertNotNull(syncResult.metadataHash)
assertNotNull(syncResult.typesHash)
}
}
@Test
fun `should not sync the same version of metadata`() {
runBlocking {
chainDaoReturnsSameRuntimeInfo()
whenever(testChain.types).thenReturn(Chain.Types("testUrl", overridesCommon = false))
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
val syncResult = syncResultFlow.first()
assertNull(syncResult.metadataHash)
}
}
@Test
fun `should sync the same version of metadata when local migration required`() {
runBlocking {
chainDaoReturnsSameRuntimeInfo()
requiresLocalMigration()
whenever(testChain.types).thenReturn(Chain.Types("testUrl", overridesCommon = false))
service.registerChain(chain = testChain, connection = testConnection)
service.applyRuntimeVersion(testChain.id)
val syncResult = syncResultFlow.first()
assertNotNull(syncResult.metadataHash)
}
}
private suspend fun chainDaoReturnsBiggerRuntimeVersion() {
chainDaoReturnsRuntimeInfo(remoteVersion = 1, syncedVersion = 0)
}
private suspend fun chainDaoReturnsSameRuntimeInfo() {
chainDaoReturnsRuntimeInfo(remoteVersion = 1, syncedVersion = 1)
}
private suspend fun chainDaoReturnsLowerRuntimeInfo() {
chainDaoReturnsRuntimeInfo(remoteVersion = 0, syncedVersion = 1)
}
private suspend fun chainDaoReturnsRuntimeInfo(remoteVersion: Int, syncedVersion: Int) {
whenever(chainDao.runtimeInfo(any())).thenReturn(ChainRuntimeInfoLocal("1", syncedVersion, remoteVersion, null, localMigratorVersion = 1))
}
private suspend fun RuntimeSyncService.latestSyncResult(chainId: String) = syncResultFlow(chainId).first()
private suspend fun requiresLocalMigration() {
whenever(cacheMigrator.needsMetadataFetch(anyInt())).thenReturn(true)
}
private fun socketAnswersRequest(request: RuntimeRequest, response: Any?) {
whenever(socket.executeRequest(eq(request), deliveryType = any(), callback = any())).thenAnswer {
(it.arguments[2] as SocketService.ResponseListener<RpcResponse>).onNext(RpcResponse(jsonrpc = "2.0", response, id = 1, error = null))
object : SocketService.Cancellable {
override fun cancel() {
// pass
}
}
}
}
private fun assertNoSyncLaunched() {
verify(syncDispatcher, never()).launchSync(anyString(), any())
}
private fun assertSyncLaunchedOnce() {
verify(syncDispatcher, times(1)).launchSync(anyString(), any())
}
private fun assertSyncCancelledTimes(times: Int) {
verify(syncDispatcher, times(times)).cancelExistingSync(anyString())
}
private fun assertSyncLaunchedTimes(times: Int) {
verify(syncDispatcher, times(times)).launchSync(anyString(), any())
}
class SyncChainSyncDispatcher() : ChainSyncDispatcher {
private val syncingChains = Collections.newSetFromMap(ConcurrentHashMap<ChainId, Boolean>())
override fun isSyncing(chainId: String): Boolean {
return syncingChains.contains(chainId)
}
override fun syncFinished(chainId: String) {
syncingChains.remove(chainId)
}
override fun cancelExistingSync(chainId: String) {
syncingChains.remove(chainId)
}
override fun launchSync(chainId: String, action: suspend () -> Unit) = runBlocking {
syncingChains.add(chainId)
action()
}
}
}