mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 19:37:55 +00:00
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:
@@ -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...
|
||||
}
|
||||
+234
@@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+146
@@ -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 }
|
||||
}
|
||||
+264
@@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+230
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+227
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+60
@@ -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()
|
||||
}
|
||||
}
|
||||
+192
@@ -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)
|
||||
}
|
||||
}
|
||||
+353
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+41
@@ -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
|
||||
}
|
||||
}
|
||||
+318
@@ -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)
|
||||
}
|
||||
}
|
||||
+381
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user