mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 02:07:58 +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 @@
|
||||
/build
|
||||
@@ -0,0 +1,69 @@
|
||||
apply from: '../scripts/secrets.gradle'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
android {
|
||||
|
||||
defaultConfig {
|
||||
|
||||
|
||||
|
||||
buildConfigField "String", "CHAINS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/chains/v22/android/chains.json\""
|
||||
buildConfigField "String", "EVM_ASSETS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/assets/evm/v3/assets.json\""
|
||||
buildConfigField "String", "PRE_CONFIGURED_CHAINS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/chains/v22/preConfigured/chains.json\""
|
||||
buildConfigField "String", "PRE_CONFIGURED_CHAIN_DETAILS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/chains/v22/preConfigured/details\""
|
||||
|
||||
buildConfigField "String", "TEST_CHAINS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/tests/chains_for_testBalance.json\""
|
||||
|
||||
buildConfigField "String", "INFURA_API_KEY", readStringSecret("INFURA_API_KEY")
|
||||
buildConfigField "String", "DWELLIR_API_KEY", readStringSecret("DWELLIR_API_KEY")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
|
||||
}
|
||||
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
buildConfigField "String", "CHAINS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/chains/v22/android/chains.json\""
|
||||
buildConfigField "String", "EVM_ASSETS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/assets/evm/v3/assets.json\""
|
||||
buildConfigField "String", "PRE_CONFIGURED_CHAINS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/chains/v22/preConfigured/chains.json\""
|
||||
buildConfigField "String", "PRE_CONFIGURED_CHAIN_DETAILS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/chains/v22/preConfigured/details\""
|
||||
}
|
||||
}
|
||||
namespace 'io.novafoundation.nova.runtime'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation project(":common")
|
||||
implementation project(":core-db")
|
||||
|
||||
implementation project(":core-api")
|
||||
|
||||
implementation project(":bindings:metadata_shortener")
|
||||
implementation project(":bindings:sr25519-bizinikiwi")
|
||||
|
||||
implementation gsonDep
|
||||
implementation substrateSdkDep
|
||||
|
||||
implementation kotlinDep
|
||||
|
||||
implementation coroutinesDep
|
||||
|
||||
implementation retrofitDep
|
||||
|
||||
implementation daggerDep
|
||||
ksp daggerCompiler
|
||||
|
||||
testImplementation project(':test-shared')
|
||||
|
||||
implementation lifeCycleKtxDep
|
||||
|
||||
androidTestImplementation androidTestRunnerDep
|
||||
androidTestImplementation androidTestRulesDep
|
||||
androidTestImplementation androidJunitDep
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,410 @@
|
||||
{
|
||||
"runtime_id": 2030,
|
||||
"types": {
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress",
|
||||
"AccountInfo": "AccountInfoWithTripleRefCount",
|
||||
"BlockNumber": "U32",
|
||||
"LeasePeriod": "BlockNumber",
|
||||
"Weight": "u64",
|
||||
"Keys": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["grandpa", "AccountId"],
|
||||
["babe", "AccountId"],
|
||||
["im_online", "AccountId"],
|
||||
["para_validator", "AccountId"],
|
||||
["para_assignment", "AccountId"],
|
||||
["authority_discovery", "AccountId"]
|
||||
]
|
||||
},
|
||||
"DispatchInfo": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["weight", "Weight"],
|
||||
["class", "DispatchClass"],
|
||||
["paysFee", "Pays"]
|
||||
]
|
||||
},
|
||||
"ProxyType": {
|
||||
"type": "enum",
|
||||
"value_list": [
|
||||
"Any",
|
||||
"NonTransfer",
|
||||
"Governance",
|
||||
"Staking",
|
||||
"IdentityJudgement",
|
||||
"CancelProxy"
|
||||
]
|
||||
},
|
||||
"RefCount": "u32",
|
||||
"ValidatorPrefs": "ValidatorPrefsWithBlocked"
|
||||
},
|
||||
"versioning": [
|
||||
{
|
||||
"runtime_range": [1019, 1031],
|
||||
"types": {
|
||||
"DispatchError": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["module", "Option<u8>"],
|
||||
["error", "u8"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1032, null],
|
||||
"types": {
|
||||
"DispatchError": {
|
||||
"type": "enum",
|
||||
"type_mapping": [
|
||||
["Other", "Null"],
|
||||
["CannotLookup", "Null"],
|
||||
["BadOrigin", "Null"],
|
||||
["Module", "DispatchErrorModule"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 1037],
|
||||
"types": {
|
||||
"IdentityInfo": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["additional", "Vec<IdentityInfoAdditional>"],
|
||||
["display", "Data"],
|
||||
["legal", "Data"],
|
||||
["web", "Data"],
|
||||
["riot", "Data"],
|
||||
["email", "Data"],
|
||||
["pgpFingerprint", "Option<H160>"],
|
||||
["image", "Data"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1038, null],
|
||||
"types": {
|
||||
"IdentityInfo": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["additional", "Vec<IdentityInfoAdditional>"],
|
||||
["display", "Data"],
|
||||
["legal", "Data"],
|
||||
["web", "Data"],
|
||||
["riot", "Data"],
|
||||
["email", "Data"],
|
||||
["pgpFingerprint", "Option<H160>"],
|
||||
["image", "Data"],
|
||||
["twitter", "Data"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 1042],
|
||||
"types": {
|
||||
"SlashingSpans": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["spanIndex", "SpanIndex"],
|
||||
["lastStart", "EraIndex"],
|
||||
["prior", "Vec<EraIndex>"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1043, null],
|
||||
"types": {
|
||||
"SlashingSpans": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["spanIndex", "SpanIndex"],
|
||||
["lastStart", "EraIndex"],
|
||||
["lastNonzeroSlash", "EraIndex"],
|
||||
["prior", "Vec<EraIndex>"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 1045],
|
||||
"types": {
|
||||
"StakingLedger": "StakingLedgerTo223",
|
||||
"BalanceLock": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["id", "LockIdentifier"],
|
||||
["amount", "Balance"],
|
||||
["until", "BlockNumber"],
|
||||
["reasons", "WithdrawReasons"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1050, 1056],
|
||||
"types": {
|
||||
"StakingLedger": "StakingLedgerTo240",
|
||||
"BalanceLock": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["id", "LockIdentifier"],
|
||||
["amount", "Balance"],
|
||||
["reasons", "Reasons"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1057, null],
|
||||
"types": {
|
||||
"StakingLedger": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
[
|
||||
"stash",
|
||||
"AccountId"
|
||||
],
|
||||
[
|
||||
"total",
|
||||
"Compact<Balance>"
|
||||
],
|
||||
[
|
||||
"active",
|
||||
"Compact<Balance>"
|
||||
],
|
||||
[
|
||||
"unlocking",
|
||||
"Vec<UnlockChunk>"
|
||||
],
|
||||
[
|
||||
"claimedRewards",
|
||||
"Vec<EraIndex>"
|
||||
]
|
||||
]
|
||||
},
|
||||
"BalanceLock": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["id", "LockIdentifier"],
|
||||
["amount", "Balance"],
|
||||
["reasons", "Reasons"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 1054],
|
||||
"types": {
|
||||
"ReferendumInfo": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["end", "BlockNumber"],
|
||||
["proposal", "Proposal"],
|
||||
["threshold", "VoteThreshold"],
|
||||
["delay", "BlockNumber"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1054, null],
|
||||
"types": {
|
||||
"ReferendumInfo": {
|
||||
"type": "enum",
|
||||
"type_mapping": [
|
||||
["Ongoing", "ReferendumStatus"],
|
||||
["Finished", "ReferendumInfoFinished"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 1056],
|
||||
"types": {
|
||||
"Weight": "u32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1057, null],
|
||||
"types": {
|
||||
"Weight": "u64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 1061],
|
||||
"types": {
|
||||
"Heartbeat": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["blockNumber", "BlockNumber"],
|
||||
["networkState", "OpaqueNetworkState"],
|
||||
["sessionIndex", "SessionIndex"],
|
||||
["authorityIndex", "AuthIndex"]
|
||||
]
|
||||
},
|
||||
"DispatchInfo": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["weight", "Weight"],
|
||||
["class", "DispatchClass"],
|
||||
["paysFee", "bool"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1062, null],
|
||||
"types": {
|
||||
"Heartbeat": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["blockNumber", "BlockNumber"],
|
||||
["networkState", "OpaqueNetworkState"],
|
||||
["sessionIndex", "SessionIndex"],
|
||||
["authorityIndex", "AuthIndex"],
|
||||
["validatorsLen", "u32"]
|
||||
]
|
||||
},
|
||||
"DispatchInfo": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["weight", "Weight"],
|
||||
["class", "DispatchClass"],
|
||||
["paysFee", "Pays"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 2012],
|
||||
"types": {
|
||||
"OpenTip": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["reason", "Hash"],
|
||||
["who", "AccountId"],
|
||||
["finder", "Option<OpenTipFinder>"],
|
||||
["closes", "Option<BlockNumber>"],
|
||||
["tips", "Vec<OpenTipTip>"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [2013, null],
|
||||
"types": {
|
||||
"OpenTip": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["reason", "Hash"],
|
||||
["who", "AccountId"],
|
||||
["finder", "AccountId"],
|
||||
["deposit", "Balance"],
|
||||
["closes", "Option<BlockNumber>"],
|
||||
["tips", "Vec<OpenTipTip>"],
|
||||
["findersFee", "bool"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 2022],
|
||||
"types": {
|
||||
"CompactAssignments": "CompactAssignmentsTo257"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [2023, null],
|
||||
"types": {
|
||||
"CompactAssignments": "CompactAssignmentsFrom258"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 2024],
|
||||
"types": {
|
||||
"RefCount": "u8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [2025, null],
|
||||
"types": {
|
||||
"RefCount": "u32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1019, 1045],
|
||||
"types": {
|
||||
"Address": "RawAddress",
|
||||
"LookupSource": "RawAddress",
|
||||
"AccountInfo": "AccountInfoWithRefCount",
|
||||
"Keys": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["grandpa", "AccountId"],
|
||||
["babe", "AccountId"],
|
||||
["im_online", "AccountId"],
|
||||
["authority_discovery", "AccountId"],
|
||||
["parachains", "AccountId"]
|
||||
]
|
||||
},
|
||||
"ValidatorPrefs": "ValidatorPrefsWithCommission"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1050, 2027],
|
||||
"types": {
|
||||
"Address": "AccountIdAddress",
|
||||
"LookupSource": "AccountIdAddress",
|
||||
"AccountInfo": "AccountInfoWithRefCount",
|
||||
"Keys": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["grandpa", "AccountId"],
|
||||
["babe", "AccountId"],
|
||||
["im_online", "AccountId"],
|
||||
["authority_discovery", "AccountId"],
|
||||
["parachains", "AccountId"]
|
||||
]
|
||||
},
|
||||
"ValidatorPrefs": "ValidatorPrefsWithCommission"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [2028, null],
|
||||
"types": {
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress",
|
||||
"Keys": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["grandpa", "AccountId"],
|
||||
["babe", "AccountId"],
|
||||
["im_online", "AccountId"],
|
||||
["para_validator", "AccountId"],
|
||||
["para_assignment", "AccountId"],
|
||||
["authority_discovery", "AccountId"]
|
||||
]
|
||||
},
|
||||
"ValidatorPrefs": "ValidatorPrefsWithBlocked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [2028, 2029],
|
||||
"types": {
|
||||
"AccountInfo": "AccountInfoWithDualRefCount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [2030, null],
|
||||
"types": {
|
||||
"AccountInfo": "AccountInfoWithTripleRefCount"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"types": {
|
||||
"ExtrinsicSignature": "MultiSignature",
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress"
|
||||
},
|
||||
"typesAlias": {
|
||||
"pezsp_runtime.multiaddress.MultiAddress": "MultiAddress",
|
||||
"pezsp_runtime.MultiSignature": "MultiSignature",
|
||||
"pezsp_runtime.generic.era.Era": "Era"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"runtime_id": 30,
|
||||
"types": {
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress",
|
||||
"AccountInfo": "AccountInfoWithTripleRefCount",
|
||||
"BlockNumber": "U32",
|
||||
"LeasePeriod": "BlockNumber",
|
||||
"Weight": "u64",
|
||||
"Keys": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["grandpa", "AccountId"],
|
||||
["babe", "AccountId"],
|
||||
["im_online", "AccountId"],
|
||||
["para_validator", "AccountId"],
|
||||
["para_assignment", "AccountId"],
|
||||
["authority_discovery", "AccountId"]
|
||||
]
|
||||
},
|
||||
"ValidatorPrefs": "ValidatorPrefsWithBlocked",
|
||||
"DispatchInfo": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["weight", "Weight"],
|
||||
["class", "DispatchClass"],
|
||||
["paysFee", "Pays"]
|
||||
]
|
||||
},
|
||||
"ProxyType": {
|
||||
"type": "enum",
|
||||
"value_list": [
|
||||
"Any",
|
||||
"NonTransfer",
|
||||
"Governance",
|
||||
"Staking",
|
||||
"DeprecatedSudoBalances",
|
||||
"IdentityJudgement",
|
||||
"CancelProxy"
|
||||
]
|
||||
},
|
||||
"RefCount": "u32"
|
||||
},
|
||||
"versioning": [
|
||||
{
|
||||
"runtime_range": [0, 12],
|
||||
"types": {
|
||||
"OpenTip": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["reason", "Hash"],
|
||||
["who", "AccountId"],
|
||||
["finder", "Option<OpenTipFinder>"],
|
||||
["closes", "Option<BlockNumber>"],
|
||||
["tips", "Vec<OpenTipTip>"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [13, null],
|
||||
"types": {
|
||||
"OpenTip": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["reason", "Hash"],
|
||||
["who", "AccountId"],
|
||||
["finder", "AccountId"],
|
||||
["deposit", "Balance"],
|
||||
["closes", "Option<BlockNumber>"],
|
||||
["tips", "Vec<OpenTipTip>"],
|
||||
["findersFee", "bool"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [0, 22],
|
||||
"types": {
|
||||
"CompactAssignments": "CompactAssignmentsTo257"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [23, null],
|
||||
"types": {
|
||||
"CompactAssignments": "CompactAssignmentsFrom258"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [0, 24],
|
||||
"types": {
|
||||
"RefCount": "u8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [25, null],
|
||||
"types": {
|
||||
"RefCount": "u32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [0, 27],
|
||||
"types": {
|
||||
"Address": "AccountIdAddress",
|
||||
"LookupSource": "AccountIdAddress",
|
||||
"AccountInfo": "AccountInfoWithRefCount",
|
||||
"Keys": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["grandpa", "AccountId"],
|
||||
["babe", "AccountId"],
|
||||
["im_online", "AccountId"],
|
||||
["authority_discovery", "AccountId"],
|
||||
["parachains", "AccountId"]
|
||||
]
|
||||
},
|
||||
"ValidatorPrefs": "ValidatorPrefsWithCommission"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [28, null],
|
||||
"types": {
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress",
|
||||
"AccountInfo": "AccountInfoWithDualRefCount",
|
||||
"Keys": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["grandpa", "AccountId"],
|
||||
["babe", "AccountId"],
|
||||
["im_online", "AccountId"],
|
||||
["para_validator", "AccountId"],
|
||||
["para_assignment", "AccountId"],
|
||||
["authority_discovery", "AccountId"]
|
||||
]
|
||||
},
|
||||
"ValidatorPrefs": "ValidatorPrefsWithBlocked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [28, 29],
|
||||
"types": {
|
||||
"AccountInfo": "AccountInfoWithDualRefCount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [30, null],
|
||||
"types": {
|
||||
"AccountInfo": "AccountInfoWithTripleRefCount"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"runtime_id": 9001,
|
||||
"types": {
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress",
|
||||
"AccountInfo": "AccountInfoWithTripleRefCount",
|
||||
"FullIdentification": "()",
|
||||
"Keys": "SessionKeys7B",
|
||||
"CompactAssignments": "CompactAssignmentsFrom258"
|
||||
},
|
||||
"versioning": [
|
||||
{
|
||||
"runtime_range": [0, 200],
|
||||
"types": {
|
||||
"Address": "AccountIdAddress",
|
||||
"LookupSource": "AccountIdAddress",
|
||||
"AccountInfo": "AccountInfoWithDualRefCount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [201, null],
|
||||
"types": {
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress",
|
||||
"AccountInfo": "AccountInfoWithTripleRefCount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [0, 228],
|
||||
"types": {
|
||||
"Keys": "SessionKeys6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [229, null],
|
||||
"types": {
|
||||
"Keys": "SessionKeys7B"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [0, 9009],
|
||||
"types": {
|
||||
"CompactAssignments": "CompactAssignmentsFrom258"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [9010, null],
|
||||
"types": {
|
||||
"CompactAssignments": "CompactAssignmentsFrom265"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"runtime_id": 900,
|
||||
"types": {
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress",
|
||||
"AccountInfo": "AccountInfoWithTripleRefCount",
|
||||
"BlockNumber": "U32",
|
||||
"LeasePeriod": "BlockNumber",
|
||||
"Keys": "SessionKeys6",
|
||||
"ValidatorPrefs": "ValidatorPrefsWithBlocked",
|
||||
"ProxyType": {
|
||||
"type": "enum",
|
||||
"value_list": [
|
||||
"Any",
|
||||
"NonTransfer",
|
||||
"Staking",
|
||||
"SudoBalances",
|
||||
"IdentityJudgement",
|
||||
"CancelProxy"
|
||||
]
|
||||
},
|
||||
"RefCount": "u32"
|
||||
},
|
||||
"versioning": [
|
||||
{
|
||||
"runtime_range": [3, 22],
|
||||
"types": {
|
||||
"OpenTip": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["reason", "Hash"],
|
||||
["who", "AccountId"],
|
||||
["finder", "Option<OpenTipFinder>"],
|
||||
["closes", "Option<BlockNumber>"],
|
||||
["tips", "Vec<OpenTipTip>"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [23, null],
|
||||
"types": {
|
||||
"OpenTip": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["reason", "Hash"],
|
||||
["who", "AccountId"],
|
||||
["finder", "AccountId"],
|
||||
["deposit", "Balance"],
|
||||
["closes", "Option<BlockNumber>"],
|
||||
["tips", "Vec<OpenTipTip>"],
|
||||
["findersFee", "bool"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [3, 44],
|
||||
"types": {
|
||||
"RefCount": "u8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [45, null],
|
||||
"types": {
|
||||
"RefCount": "u32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1, 42],
|
||||
"types": {
|
||||
"CompactAssignments": "CompactAssignmentsTo257"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [43, null],
|
||||
"types": {
|
||||
"CompactAssignments": "CompactAssignmentsFrom258"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1, 44],
|
||||
"types": {
|
||||
"Heartbeat": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["blockNumber", "BlockNumber"],
|
||||
["networkState", "OpaqueNetworkState"],
|
||||
["sessionIndex", "SessionIndex"],
|
||||
["authorityIndex", "AuthIndex"]
|
||||
]
|
||||
},
|
||||
"DispatchInfo": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["weight", "Weight"],
|
||||
["class", "DispatchClass"],
|
||||
["paysFee", "bool"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [45, null],
|
||||
"types": {
|
||||
"Heartbeat": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["blockNumber", "BlockNumber"],
|
||||
["networkState", "OpaqueNetworkState"],
|
||||
["sessionIndex", "SessionIndex"],
|
||||
["authorityIndex", "AuthIndex"],
|
||||
["validatorsLen", "u32"]
|
||||
]
|
||||
},
|
||||
"DispatchInfo": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["weight", "Weight"],
|
||||
["class", "DispatchClass"],
|
||||
["paysFee", "Pays"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [1, 44],
|
||||
"types": {
|
||||
"Weight": "u32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [45, null],
|
||||
"types": {
|
||||
"Weight": "u64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [0, 47],
|
||||
"types": {
|
||||
"Address": "AccountIdAddress",
|
||||
"LookupSource": "AccountIdAddress",
|
||||
"AccountInfo": "AccountInfoWithRefCount",
|
||||
"Keys": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["grandpa", "AccountId"],
|
||||
["babe", "AccountId"],
|
||||
["im_online", "AccountId"],
|
||||
["authority_discovery", "AccountId"],
|
||||
["parachains", "AccountId"]
|
||||
]
|
||||
},
|
||||
"ValidatorPrefs": "ValidatorPrefsWithCommission"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [48, null],
|
||||
"types": {
|
||||
"Address": "MultiAddress",
|
||||
"LookupSource": "MultiAddress",
|
||||
"ValidatorPrefs": "ValidatorPrefsWithBlocked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [48, 49],
|
||||
"types": {
|
||||
"AccountInfo": "AccountInfoWithDualRefCount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [50, null],
|
||||
"types": {
|
||||
"AccountInfo": "AccountInfoWithTripleRefCount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runtime_range": [48, null],
|
||||
"types": {
|
||||
"Keys": "SessionKeys6"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.runtime.call
|
||||
|
||||
import io.novafoundation.nova.common.utils.hasDetectedRuntimeApi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getSocket
|
||||
|
||||
interface MultiChainRuntimeCallsApi {
|
||||
|
||||
suspend fun forChain(chainId: ChainId): RuntimeCallsApi
|
||||
|
||||
suspend fun isSupported(chainId: ChainId, section: String, method: String): Boolean
|
||||
}
|
||||
|
||||
internal class RealMultiChainRuntimeCallsApi(
|
||||
private val chainRegistry: ChainRegistry
|
||||
) : MultiChainRuntimeCallsApi {
|
||||
|
||||
override suspend fun forChain(chainId: ChainId): RuntimeCallsApi {
|
||||
val runtime = chainRegistry.getRuntime(chainId)
|
||||
val socket = chainRegistry.getSocket(chainId)
|
||||
|
||||
return RealRuntimeCallsApi(runtime, chainId, socket)
|
||||
}
|
||||
|
||||
override suspend fun isSupported(chainId: ChainId, section: String, method: String): Boolean {
|
||||
val runtime = chainRegistry.getRuntime(chainId)
|
||||
// Avoid extra allocations of RealRuntimeCallsApi and socket retrieval - check directly from the metadata
|
||||
return runtime.metadata.hasDetectedRuntimeApi(section, method)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package io.novafoundation.nova.runtime.call
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.fromHexOrIncompatible
|
||||
import io.novafoundation.nova.common.utils.hasDetectedRuntimeApi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.network.rpc.stateCall
|
||||
import io.novasama.substrate_sdk_android.extensions.requireHexPrefix
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.registry.TypeRegistry
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.registry.getOrThrow
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.bytes
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.createRequest
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.decodeOutput
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.method
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.runtimeApi
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.state.StateCallRequest
|
||||
|
||||
typealias RuntimeTypeName = String
|
||||
typealias RuntimeTypeValue = Any?
|
||||
|
||||
interface RuntimeCallsApi {
|
||||
|
||||
val chainId: ChainId
|
||||
|
||||
val runtime: RuntimeSnapshot
|
||||
|
||||
/**
|
||||
* @param arguments - list of pairs [runtimeTypeValue, runtimeTypeName],
|
||||
* where runtimeTypeValue is value to be encoded and runtimeTypeName is type name that can be found in [TypeRegistry]
|
||||
* It can also be null, in that case argument is considered as already encoded in hex form
|
||||
*
|
||||
* This should only be used if automatic decoding via metadata is not possible
|
||||
* For the other cases use another [call] overload
|
||||
*/
|
||||
suspend fun <R> call(
|
||||
section: String,
|
||||
method: String,
|
||||
arguments: List<Pair<RuntimeTypeValue, RuntimeTypeName?>>,
|
||||
returnType: RuntimeTypeName,
|
||||
returnBinding: (Any?) -> R
|
||||
): R
|
||||
|
||||
suspend fun <R> call(
|
||||
section: String,
|
||||
method: String,
|
||||
arguments: Map<String, Any?>,
|
||||
returnBinding: (Any?) -> R
|
||||
): R
|
||||
|
||||
fun isSupported(
|
||||
section: String,
|
||||
method: String
|
||||
): Boolean
|
||||
}
|
||||
|
||||
suspend fun <R> RuntimeCallsApi.callCatching(
|
||||
section: String,
|
||||
method: String,
|
||||
arguments: Map<String, Any?>,
|
||||
returnBinding: (Any?) -> R
|
||||
): Result<R> {
|
||||
return runCatching { call(section, method, arguments, returnBinding) }
|
||||
}
|
||||
|
||||
internal class RealRuntimeCallsApi(
|
||||
override val runtime: RuntimeSnapshot,
|
||||
override val chainId: ChainId,
|
||||
private val socketService: SocketService,
|
||||
) : RuntimeCallsApi {
|
||||
|
||||
override suspend fun <R> call(
|
||||
section: String,
|
||||
method: String,
|
||||
arguments: List<Pair<RuntimeTypeValue, RuntimeTypeName?>>,
|
||||
returnType: String,
|
||||
returnBinding: (Any?) -> R
|
||||
): R {
|
||||
val runtimeApiName = createRuntimeApiName(section, method)
|
||||
val data = encodeArguments(arguments)
|
||||
|
||||
val request = StateCallRequest(runtimeApiName, data)
|
||||
val response = socketService.stateCall(request)
|
||||
|
||||
val decoded = decodeResponse(response, returnType)
|
||||
|
||||
return returnBinding(decoded)
|
||||
}
|
||||
|
||||
override suspend fun <R> call(
|
||||
section: String,
|
||||
method: String,
|
||||
arguments: Map<String, Any?>,
|
||||
returnBinding: (Any?) -> R
|
||||
): R {
|
||||
val apiMethod = runtime.metadata.runtimeApi(section).method(method)
|
||||
val request = apiMethod.createRequest(runtime, arguments)
|
||||
|
||||
val response = socketService.stateCall(request)
|
||||
|
||||
val decoded = response?.let { apiMethod.decodeOutput(runtime, it) }
|
||||
|
||||
return returnBinding(decoded)
|
||||
}
|
||||
|
||||
override fun isSupported(section: String, method: String): Boolean {
|
||||
return runtime.metadata.hasDetectedRuntimeApi(section, method)
|
||||
}
|
||||
|
||||
private fun decodeResponse(responseHex: String?, returnTypeName: String): Any? {
|
||||
val returnType = runtime.typeRegistry.getOrThrow(returnTypeName)
|
||||
|
||||
return responseHex?.let {
|
||||
returnType.fromHexOrIncompatible(it, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeArguments(arguments: List<Pair<RuntimeTypeValue, RuntimeTypeName?>>): String {
|
||||
return buildString {
|
||||
arguments.forEach { (typeValue, runtimeTypeName) ->
|
||||
val argument = if (runtimeTypeName != null) {
|
||||
val type = runtime.typeRegistry.getOrThrow(runtimeTypeName)
|
||||
val encodedArgument = type.bytes(runtime, typeValue)
|
||||
|
||||
encodedArgument.toHexString(withPrefix = false)
|
||||
} else {
|
||||
typeValue.toString()
|
||||
}
|
||||
|
||||
append(argument)
|
||||
}
|
||||
}.requireHexPrefix()
|
||||
}
|
||||
|
||||
private fun createRuntimeApiName(section: String, method: String): String {
|
||||
return "${section}_$method"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package io.novafoundation.nova.runtime.di
|
||||
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.BuildConfig
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.data.network.rpc.BulkRetriever
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.common.interfaces.FileProvider
|
||||
import io.novafoundation.nova.core_db.dao.ChainAssetDao
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset.EvmAssetsSyncService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset.remote.AssetFetcher
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.ChainSyncService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.ChainFetcher
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnectionFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionPool
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.Web3ApiPool
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.NodeAutobalancer
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSelectionStrategyProvider
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.healthState.NodeHealthStateTesterFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.AsyncChainSyncDispatcher
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeCacheMigrator
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeFilesCache
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeMetadataFetcher
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProviderPool
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeSubscriptionPool
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeSyncService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.BaseTypeSynchronizer
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.TypesFetcher
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.web3j.protocol.http.HttpService
|
||||
import javax.inject.Provider
|
||||
|
||||
@Module
|
||||
class ChainRegistryModule {
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideChainFetcher(apiCreator: NetworkApiCreator) = apiCreator.create(ChainFetcher::class.java)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideChainSyncService(
|
||||
dao: ChainDao,
|
||||
chainAssetDao: ChainAssetDao,
|
||||
chainFetcher: ChainFetcher,
|
||||
gson: Gson
|
||||
) = ChainSyncService(dao, chainFetcher, gson)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideAssetFetcher(apiCreator: NetworkApiCreator) = apiCreator.create(AssetFetcher::class.java)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideAssetSyncService(
|
||||
chainAssetDao: ChainAssetDao,
|
||||
chainDao: ChainDao,
|
||||
assetFetcher: AssetFetcher,
|
||||
gson: Gson
|
||||
) = EvmAssetsSyncService(chainDao, chainAssetDao, assetFetcher, gson)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideRuntimeFactory(
|
||||
runtimeFilesCache: RuntimeFilesCache,
|
||||
chainDao: ChainDao,
|
||||
gson: Gson,
|
||||
): RuntimeFactory {
|
||||
return RuntimeFactory(runtimeFilesCache, chainDao, gson)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideRuntimeMetadataFetcher(): RuntimeMetadataFetcher {
|
||||
return RuntimeMetadataFetcher()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideRuntimeCacheMigrator(): RuntimeCacheMigrator {
|
||||
return RuntimeCacheMigrator()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideRuntimeFilesCache(
|
||||
fileProvider: FileProvider,
|
||||
preferences: Preferences
|
||||
) = RuntimeFilesCache(fileProvider, preferences)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideTypesFetcher(
|
||||
networkApiCreator: NetworkApiCreator,
|
||||
) = networkApiCreator.create(TypesFetcher::class.java)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideRuntimeSyncService(
|
||||
typesFetcher: TypesFetcher,
|
||||
runtimeFilesCache: RuntimeFilesCache,
|
||||
chainDao: ChainDao,
|
||||
runtimeCacheMigrator: RuntimeCacheMigrator,
|
||||
runtimeMetadataFetcher: RuntimeMetadataFetcher
|
||||
) = RuntimeSyncService(
|
||||
typesFetcher = typesFetcher,
|
||||
runtimeFilesCache = runtimeFilesCache,
|
||||
chainDao = chainDao,
|
||||
runtimeMetadataFetcher = runtimeMetadataFetcher,
|
||||
cacheMigrator = runtimeCacheMigrator,
|
||||
chainSyncDispatcher = AsyncChainSyncDispatcher()
|
||||
)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideBaseTypeSynchronizer(
|
||||
typesFetcher: TypesFetcher,
|
||||
runtimeFilesCache: RuntimeFilesCache,
|
||||
) = BaseTypeSynchronizer(runtimeFilesCache, typesFetcher)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideRuntimeProviderPool(
|
||||
runtimeFactory: RuntimeFactory,
|
||||
runtimeSyncService: RuntimeSyncService,
|
||||
runtimeFilesCache: RuntimeFilesCache,
|
||||
baseTypeSynchronizer: BaseTypeSynchronizer,
|
||||
) = RuntimeProviderPool(runtimeFactory, runtimeSyncService, runtimeFilesCache, baseTypeSynchronizer)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideAutoBalanceProvider(
|
||||
connectionSecrets: ConnectionSecrets
|
||||
) = NodeSelectionStrategyProvider(connectionSecrets)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideNodeAutoBalancer(
|
||||
nodeSelectionStrategyProvider: NodeSelectionStrategyProvider,
|
||||
) = NodeAutobalancer(nodeSelectionStrategyProvider)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideConnectionSecrets(): ConnectionSecrets = ConnectionSecrets.default()
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideNodeConnectionFactory(
|
||||
socketProvider: Provider<SocketService>,
|
||||
bulkRetriever: BulkRetriever,
|
||||
connectionSecrets: ConnectionSecrets,
|
||||
web3ApiFactory: Web3ApiFactory
|
||||
) = NodeHealthStateTesterFactory(
|
||||
socketProvider,
|
||||
connectionSecrets,
|
||||
bulkRetriever,
|
||||
web3ApiFactory
|
||||
)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideChainConnectionFactory(
|
||||
socketProvider: Provider<SocketService>,
|
||||
externalRequirementsFlow: MutableStateFlow<ChainConnection.ExternalRequirement>,
|
||||
nodeAutobalancer: NodeAutobalancer,
|
||||
) = ChainConnectionFactory(
|
||||
externalRequirementsFlow,
|
||||
nodeAutobalancer,
|
||||
socketProvider,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideConnectionPool(chainConnectionFactory: ChainConnectionFactory) = ConnectionPool(chainConnectionFactory)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideRuntimeVersionSubscriptionPool(
|
||||
chainDao: ChainDao,
|
||||
runtimeSyncService: RuntimeSyncService,
|
||||
) = RuntimeSubscriptionPool(chainDao, runtimeSyncService)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideWeb3ApiFactory(
|
||||
strategyProvider: NodeSelectionStrategyProvider,
|
||||
): Web3ApiFactory {
|
||||
val builder = HttpService.getOkHttpClientBuilder()
|
||||
builder.interceptors().clear() // getOkHttpClientBuilder() adds logging interceptor which doesn't log into LogCat
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
builder.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
|
||||
}
|
||||
|
||||
val okHttpClient = builder.build()
|
||||
|
||||
return Web3ApiFactory(strategyProvider = strategyProvider, httpClient = okHttpClient)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideWeb3ApiPool(web3ApiFactory: Web3ApiFactory) = Web3ApiPool(web3ApiFactory)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideExternalRequirementsFlow() = MutableStateFlow(ChainConnection.ExternalRequirement.ALLOWED)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideChainRegistry(
|
||||
runtimeProviderPool: RuntimeProviderPool,
|
||||
chainConnectionPool: ConnectionPool,
|
||||
runtimeSubscriptionPool: RuntimeSubscriptionPool,
|
||||
chainDao: ChainDao,
|
||||
chainSyncService: ChainSyncService,
|
||||
evmAssetsSyncService: EvmAssetsSyncService,
|
||||
baseTypeSynchronizer: BaseTypeSynchronizer,
|
||||
runtimeSyncService: RuntimeSyncService,
|
||||
web3ApiPool: Web3ApiPool,
|
||||
gson: Gson
|
||||
) = ChainRegistry(
|
||||
runtimeProviderPool,
|
||||
chainConnectionPool,
|
||||
runtimeSubscriptionPool,
|
||||
chainDao,
|
||||
chainSyncService,
|
||||
evmAssetsSyncService,
|
||||
baseTypeSynchronizer,
|
||||
runtimeSyncService,
|
||||
web3ApiPool,
|
||||
gson
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package io.novafoundation.nova.runtime.di
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.core.storage.StorageCache
|
||||
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
|
||||
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory
|
||||
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory
|
||||
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase
|
||||
import io.novafoundation.nova.runtime.extrinsic.MortalityConstructor
|
||||
import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallTraversal
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicWalk
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.ChainSyncService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.RemoteToDomainChainMapperFacade
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.healthState.NodeHealthStateTesterFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.qr.MultiChainQrSharingFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeFilesCache
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProviderPool
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.RuntimeVersionsRepository
|
||||
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
|
||||
import io.novafoundation.nova.runtime.repository.BlockLimitsRepository
|
||||
import io.novafoundation.nova.runtime.repository.ChainNodeRepository
|
||||
import io.novafoundation.nova.runtime.repository.ChainRepository
|
||||
import io.novafoundation.nova.runtime.repository.ChainStateRepository
|
||||
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
|
||||
import io.novafoundation.nova.runtime.repository.PreConfiguredChainsRepository
|
||||
import io.novafoundation.nova.runtime.repository.TimestampRepository
|
||||
import io.novafoundation.nova.runtime.repository.TotalIssuanceRepository
|
||||
import io.novafoundation.nova.runtime.storage.SampledBlockTimeStorage
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import javax.inject.Named
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class ExtrinsicSerialization
|
||||
|
||||
interface RuntimeApi {
|
||||
|
||||
fun provideExtrinsicBuilderFactory(): ExtrinsicBuilderFactory
|
||||
|
||||
fun externalRequirementFlow(): MutableStateFlow<ChainConnection.ExternalRequirement>
|
||||
|
||||
fun storageCache(): StorageCache
|
||||
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
fun remoteStorageSource(): StorageDataSource
|
||||
|
||||
@Named(LOCAL_STORAGE_SOURCE)
|
||||
fun localStorageSource(): StorageDataSource
|
||||
|
||||
fun chainSyncService(): ChainSyncService
|
||||
|
||||
fun chainStateRepository(): ChainStateRepository
|
||||
|
||||
fun chainRegistry(): ChainRegistry
|
||||
|
||||
fun rpcCalls(): RpcCalls
|
||||
|
||||
@ExtrinsicSerialization
|
||||
fun extrinsicGson(): Gson
|
||||
|
||||
fun runtimeVersionsRepository(): RuntimeVersionsRepository
|
||||
|
||||
fun eventsRepository(): EventsRepository
|
||||
|
||||
val multiChainQrSharingFactory: MultiChainQrSharingFactory
|
||||
|
||||
val sampledBlockTime: SampledBlockTimeStorage
|
||||
|
||||
val parachainInfoRepository: ParachainInfoRepository
|
||||
|
||||
val mortalityConstructor: MortalityConstructor
|
||||
|
||||
val extrinsicValidityUseCase: ExtrinsicValidityUseCase
|
||||
|
||||
val timestampRepository: TimestampRepository
|
||||
|
||||
val totalIssuanceRepository: TotalIssuanceRepository
|
||||
|
||||
val storageStorageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory
|
||||
|
||||
val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi
|
||||
|
||||
val gasPriceProviderFactory: GasPriceProviderFactory
|
||||
|
||||
val extrinsicWalk: ExtrinsicWalk
|
||||
|
||||
val callTraversal: CallTraversal
|
||||
|
||||
val runtimeFilesCache: RuntimeFilesCache
|
||||
|
||||
val metadataShortenerService: MetadataShortenerService
|
||||
|
||||
val runtimeProviderPool: RuntimeProviderPool
|
||||
|
||||
val nodeHealthStateTesterFactory: NodeHealthStateTesterFactory
|
||||
|
||||
val chainNodeRepository: ChainNodeRepository
|
||||
|
||||
val nodeConnectionFactory: NodeConnectionFactory
|
||||
|
||||
val web3ApiFactory: Web3ApiFactory
|
||||
|
||||
val preConfiguredChainsRepository: PreConfiguredChainsRepository
|
||||
|
||||
val chainRepository: ChainRepository
|
||||
|
||||
val remoteToDomainChainMapperFacade: RemoteToDomainChainMapperFacade
|
||||
|
||||
val blockLimitsRepository: BlockLimitsRepository
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.novafoundation.nova.runtime.di
|
||||
|
||||
import dagger.Component
|
||||
import io.novafoundation.nova.common.di.CommonApi
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
|
||||
@Component(
|
||||
modules = [
|
||||
RuntimeModule::class,
|
||||
ChainRegistryModule::class
|
||||
],
|
||||
dependencies = [
|
||||
RuntimeDependencies::class
|
||||
]
|
||||
)
|
||||
@ApplicationScope
|
||||
abstract class RuntimeComponent : RuntimeApi {
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
DbApi::class,
|
||||
]
|
||||
)
|
||||
interface RuntimeDependenciesComponent : RuntimeDependencies
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.runtime.di
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.common.interfaces.FileProvider
|
||||
import io.novafoundation.nova.core_db.dao.ChainAssetDao
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
import io.novafoundation.nova.core_db.dao.StorageDao
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
|
||||
interface RuntimeDependencies {
|
||||
|
||||
fun networkApiCreator(): NetworkApiCreator
|
||||
|
||||
fun socketServiceCreator(): SocketService
|
||||
|
||||
fun gson(): Gson
|
||||
|
||||
fun preferences(): Preferences
|
||||
|
||||
fun fileProvider(): FileProvider
|
||||
|
||||
fun context(): Context
|
||||
|
||||
fun storageDao(): StorageDao
|
||||
|
||||
fun chainDao(): ChainDao
|
||||
|
||||
fun chainAssetDao(): ChainAssetDao
|
||||
|
||||
fun assetsIconModeService(): AssetsIconModeRepository
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.runtime.di
|
||||
|
||||
import io.novafoundation.nova.common.di.FeatureApiHolder
|
||||
import io.novafoundation.nova.common.di.FeatureContainer
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class RuntimeHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val dbDependencies = DaggerRuntimeComponent_RuntimeDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.dbApi(getFeature(DbApi::class.java))
|
||||
.build()
|
||||
return DaggerRuntimeComponent.builder()
|
||||
.runtimeDependencies(dbDependencies)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package io.novafoundation.nova.runtime.di
|
||||
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.rpc.BulkRetriever
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.core.storage.StorageCache
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
import io.novafoundation.nova.core_db.dao.StorageDao
|
||||
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
|
||||
import io.novafoundation.nova.runtime.call.RealMultiChainRuntimeCallsApi
|
||||
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.gas.RealGasPriceProviderFactory
|
||||
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory
|
||||
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicSerializers
|
||||
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase
|
||||
import io.novafoundation.nova.runtime.extrinsic.MortalityConstructor
|
||||
import io.novafoundation.nova.runtime.extrinsic.RealExtrinsicValidityUseCase
|
||||
import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService
|
||||
import io.novafoundation.nova.runtime.extrinsic.metadata.RealMetadataShortenerService
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallTraversal
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.RealCallTraversal
|
||||
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.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.RemoteToDomainChainMapperFacade
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.ChainFetcher
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.qr.MultiChainQrSharingFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.DbRuntimeVersionsRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.RemoteEventsRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.RuntimeVersionsRepository
|
||||
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
|
||||
import io.novafoundation.nova.runtime.repository.BlockLimitsRepository
|
||||
import io.novafoundation.nova.runtime.repository.ChainNodeRepository
|
||||
import io.novafoundation.nova.runtime.repository.ChainRepository
|
||||
import io.novafoundation.nova.runtime.repository.ChainStateRepository
|
||||
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
|
||||
import io.novafoundation.nova.runtime.repository.PreConfiguredChainsRepository
|
||||
import io.novafoundation.nova.runtime.repository.RealBlockLimitsRepository
|
||||
import io.novafoundation.nova.runtime.repository.RealChainNodeRepository
|
||||
import io.novafoundation.nova.runtime.repository.RealChainRepository
|
||||
import io.novafoundation.nova.runtime.repository.RealParachainInfoRepository
|
||||
import io.novafoundation.nova.runtime.repository.RealPreConfiguredChainsRepository
|
||||
import io.novafoundation.nova.runtime.repository.RealTotalIssuanceRepository
|
||||
import io.novafoundation.nova.runtime.repository.RemoteTimestampRepository
|
||||
import io.novafoundation.nova.runtime.repository.TimestampRepository
|
||||
import io.novafoundation.nova.runtime.repository.TotalIssuanceRepository
|
||||
import io.novafoundation.nova.runtime.storage.DbStorageCache
|
||||
import io.novafoundation.nova.runtime.storage.PrefsSampledBlockTimeStorage
|
||||
import io.novafoundation.nova.runtime.storage.SampledBlockTimeStorage
|
||||
import io.novafoundation.nova.runtime.storage.source.LocalStorageSource
|
||||
import io.novafoundation.nova.runtime.storage.source.RemoteStorageSource
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import javax.inject.Named
|
||||
import javax.inject.Provider
|
||||
|
||||
const val LOCAL_STORAGE_SOURCE = "LOCAL_STORAGE_SOURCE"
|
||||
const val REMOTE_STORAGE_SOURCE = "REMOTE_STORAGE_SOURCE"
|
||||
|
||||
const val BULK_RETRIEVER_PAGE_SIZE = 1000
|
||||
|
||||
@Module
|
||||
class RuntimeModule {
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideExtrinsicBuilderFactory(
|
||||
chainRegistry: ChainRegistry,
|
||||
mortalityConstructor: MortalityConstructor,
|
||||
metadataShortenerService: MetadataShortenerService
|
||||
) = ExtrinsicBuilderFactory(
|
||||
chainRegistry,
|
||||
mortalityConstructor,
|
||||
metadataShortenerService
|
||||
)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideStorageCache(
|
||||
storageDao: StorageDao,
|
||||
): StorageCache = DbStorageCache(storageDao)
|
||||
|
||||
@Provides
|
||||
@Named(LOCAL_STORAGE_SOURCE)
|
||||
@ApplicationScope
|
||||
fun provideLocalStorageSource(
|
||||
chainRegistry: ChainRegistry,
|
||||
storageCache: StorageCache,
|
||||
sharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
|
||||
): StorageDataSource = LocalStorageSource(chainRegistry, sharedRequestsBuilderFactory, storageCache)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideBulkRetriever(): BulkRetriever {
|
||||
return BulkRetriever(BULK_RETRIEVER_PAGE_SIZE)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
@ApplicationScope
|
||||
fun provideRemoteStorageSource(
|
||||
chainRegistry: ChainRegistry,
|
||||
sharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
|
||||
bulkRetriever: BulkRetriever,
|
||||
): StorageDataSource = RemoteStorageSource(chainRegistry, sharedRequestsBuilderFactory, bulkRetriever)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideSampledBlockTimeStorage(
|
||||
gson: Gson,
|
||||
preferences: Preferences,
|
||||
): SampledBlockTimeStorage = PrefsSampledBlockTimeStorage(gson, preferences)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideChainStateRepository(
|
||||
@Named(LOCAL_STORAGE_SOURCE) localStorageSource: StorageDataSource,
|
||||
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource,
|
||||
sampledBlockTimeStorage: SampledBlockTimeStorage,
|
||||
chainRegistry: ChainRegistry
|
||||
) = ChainStateRepository(localStorageSource, remoteStorageSource, sampledBlockTimeStorage, chainRegistry)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideMortalityProvider(
|
||||
chainStateRepository: ChainStateRepository,
|
||||
rpcCalls: RpcCalls,
|
||||
) = MortalityConstructor(rpcCalls, chainStateRepository)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideSubstrateCalls(
|
||||
chainRegistry: ChainRegistry,
|
||||
multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi
|
||||
) = RpcCalls(chainRegistry, multiChainRuntimeCallsApi)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
@ExtrinsicSerialization
|
||||
fun provideExtrinsicGson() = ExtrinsicSerializers.gson()
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideRuntimeVersionsRepository(
|
||||
chainDao: ChainDao
|
||||
): RuntimeVersionsRepository = DbRuntimeVersionsRepository(chainDao)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideEventsRepository(
|
||||
rpcCalls: RpcCalls,
|
||||
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource
|
||||
): EventsRepository = RemoteEventsRepository(rpcCalls, remoteStorageSource)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideMultiChainQrSharingFactory() = MultiChainQrSharingFactory()
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideParachainInfoRepository(
|
||||
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource
|
||||
): ParachainInfoRepository = RealParachainInfoRepository(remoteStorageSource)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideExtrinsicValidityUseCase(
|
||||
mortalityConstructor: MortalityConstructor
|
||||
): ExtrinsicValidityUseCase = RealExtrinsicValidityUseCase(mortalityConstructor)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideTimestampRepository(
|
||||
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource,
|
||||
): TimestampRepository = RemoteTimestampRepository(remoteStorageSource)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideTotalIssuanceRepository(
|
||||
@Named(LOCAL_STORAGE_SOURCE) localStorageSource: StorageDataSource,
|
||||
): TotalIssuanceRepository = RealTotalIssuanceRepository(localStorageSource)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideBlockLimitsRepository(
|
||||
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource,
|
||||
chainRegistry: ChainRegistry
|
||||
): BlockLimitsRepository = RealBlockLimitsRepository(remoteStorageSource, chainRegistry)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideStorageSharedRequestBuilderFactory(chainRegistry: ChainRegistry) = StorageSharedRequestsBuilderFactory(chainRegistry)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideMultiChainRuntimeCallsApi(chainRegistry: ChainRegistry): MultiChainRuntimeCallsApi = RealMultiChainRuntimeCallsApi(chainRegistry)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideGasPriceProviderFactory(
|
||||
chainRegistry: ChainRegistry
|
||||
): GasPriceProviderFactory = RealGasPriceProviderFactory(chainRegistry)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideExtrinsicWalk(
|
||||
chainRegistry: ChainRegistry,
|
||||
): ExtrinsicWalk = RealExtrinsicWalk(chainRegistry)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideCallTraversal(): CallTraversal = RealCallTraversal()
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideMetadataShortenerService(
|
||||
chainRegistry: ChainRegistry,
|
||||
rpcCalls: RpcCalls,
|
||||
): MetadataShortenerService {
|
||||
return RealMetadataShortenerService(chainRegistry, rpcCalls)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideChainNodeRepository(
|
||||
chainDao: ChainDao,
|
||||
): ChainNodeRepository = RealChainNodeRepository(chainDao)
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideNodeConnectionFactory(
|
||||
socketServiceProvider: Provider<SocketService>,
|
||||
connectionSecrets: ConnectionSecrets
|
||||
): NodeConnectionFactory {
|
||||
return NodeConnectionFactory(
|
||||
socketServiceProvider,
|
||||
connectionSecrets
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideRemoteToDomainChainMapperFacade(
|
||||
gson: Gson
|
||||
): RemoteToDomainChainMapperFacade {
|
||||
return RemoteToDomainChainMapperFacade(
|
||||
gson
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun providePreConfiguredChainsRepository(
|
||||
chainFetcher: ChainFetcher,
|
||||
chainMapperFacade: RemoteToDomainChainMapperFacade
|
||||
): PreConfiguredChainsRepository {
|
||||
return RealPreConfiguredChainsRepository(
|
||||
chainFetcher,
|
||||
chainMapperFacade
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideChainRepository(
|
||||
chainRegistry: ChainRegistry,
|
||||
chainDao: ChainDao,
|
||||
gson: Gson
|
||||
): ChainRepository {
|
||||
return RealChainRepository(
|
||||
chainRegistry,
|
||||
chainDao,
|
||||
gson
|
||||
)
|
||||
}
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
package io.novafoundation.nova.runtime.ethereum
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import io.novafoundation.nova.common.utils.requireException
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.NodeWithSaturatedUrl
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.UpdatableNodes
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSelectionStrategyProvider
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSequenceGenerator
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.generateNodeIterator
|
||||
import io.novasama.substrate_sdk_android.extensions.tryFindNonNull
|
||||
import io.reactivex.Flowable
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.web3j.protocol.ObjectMapperFactory
|
||||
import org.web3j.protocol.Web3jService
|
||||
import org.web3j.protocol.core.BatchRequest
|
||||
import org.web3j.protocol.core.BatchResponse
|
||||
import org.web3j.protocol.core.Request
|
||||
import org.web3j.protocol.core.Response
|
||||
import org.web3j.protocol.exceptions.ClientConnectionException
|
||||
import org.web3j.protocol.http.HttpService
|
||||
import org.web3j.protocol.websocket.events.Notification
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class BalancingHttpWeb3jService(
|
||||
initialNodes: Chain.Nodes,
|
||||
private val httpClient: OkHttpClient,
|
||||
private val strategyProvider: NodeSelectionStrategyProvider,
|
||||
private val objectMapper: ObjectMapper = ObjectMapperFactory.getObjectMapper(),
|
||||
) : Web3jService, UpdatableNodes {
|
||||
|
||||
private val nodeSwitcher = NodeSwitcher(
|
||||
initialStrategy = strategyProvider.createHttp(initialNodes),
|
||||
)
|
||||
|
||||
override fun updateNodes(nodes: Chain.Nodes) {
|
||||
val strategy = strategyProvider.createHttp(nodes)
|
||||
nodeSwitcher.updateNodes(strategy)
|
||||
}
|
||||
|
||||
override fun <T : Response<*>> send(request: Request<*, out Response<*>>, responseType: Class<T>): T {
|
||||
val payload: String = objectMapper.writeValueAsString(request)
|
||||
|
||||
val result = nodeSwitcher.makeRetryingRequest { url ->
|
||||
val call = createHttpCall(payload, url)
|
||||
|
||||
call.execute().parseSingleResponse(responseType)
|
||||
}
|
||||
|
||||
return result.throwOnRpcError()
|
||||
}
|
||||
|
||||
override fun <T : Response<*>> sendAsync(request: Request<*, out Response<*>>, responseType: Class<T>): CompletableFuture<T> {
|
||||
val payload: String = objectMapper.writeValueAsString(request)
|
||||
|
||||
return enqueueRetryingRequest(
|
||||
payload = payload,
|
||||
retriableProcessResponse = { response -> response.parseSingleResponse(responseType) },
|
||||
nonRetriableProcessResponse = { it.throwOnRpcError() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun sendBatch(batchRequest: BatchRequest): BatchResponse {
|
||||
if (batchRequest.requests.isEmpty()) {
|
||||
return BatchResponse(emptyList(), emptyList())
|
||||
}
|
||||
|
||||
val payload = objectMapper.writeValueAsString(batchRequest.requests)
|
||||
|
||||
val result = nodeSwitcher.makeRetryingRequest { url ->
|
||||
val call = createHttpCall(payload, url)
|
||||
|
||||
call.execute().parseBatchResponse(batchRequest)
|
||||
}
|
||||
|
||||
return result.throwOnRpcError()
|
||||
}
|
||||
|
||||
override fun sendBatchAsync(batchRequest: BatchRequest): CompletableFuture<BatchResponse> {
|
||||
val payload: String = objectMapper.writeValueAsString(batchRequest.requests)
|
||||
|
||||
return enqueueRetryingRequest(
|
||||
payload = payload,
|
||||
retriableProcessResponse = { response -> response.parseBatchResponse(batchRequest) },
|
||||
nonRetriableProcessResponse = { it.throwOnRpcError() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun <T : Notification<*>?> subscribe(
|
||||
request: Request<*, out Response<*>>,
|
||||
unsubscribeMethod: String,
|
||||
responseType: Class<T>
|
||||
): Flowable<T> {
|
||||
throw UnsupportedOperationException("Http transport does not support subscriptions")
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// nothing to close
|
||||
}
|
||||
|
||||
private fun <T> enqueueRetryingRequest(
|
||||
payload: String,
|
||||
retriableProcessResponse: (okhttp3.Response) -> T,
|
||||
nonRetriableProcessResponse: (T) -> Unit
|
||||
): CompletableFuture<T> {
|
||||
val completableFuture = CallCancellableFuture<T>()
|
||||
|
||||
enqueueRetryingRequest(completableFuture, payload, retriableProcessResponse, nonRetriableProcessResponse)
|
||||
|
||||
return completableFuture
|
||||
}
|
||||
|
||||
private fun <T> enqueueRetryingRequest(
|
||||
future: CallCancellableFuture<T>,
|
||||
payload: String,
|
||||
retriableProcessResponse: (okhttp3.Response) -> T,
|
||||
nonRetriableProcessResponse: (T) -> Unit
|
||||
) {
|
||||
val url = nodeSwitcher.getCurrentNodeUrl() ?: return
|
||||
|
||||
val call = createHttpCall(payload, url)
|
||||
future.call = call
|
||||
|
||||
call.enqueue(object : Callback {
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (future.isCancelled) return
|
||||
|
||||
nodeSwitcher.markCurrentNodeNotAccessible()
|
||||
enqueueRetryingRequest(future, payload, retriableProcessResponse, nonRetriableProcessResponse)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: okhttp3.Response) {
|
||||
if (future.isCancelled) return
|
||||
|
||||
try {
|
||||
val parsedResponse = retriableProcessResponse(response)
|
||||
|
||||
try {
|
||||
nonRetriableProcessResponse(parsedResponse)
|
||||
|
||||
future.complete(parsedResponse)
|
||||
} catch (e: Throwable) {
|
||||
future.completeExceptionally(e)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
nodeSwitcher.markCurrentNodeNotAccessible()
|
||||
enqueueRetryingRequest(future, payload, retriableProcessResponse, nonRetriableProcessResponse)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun createHttpCall(request: String, url: String): Call {
|
||||
val mediaType = HttpService.JSON_MEDIA_TYPE
|
||||
val requestBody: RequestBody = request.toRequestBody(mediaType)
|
||||
|
||||
val httpRequest: okhttp3.Request = okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
return httpClient.newCall(httpRequest)
|
||||
}
|
||||
|
||||
private fun <T : Response<*>> T.throwOnRpcError(): T {
|
||||
val rpcError = error
|
||||
if (rpcError != null) {
|
||||
throw EvmRpcException(rpcError.code, rpcError.message)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun BatchResponse.throwOnRpcError(): BatchResponse {
|
||||
val rpcError = responses.tryFindNonNull { it.error }
|
||||
if (rpcError != null) {
|
||||
throw EvmRpcException(rpcError.code, rpcError.message)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun <T : Response<*>> okhttp3.Response.parseSingleResponse(responseType: Class<T>): T {
|
||||
val parsedResponse = runCatching {
|
||||
body?.let {
|
||||
objectMapper.readValue(it.bytes(), responseType)
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
if (!isSuccessful || parsedResponse == null) {
|
||||
throw ClientConnectionException("Invalid response received: $code; ${body?.string()}")
|
||||
}
|
||||
|
||||
return parsedResponse
|
||||
}
|
||||
|
||||
private fun okhttp3.Response.parseBatchResponse(origin: BatchRequest): BatchResponse {
|
||||
val bodyContent = body?.string()
|
||||
|
||||
val parsedResponseResult = runCatching {
|
||||
origin.parseResponse(bodyContent!!)
|
||||
}
|
||||
|
||||
val parsedResponses = parsedResponseResult.getOrNull()
|
||||
|
||||
if (isSuccessful && parsedResponseResult.isFailure) {
|
||||
throw parsedResponseResult.requireException()
|
||||
}
|
||||
|
||||
if (!isSuccessful) {
|
||||
throw ClientConnectionException("Invalid response received: $code; $bodyContent")
|
||||
}
|
||||
|
||||
return BatchResponse(origin.requests, parsedResponses)
|
||||
}
|
||||
|
||||
private fun BatchRequest.parseResponse(response: String): List<Response<*>> {
|
||||
val requestsById = requests.associateBy { it.id }
|
||||
val nodes = objectMapper.readTree(response) as ArrayNode
|
||||
|
||||
return nodes.map { node ->
|
||||
val id = (node as ObjectNode).get("id").asLong()
|
||||
val request = requestsById.getValue(id)
|
||||
|
||||
objectMapper.treeToValue(node, request.responseType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NodeSwitcher(
|
||||
initialStrategy: NodeSequenceGenerator,
|
||||
) {
|
||||
|
||||
@Volatile
|
||||
private var balanceStrategy: NodeSequenceGenerator = initialStrategy
|
||||
|
||||
@Volatile
|
||||
private var nodeIterator: Iterator<NodeWithSaturatedUrl>? = null
|
||||
|
||||
@Volatile
|
||||
private var currentNodeUrl: String? = null
|
||||
|
||||
init {
|
||||
updateNodes(initialStrategy)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateNodes(strategy: NodeSequenceGenerator) {
|
||||
balanceStrategy = strategy
|
||||
|
||||
nodeIterator = balanceStrategy.generateNodeIterator()
|
||||
selectNextNode()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getCurrentNodeUrl(): String? {
|
||||
return currentNodeUrl
|
||||
}
|
||||
|
||||
@Suppress
|
||||
fun markCurrentNodeNotAccessible() {
|
||||
selectNextNode()
|
||||
}
|
||||
|
||||
private fun selectNextNode() {
|
||||
val iterator = nodeIterator ?: return
|
||||
if (iterator.hasNext()) {
|
||||
currentNodeUrl = iterator.next().saturatedUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> NodeSwitcher.makeRetryingRequest(request: (url: String) -> T): T {
|
||||
val url = getCurrentNodeUrl() ?: error("No url present to make a request")
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
return request(url)
|
||||
} catch (e: Throwable) {
|
||||
Log.w("Failed to execute request for url $url", e)
|
||||
|
||||
markCurrentNodeNotAccessible()
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CallCancellableFuture<T> : CompletableFuture<T>() {
|
||||
|
||||
var call: Call? = null
|
||||
|
||||
override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
|
||||
call?.cancel()
|
||||
|
||||
return super.cancel(mayInterruptIfRunning)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.runtime.ethereum
|
||||
|
||||
class EvmRpcException(val type: Type, message: String) : Throwable("${type.name}: $message") {
|
||||
|
||||
enum class Type(val code: Int?) {
|
||||
EXECUTION_REVERTED(-32603),
|
||||
INVALID_INPUT(-32000),
|
||||
UNKNOWN(null);
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: Int): Type {
|
||||
return values().firstOrNull { it.code == code } ?: UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun EvmRpcException(code: Int, message: String): EvmRpcException {
|
||||
return EvmRpcException(EvmRpcException.Type.fromCode(code), message)
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package io.novafoundation.nova.runtime.ethereum
|
||||
|
||||
import io.reactivex.Observable
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import io.novasama.substrate_sdk_android.wsrpc.request.DeliveryType
|
||||
import io.novasama.substrate_sdk_android.wsrpc.request.base.RpcRequest
|
||||
import io.novasama.substrate_sdk_android.wsrpc.response.RpcResponse
|
||||
import io.novasama.substrate_sdk_android.wsrpc.subscription.response.SubscriptionChange
|
||||
import kotlinx.coroutines.future.await
|
||||
import org.web3j.protocol.core.Request
|
||||
import org.web3j.protocol.core.Response
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
fun SocketService.executeRequestAsFuture(
|
||||
request: RpcRequest,
|
||||
deliveryType: DeliveryType = DeliveryType.AT_LEAST_ONCE,
|
||||
): CompletableFuture<RpcResponse> {
|
||||
val future = RequestCancellableFuture<RpcResponse>()
|
||||
|
||||
val callback = object : SocketService.ResponseListener<RpcResponse> {
|
||||
override fun onError(throwable: Throwable) {
|
||||
future.completeExceptionally(throwable)
|
||||
}
|
||||
|
||||
override fun onNext(response: RpcResponse) {
|
||||
future.complete(response)
|
||||
}
|
||||
}
|
||||
|
||||
future.cancellable = executeRequest(request, deliveryType, callback)
|
||||
|
||||
return future
|
||||
}
|
||||
|
||||
fun SocketService.executeBatchRequestAsFuture(
|
||||
requests: List<RpcRequest>,
|
||||
deliveryType: DeliveryType = DeliveryType.AT_LEAST_ONCE,
|
||||
): CompletableFuture<List<RpcResponse>> {
|
||||
val future = RequestCancellableFuture<List<RpcResponse>>()
|
||||
|
||||
val callback = object : SocketService.ResponseListener<List<RpcResponse>> {
|
||||
override fun onError(throwable: Throwable) {
|
||||
future.completeExceptionally(throwable)
|
||||
}
|
||||
|
||||
override fun onNext(response: List<RpcResponse>) {
|
||||
future.complete(response)
|
||||
}
|
||||
}
|
||||
|
||||
future.cancellable = executeAccumulatingBatchRequest(requests, deliveryType, callback)
|
||||
|
||||
return future
|
||||
}
|
||||
|
||||
fun SocketService.subscribeAsObservable(
|
||||
request: RpcRequest,
|
||||
unsubscribeMethod: String
|
||||
): Observable<SubscriptionChange> {
|
||||
return Observable.create { emitter ->
|
||||
val callback = object : SocketService.ResponseListener<SubscriptionChange> {
|
||||
override fun onError(throwable: Throwable) {
|
||||
emitter.tryOnError(throwable)
|
||||
}
|
||||
|
||||
override fun onNext(response: SubscriptionChange) {
|
||||
emitter.onNext(response)
|
||||
}
|
||||
}
|
||||
|
||||
val cancellable = subscribe(request, callback, unsubscribeMethod)
|
||||
|
||||
emitter.setCancellable(cancellable::cancel)
|
||||
}
|
||||
}
|
||||
|
||||
private class RequestCancellableFuture<T> : CompletableFuture<T>() {
|
||||
|
||||
var cancellable: SocketService.Cancellable? = null
|
||||
|
||||
override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
|
||||
cancellable?.cancel()
|
||||
|
||||
return super.cancel(mayInterruptIfRunning)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <S, T : Response<*>> Request<S, T>.sendSuspend(): T = sendAsync().await()
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package io.novafoundation.nova.runtime.ethereum
|
||||
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.common.utils.invokeOnCompletion
|
||||
import io.novafoundation.nova.core.ethereum.Web3Api
|
||||
import io.novafoundation.nova.core.ethereum.log.Topic
|
||||
import io.novafoundation.nova.core.model.StorageChange
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.runtime.ethereum.subscribtion.EthereumRequestsAggregator
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getSocketOrNull
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getSubscriptionEthereumApi
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.storage.StorageSubscriptionMultiplexer
|
||||
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.storage.subscribeUsing
|
||||
import io.novasama.substrate_sdk_android.wsrpc.subscribe
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.web3j.protocol.core.Request
|
||||
import org.web3j.protocol.core.Response
|
||||
import org.web3j.protocol.websocket.events.LogNotification
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class StorageSharedRequestsBuilderFactory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
) {
|
||||
|
||||
suspend fun create(chainId: ChainId): StorageSharedRequestsBuilder {
|
||||
val substrateProxy = StorageSubscriptionMultiplexer.Builder()
|
||||
val ethereumProxy = EthereumRequestsAggregator.Builder()
|
||||
|
||||
val rpcSocket = chainRegistry.getSocketOrNull(chainId)
|
||||
|
||||
val subscriptionApi = chainRegistry.getSubscriptionEthereumApi(chainId)
|
||||
val callApi = chainRegistry.getCallEthereumApi(chainId)
|
||||
|
||||
return StorageSharedRequestsBuilder(
|
||||
socketService = rpcSocket,
|
||||
substrateProxy = substrateProxy,
|
||||
ethereumProxy = ethereumProxy,
|
||||
subscriptionApi = subscriptionApi,
|
||||
callApi = callApi
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class StorageSharedRequestsBuilder(
|
||||
override val socketService: SocketService?,
|
||||
private val substrateProxy: StorageSubscriptionMultiplexer.Builder,
|
||||
private val ethereumProxy: EthereumRequestsAggregator.Builder,
|
||||
override val subscriptionApi: Web3Api?,
|
||||
override val callApi: Web3Api?,
|
||||
) : SharedRequestsBuilder {
|
||||
|
||||
override fun subscribe(key: String): Flow<StorageChange> {
|
||||
return substrateProxy.subscribe(key)
|
||||
.map { StorageChange(it.block, it.key, it.value) }
|
||||
}
|
||||
|
||||
override fun <S, T : Response<*>> ethBatchRequestAsync(batchId: String, request: Request<S, T>): CompletableFuture<T> {
|
||||
return ethereumProxy.batchRequest(batchId, request)
|
||||
}
|
||||
|
||||
override fun subscribeEthLogs(address: String, topics: List<Topic>): Flow<LogNotification> {
|
||||
return ethereumProxy.subscribeLogs(address, topics)
|
||||
}
|
||||
|
||||
fun subscribe(coroutineScope: CoroutineScope) {
|
||||
val ethereumRequestsAggregator = ethereumProxy.build()
|
||||
|
||||
subscriptionApi?.let { web3Api ->
|
||||
ethereumRequestsAggregator.subscribeUsing(web3Api)
|
||||
.inBackground()
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
callApi?.let { web3Api ->
|
||||
ethereumRequestsAggregator.executeBatches(coroutineScope, web3Api)
|
||||
}
|
||||
|
||||
val cancellable = socketService?.subscribeUsing(substrateProxy.build())
|
||||
|
||||
if (cancellable != null) {
|
||||
coroutineScope.invokeOnCompletion { cancellable.cancel() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun StorageSharedRequestsBuilder.subscribe(coroutineContext: CoroutineContext) {
|
||||
subscribe(CoroutineScope(coroutineContext))
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package io.novafoundation.nova.runtime.ethereum
|
||||
|
||||
import io.novafoundation.nova.core.ethereum.Web3Api
|
||||
import io.novafoundation.nova.core.ethereum.log.Topic
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.UpdatableNodes
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSelectionStrategyProvider
|
||||
import io.novasama.substrate_sdk_android.extensions.requireHexPrefix
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
import okhttp3.OkHttpClient
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.Web3jService
|
||||
import org.web3j.protocol.core.JsonRpc2_0Web3j
|
||||
import org.web3j.protocol.core.Request
|
||||
import org.web3j.protocol.core.methods.response.EthSubscribe
|
||||
import org.web3j.protocol.websocket.events.LogNotification
|
||||
import org.web3j.protocol.websocket.events.NewHeadsNotification
|
||||
import org.web3j.utils.Async
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
|
||||
class Web3ApiFactory(
|
||||
private val requestExecutorService: ScheduledExecutorService = Async.defaultExecutorService(),
|
||||
private val httpClient: OkHttpClient,
|
||||
private val strategyProvider: NodeSelectionStrategyProvider,
|
||||
) {
|
||||
|
||||
fun createWss(socketService: SocketService): Web3Api {
|
||||
val web3jService = WebSocketWeb3jService(socketService)
|
||||
|
||||
return RealWeb3Api(
|
||||
web3jService = web3jService,
|
||||
delegate = Web3j.build(web3jService, JsonRpc2_0Web3j.DEFAULT_BLOCK_TIME.toLong(), requestExecutorService)
|
||||
)
|
||||
}
|
||||
|
||||
fun createHttps(chainNode: Chain.Node): Pair<Web3Api, UpdatableNodes> {
|
||||
val nodes = Chain.Nodes(
|
||||
autoBalanceStrategy = Chain.Nodes.AutoBalanceStrategy.ROUND_ROBIN,
|
||||
wssNodeSelectionStrategy = Chain.Nodes.NodeSelectionStrategy.AutoBalance,
|
||||
nodes = listOf(chainNode)
|
||||
)
|
||||
|
||||
return createHttps(nodes)
|
||||
}
|
||||
|
||||
fun createHttps(chainNodes: Chain.Nodes): Pair<Web3Api, UpdatableNodes> {
|
||||
val service = BalancingHttpWeb3jService(
|
||||
initialNodes = chainNodes,
|
||||
httpClient = httpClient,
|
||||
strategyProvider = strategyProvider,
|
||||
)
|
||||
|
||||
val api = RealWeb3Api(
|
||||
web3jService = service,
|
||||
delegate = Web3j.build(service, JsonRpc2_0Web3j.DEFAULT_BLOCK_TIME.toLong(), requestExecutorService)
|
||||
)
|
||||
|
||||
return api to service
|
||||
}
|
||||
}
|
||||
|
||||
internal class RealWeb3Api(
|
||||
private val web3jService: Web3jService,
|
||||
private val delegate: Web3j
|
||||
) : Web3Api, Web3j by delegate {
|
||||
override fun newHeadsFlow(): Flow<NewHeadsNotification> = newHeadsNotifications().asFlow()
|
||||
|
||||
override fun logsNotifications(addresses: List<String>, topics: List<Topic>): Flow<LogNotification> {
|
||||
val logParams = createLogParams(addresses, topics)
|
||||
val requestParams = listOf("logs", logParams)
|
||||
|
||||
val request = Request("eth_subscribe", requestParams, web3jService, EthSubscribe::class.java)
|
||||
|
||||
return web3jService.subscribe(request, "eth_unsubscribe", LogNotification::class.java)
|
||||
.asFlow()
|
||||
}
|
||||
|
||||
private fun createLogParams(addresses: List<String>, topics: List<Topic>): Map<String, Any> {
|
||||
return buildMap {
|
||||
if (addresses.isNotEmpty()) {
|
||||
put("address", addresses)
|
||||
}
|
||||
|
||||
if (topics.isNotEmpty()) {
|
||||
put("topics", topics.unifyTopics())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Topic>.unifyTopics(): List<Any?> {
|
||||
return map { topic ->
|
||||
when (topic) {
|
||||
Topic.Any -> null
|
||||
is Topic.AnyOf -> topic.values.map { it.requireHexPrefix() }
|
||||
is Topic.Single -> topic.value.requireHexPrefix()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package io.novafoundation.nova.runtime.ethereum
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.reactivex.BackpressureStrategy
|
||||
import io.reactivex.Flowable
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import io.novasama.substrate_sdk_android.wsrpc.request.base.RpcRequest
|
||||
import io.novasama.substrate_sdk_android.wsrpc.response.RpcResponse
|
||||
import org.web3j.protocol.ObjectMapperFactory
|
||||
import org.web3j.protocol.Web3jService
|
||||
import org.web3j.protocol.core.BatchRequest
|
||||
import org.web3j.protocol.core.BatchResponse
|
||||
import org.web3j.protocol.core.Request
|
||||
import org.web3j.protocol.core.Response
|
||||
import org.web3j.protocol.websocket.WebSocketService
|
||||
import org.web3j.protocol.websocket.events.Notification
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
class WebSocketWeb3jService(
|
||||
private val socketService: SocketService,
|
||||
private val jsonMapper: ObjectMapper = ObjectMapperFactory.getObjectMapper()
|
||||
) : Web3jService {
|
||||
|
||||
/**
|
||||
* Implementation based on [WebSocketService.send]
|
||||
*/
|
||||
override fun <T : Response<*>?> send(request: Request<*, out Response<*>>, responseType: Class<T>): T {
|
||||
return try {
|
||||
sendAsync(request, responseType).get()
|
||||
} catch (e: InterruptedException) {
|
||||
Thread.interrupted()
|
||||
throw IOException("Interrupted WebSocket request", e)
|
||||
} catch (e: ExecutionException) {
|
||||
if (e.cause is IOException) {
|
||||
throw e.cause as IOException
|
||||
}
|
||||
throw RuntimeException("Unexpected exception", e.cause)
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Response<*>?> sendAsync(request: Request<*, out Response<*>>, responseType: Class<T>): CompletableFuture<T> {
|
||||
val rpcRequest = request.toRpcRequest()
|
||||
|
||||
return socketService.executeRequestAsFuture(rpcRequest).thenApply {
|
||||
if (it.error != null) {
|
||||
throw EvmRpcException(it.error!!.code, it.error!!.message)
|
||||
}
|
||||
|
||||
jsonMapper.convertValue(it, responseType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Notification<*>?> subscribe(
|
||||
request: Request<*, out Response<*>>,
|
||||
unsubscribeMethod: String,
|
||||
responseType: Class<T>
|
||||
): Flowable<T> {
|
||||
val rpcRequest = request.toRpcRequest()
|
||||
|
||||
return socketService.subscribeAsObservable(rpcRequest, unsubscribeMethod).map {
|
||||
jsonMapper.convertValue(it, responseType)
|
||||
}.toFlowable(BackpressureStrategy.LATEST)
|
||||
}
|
||||
|
||||
override fun sendBatch(batchRequest: BatchRequest): BatchResponse {
|
||||
return try {
|
||||
sendBatchAsync(batchRequest).get()
|
||||
} catch (e: InterruptedException) {
|
||||
Thread.interrupted()
|
||||
throw IOException("Interrupted WebSocket batch request", e)
|
||||
} catch (e: ExecutionException) {
|
||||
if (e.cause is IOException) {
|
||||
throw e.cause as IOException
|
||||
}
|
||||
throw RuntimeException("Unexpected exception", e.cause)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendBatchAsync(batchRequest: BatchRequest): CompletableFuture<BatchResponse> {
|
||||
val rpcRequests = batchRequest.requests.map { it.toRpcRequest() }
|
||||
|
||||
return socketService.executeBatchRequestAsFuture(rpcRequests).thenApply { responses ->
|
||||
val responsesById = responses.associateBy(RpcResponse::id)
|
||||
|
||||
val parsedResponses = batchRequest.requests.mapNotNull { request ->
|
||||
responsesById[request.id.toInt()]?.let { rpcResponse ->
|
||||
jsonMapper.convertValue(rpcResponse, request.responseType)
|
||||
}
|
||||
}
|
||||
|
||||
BatchResponse(batchRequest.requests, parsedResponses)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// other components handle lifecycle of socketService
|
||||
}
|
||||
|
||||
private fun Request<*, *>.toRpcRequest(): RpcRequest {
|
||||
val raw = jsonMapper.writeValueAsString(this)
|
||||
|
||||
return RpcRequest.Raw(raw, id.toInt())
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.contract.base
|
||||
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.caller.ContractCaller
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.caller.ethCallSuspend
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.future.asDeferred
|
||||
import org.web3j.abi.FunctionEncoder
|
||||
import org.web3j.abi.FunctionReturnDecoder
|
||||
import org.web3j.abi.datatypes.Function
|
||||
import org.web3j.abi.datatypes.Type
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.methods.request.Transaction
|
||||
import org.web3j.protocol.core.methods.response.EthCall
|
||||
import org.web3j.tx.TransactionManager
|
||||
import org.web3j.tx.exceptions.ContractCallException
|
||||
|
||||
open class CallableContract(
|
||||
protected val contractAddress: String,
|
||||
protected val contractCaller: ContractCaller,
|
||||
protected val defaultBlockParameter: DefaultBlockParameter,
|
||||
) {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
protected fun <T : Type<*>?, R> executeCallSingleValueReturnAsync(
|
||||
function: Function,
|
||||
extractResult: (T) -> R,
|
||||
): Deferred<R> {
|
||||
val tx = createTx(function)
|
||||
|
||||
return contractCaller.ethCall(tx, defaultBlockParameter).thenApply { ethCall ->
|
||||
processEthCallResponse(ethCall, function, extractResult)
|
||||
}.asDeferred()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
protected suspend fun <T : Type<*>?, R> executeCallSingleValueReturnSuspend(
|
||||
function: Function,
|
||||
extractResult: (T) -> R,
|
||||
): R {
|
||||
val tx = createTx(function)
|
||||
val ethCall = contractCaller.ethCallSuspend(tx, defaultBlockParameter)
|
||||
|
||||
return processEthCallResponse(ethCall, function, extractResult)
|
||||
}
|
||||
|
||||
private fun <R, T : Type<*>?> processEthCallResponse(
|
||||
ethCall: EthCall,
|
||||
function: Function,
|
||||
extractResult: (T) -> R
|
||||
): R {
|
||||
assertCallNotReverted(ethCall)
|
||||
|
||||
val args = FunctionReturnDecoder.decode(ethCall.value, function.outputParameters)
|
||||
val type = args.first() as T
|
||||
|
||||
return extractResult(type)
|
||||
}
|
||||
|
||||
private fun createTx(function: Function): Transaction {
|
||||
val encodedFunction = FunctionEncoder.encode(function)
|
||||
return Transaction.createEthCallTransaction(null, contractAddress, encodedFunction)
|
||||
}
|
||||
|
||||
private fun assertCallNotReverted(ethCall: EthCall) {
|
||||
if (ethCall.isReverted) {
|
||||
throw ContractCallException(String.format(TransactionManager.REVERT_ERR_STR, ethCall.revertReason))
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.contract.base
|
||||
|
||||
import io.novafoundation.nova.core.updater.EthereumSharedRequestsBuilder
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.caller.BatchContractCaller
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.caller.ContractCaller
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.caller.SingleContractCaller
|
||||
import io.novafoundation.nova.runtime.ethereum.subscribtion.BatchId
|
||||
import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.DefaultBlockParameterName
|
||||
|
||||
interface ContractStandard<Q, T> {
|
||||
|
||||
fun query(
|
||||
address: String,
|
||||
caller: ContractCaller,
|
||||
defaultBlockParameter: DefaultBlockParameter = DefaultBlockParameterName.LATEST
|
||||
): Q
|
||||
|
||||
fun transact(
|
||||
contractAddress: String,
|
||||
transactionBuilder: EvmTransactionBuilder
|
||||
): T
|
||||
}
|
||||
|
||||
fun <C> ContractStandard<C, *>.queryBatched(
|
||||
address: String,
|
||||
batchId: BatchId,
|
||||
ethereumSharedRequestsBuilder: EthereumSharedRequestsBuilder,
|
||||
): C = query(address, BatchContractCaller(batchId, ethereumSharedRequestsBuilder))
|
||||
|
||||
fun <C> ContractStandard<C, *>.querySingle(
|
||||
address: String,
|
||||
web3j: Web3j,
|
||||
): C = query(address, SingleContractCaller(web3j))
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.contract.base.caller
|
||||
|
||||
import io.novafoundation.nova.core.updater.EthereumSharedRequestsBuilder
|
||||
import io.novafoundation.nova.core.updater.callApiOrThrow
|
||||
import io.novafoundation.nova.runtime.ethereum.subscribtion.BatchId
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.methods.request.Transaction
|
||||
import org.web3j.protocol.core.methods.response.EthCall
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class BatchContractCaller(
|
||||
private val batchId: BatchId,
|
||||
private val ethereumSharedRequestsBuilder: EthereumSharedRequestsBuilder,
|
||||
) : ContractCaller {
|
||||
|
||||
override fun ethCall(transaction: Transaction, defaultBlockParameter: DefaultBlockParameter): CompletableFuture<EthCall> {
|
||||
val request = ethereumSharedRequestsBuilder.callApiOrThrow.ethCall(transaction, defaultBlockParameter)
|
||||
|
||||
return ethereumSharedRequestsBuilder.ethBatchRequestAsync(batchId, request)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.contract.base.caller
|
||||
|
||||
import kotlinx.coroutines.future.asDeferred
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.methods.request.Transaction
|
||||
import org.web3j.protocol.core.methods.response.EthCall
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
interface ContractCaller {
|
||||
|
||||
fun ethCall(
|
||||
transaction: Transaction,
|
||||
defaultBlockParameter: DefaultBlockParameter,
|
||||
): CompletableFuture<EthCall>
|
||||
}
|
||||
|
||||
suspend fun ContractCaller.ethCallSuspend(
|
||||
transaction: Transaction,
|
||||
defaultBlockParameter: DefaultBlockParameter,
|
||||
): EthCall = ethCall(transaction, defaultBlockParameter).asDeferred().await()
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.contract.base.caller
|
||||
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.methods.request.Transaction
|
||||
import org.web3j.protocol.core.methods.response.EthCall
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class SingleContractCaller(
|
||||
private val web3j: Web3j
|
||||
) : ContractCaller {
|
||||
|
||||
override fun ethCall(transaction: Transaction, defaultBlockParameter: DefaultBlockParameter): CompletableFuture<EthCall> {
|
||||
val request = web3j.ethCall(transaction, defaultBlockParameter)
|
||||
|
||||
return request.sendAsync()
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.contract.erc20
|
||||
|
||||
import io.novafoundation.nova.common.utils.ethereumAccountIdToAddress
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.CallableContract
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.ContractStandard
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.caller.ContractCaller
|
||||
import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.Deferred
|
||||
import org.web3j.abi.TypeReference
|
||||
import org.web3j.abi.datatypes.Address
|
||||
import org.web3j.abi.datatypes.Function
|
||||
import org.web3j.abi.datatypes.Utf8String
|
||||
import org.web3j.abi.datatypes.generated.Uint256
|
||||
import org.web3j.abi.datatypes.generated.Uint8
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import java.math.BigInteger
|
||||
|
||||
class Erc20Standard : ContractStandard<Erc20Queries, Erc20Transactions> {
|
||||
|
||||
override fun query(address: String, caller: ContractCaller, defaultBlockParameter: DefaultBlockParameter): Erc20Queries {
|
||||
return Erc20QueriesImpl(address, caller, defaultBlockParameter)
|
||||
}
|
||||
|
||||
override fun transact(contractAddress: String, transactionBuilder: EvmTransactionBuilder): Erc20Transactions {
|
||||
return Erc20TransactionsImpl(contractAddress, transactionBuilder)
|
||||
}
|
||||
}
|
||||
|
||||
private class Erc20TransactionsImpl(
|
||||
private val contractAddress: String,
|
||||
private val evmTransactionsBuilder: EvmTransactionBuilder,
|
||||
) : Erc20Transactions {
|
||||
|
||||
override fun transfer(recipient: AccountId, amount: BigInteger) {
|
||||
evmTransactionsBuilder.contractCall(contractAddress) {
|
||||
function = "transfer"
|
||||
|
||||
inputParameter(Address(recipient.ethereumAccountIdToAddress(withChecksum = true)))
|
||||
inputParameter(Uint256(amount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Erc20QueriesImpl(
|
||||
contractAddress: String,
|
||||
caller: ContractCaller,
|
||||
blockParameter: DefaultBlockParameter,
|
||||
) : CallableContract(contractAddress, caller, blockParameter), Erc20Queries {
|
||||
|
||||
override suspend fun balanceOfAsync(account: String): Deferred<BigInteger> {
|
||||
val function = Function(
|
||||
/* name = */
|
||||
"balanceOf",
|
||||
/* inputParameters = */
|
||||
listOf(
|
||||
Address(account)
|
||||
),
|
||||
/* outputParameters = */
|
||||
listOf(
|
||||
object : TypeReference<Uint256>() {}
|
||||
),
|
||||
)
|
||||
|
||||
return executeCallSingleValueReturnAsync(function, Uint256::getValue)
|
||||
}
|
||||
|
||||
override suspend fun symbol(): String {
|
||||
val outputParams = listOf(
|
||||
object : TypeReference<Utf8String>() {}
|
||||
)
|
||||
val function = Function("symbol", emptyList(), outputParams)
|
||||
|
||||
return executeCallSingleValueReturnSuspend(function, Utf8String::getValue)
|
||||
}
|
||||
|
||||
override suspend fun decimals(): BigInteger {
|
||||
val outputParams = listOf(
|
||||
object : TypeReference<Uint8>() {}
|
||||
)
|
||||
val function = Function("decimals", emptyList(), outputParams)
|
||||
|
||||
return executeCallSingleValueReturnSuspend(function, Uint8::getValue)
|
||||
}
|
||||
|
||||
override suspend fun totalSupply(): BigInteger {
|
||||
val outputParams = listOf(
|
||||
object : TypeReference<Uint256>() {}
|
||||
)
|
||||
val function = Function("totalSupply", emptyList(), outputParams)
|
||||
|
||||
return executeCallSingleValueReturnSuspend(function, Uint256::getValue)
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.contract.erc20
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
import org.web3j.abi.EventEncoder
|
||||
import org.web3j.abi.TypeDecoder
|
||||
import org.web3j.abi.TypeReference
|
||||
import org.web3j.abi.datatypes.Address
|
||||
import org.web3j.abi.datatypes.Event
|
||||
import org.web3j.abi.datatypes.generated.Uint256
|
||||
import org.web3j.protocol.websocket.events.Log
|
||||
import java.math.BigInteger
|
||||
|
||||
interface Erc20Queries {
|
||||
|
||||
class Transfer(val from: Address, val to: Address, val amount: Uint256)
|
||||
|
||||
companion object {
|
||||
val TRANSFER_EVENT = Event(
|
||||
"Transfer",
|
||||
listOf(
|
||||
object : TypeReference<Address>(true) {},
|
||||
object : TypeReference<Address>(true) {},
|
||||
object : TypeReference<Uint256>(false) {}
|
||||
)
|
||||
)
|
||||
|
||||
fun transferEventSignature(): String {
|
||||
return EventEncoder.encode(TRANSFER_EVENT)
|
||||
}
|
||||
|
||||
fun parseTransferEvent(log: Log): Transfer {
|
||||
return parseTransferEvent(
|
||||
topic1 = log.topics[1],
|
||||
topic2 = log.topics[2],
|
||||
data = log.data
|
||||
)
|
||||
}
|
||||
|
||||
fun parseTransferEvent(
|
||||
topic1: String,
|
||||
topic2: String,
|
||||
data: String
|
||||
): Transfer {
|
||||
return Transfer(
|
||||
from = TypeDecoder.decodeAddress(topic1),
|
||||
to = TypeDecoder.decodeAddress(topic2),
|
||||
amount = TypeDecoder.decodeNumeric(data, Uint256::class.java)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun balanceOfAsync(account: String): Deferred<BigInteger>
|
||||
|
||||
suspend fun symbol(): String
|
||||
|
||||
suspend fun decimals(): BigInteger
|
||||
|
||||
suspend fun totalSupply(): BigInteger
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.contract.erc20
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import java.math.BigInteger
|
||||
|
||||
interface Erc20Transactions {
|
||||
|
||||
fun transfer(recipient: AccountId, amount: BigInteger)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.gas
|
||||
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.utils.tryFindNonNull
|
||||
import java.math.BigInteger
|
||||
|
||||
class CompoundGasPriceProvider(vararg val delegates: GasPriceProvider) : GasPriceProvider {
|
||||
|
||||
override suspend fun getGasPrice(): BigInteger {
|
||||
return delegates.tryFindNonNull { delegate ->
|
||||
runCatching { delegate.getGasPrice() }.getOrNull()
|
||||
}.orZero()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.gas
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
interface GasPriceProvider {
|
||||
|
||||
suspend fun getGasPrice(): BigInteger
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.gas
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import org.web3j.protocol.Web3j
|
||||
|
||||
interface GasPriceProviderFactory {
|
||||
|
||||
/**
|
||||
* Creates gas provider for a [chainId] that is known to the app
|
||||
*/
|
||||
suspend fun createKnown(chainId: ChainId): GasPriceProvider
|
||||
|
||||
/**
|
||||
* Creates gas provider for arbitrary EVM chain given instance of [Web3j]
|
||||
*/
|
||||
suspend fun create(web3j: Web3j): GasPriceProvider
|
||||
}
|
||||
|
||||
class RealGasPriceProviderFactory(
|
||||
private val chainRegistry: ChainRegistry
|
||||
) : GasPriceProviderFactory {
|
||||
|
||||
override suspend fun createKnown(chainId: ChainId): GasPriceProvider {
|
||||
val api = chainRegistry.getCallEthereumApiOrThrow(chainId)
|
||||
|
||||
return create(api)
|
||||
}
|
||||
|
||||
override suspend fun create(web3j: Web3j): GasPriceProvider {
|
||||
return CompoundGasPriceProvider(
|
||||
MaxPriorityFeeGasProvider(web3j),
|
||||
LegacyGasPriceProvider(web3j)
|
||||
)
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.gas
|
||||
|
||||
import io.novafoundation.nova.runtime.ethereum.sendSuspend
|
||||
import org.web3j.protocol.Web3j
|
||||
import java.math.BigInteger
|
||||
|
||||
class LegacyGasPriceProvider(private val api: Web3j) : GasPriceProvider {
|
||||
|
||||
override suspend fun getGasPrice(): BigInteger {
|
||||
return api.ethGasPrice().sendSuspend().gasPrice
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.gas
|
||||
|
||||
import io.novafoundation.nova.runtime.ethereum.sendSuspend
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameterName
|
||||
import java.math.BigInteger
|
||||
|
||||
class MaxPriorityFeeGasProvider(private val api: Web3j) : GasPriceProvider {
|
||||
|
||||
override suspend fun getGasPrice(): BigInteger {
|
||||
val baseFeePerGas = api.getLatestBaseFeePerGas()
|
||||
val maxPriorityFee = api.ethMaxPriorityFeePerGas().sendSuspend().maxPriorityFeePerGas
|
||||
|
||||
return baseFeePerGas + maxPriorityFee
|
||||
}
|
||||
|
||||
private suspend fun Web3j.getLatestBaseFeePerGas(): BigInteger {
|
||||
val block = ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false).sendSuspend()
|
||||
|
||||
return block.block.baseFeePerGas
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.log
|
||||
|
||||
sealed class Topic {
|
||||
|
||||
object Any : Topic()
|
||||
|
||||
class Single(val value: String) : Topic()
|
||||
|
||||
class AnyOf(val values: List<String>) : Topic() {
|
||||
|
||||
constructor(vararg values: String) : this(values.toList())
|
||||
}
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.subscribtion
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.cast
|
||||
import io.novafoundation.nova.common.utils.mergeIfMultiple
|
||||
import io.novafoundation.nova.core.ethereum.Web3Api
|
||||
import io.novafoundation.nova.core.ethereum.log.Topic
|
||||
import io.novasama.substrate_sdk_android.extensions.asEthereumAddress
|
||||
import io.novasama.substrate_sdk_android.extensions.toAccountId
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService.ResponseListener
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.future.asDeferred
|
||||
import org.web3j.protocol.core.BatchRequest
|
||||
import org.web3j.protocol.core.BatchResponse
|
||||
import org.web3j.protocol.core.Request
|
||||
import org.web3j.protocol.core.Response
|
||||
import org.web3j.protocol.websocket.events.LogNotification
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
typealias SubscriptionId = String
|
||||
typealias BatchId = String
|
||||
typealias RequestId = Int
|
||||
|
||||
sealed class EthereumSubscription<S>(val id: SubscriptionId) {
|
||||
|
||||
class Log(val addresses: List<String>, val topics: List<Topic>, id: SubscriptionId) : EthereumSubscription<LogNotification>(id)
|
||||
}
|
||||
|
||||
class EthereumRequestsAggregator private constructor(
|
||||
private val subscriptions: List<EthereumSubscription<*>>,
|
||||
private val collectors: Map<SubscriptionId, List<ResponseListener<*>>>,
|
||||
private val batches: List<PendingBatchRequest>
|
||||
) {
|
||||
|
||||
fun subscribeUsing(web3Api: Web3Api): Flow<*> {
|
||||
return subscriptions.map {
|
||||
when (it) {
|
||||
is EthereumSubscription.Log -> web3Api.subscribeLogs(it)
|
||||
}
|
||||
}.mergeIfMultiple()
|
||||
}
|
||||
|
||||
fun executeBatches(scope: CoroutineScope, web3Api: Web3Api) {
|
||||
batches.forEach { pendingBatchRequest ->
|
||||
scope.async {
|
||||
val batch = web3Api.newBatch().apply {
|
||||
pendingBatchRequest.requests.forEach {
|
||||
add(it)
|
||||
}
|
||||
}
|
||||
|
||||
executeBatch(batch, pendingBatchRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Web3Api.subscribeLogs(subscription: EthereumSubscription.Log): Flow<*> {
|
||||
return logsNotifications(subscription.addresses, subscription.topics).onEach { logNotification ->
|
||||
subscription.dispatchChange(logNotification)
|
||||
}.catch {
|
||||
subscription.dispatchError(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun executeBatch(
|
||||
batch: BatchRequest,
|
||||
pendingBatchRequest: PendingBatchRequest
|
||||
): Result<BatchResponse> {
|
||||
return runCatching { batch.sendAsync().asDeferred().await() }
|
||||
.onSuccess { batchResponse ->
|
||||
batchResponse.responses.onEach { response ->
|
||||
val callback = pendingBatchRequest.callbacks[response.id.toInt()] ?: return@onEach
|
||||
|
||||
callback.cast<BatchCallback<Any?>>().onNext(response)
|
||||
}
|
||||
}
|
||||
.onFailure { error ->
|
||||
pendingBatchRequest.callbacks.values.forEach {
|
||||
it.onError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified S> EthereumSubscription<S>.dispatchChange(change: S) {
|
||||
val collectors = collectors[id].orEmpty()
|
||||
|
||||
collectors.forEach {
|
||||
it.cast<ResponseListener<S>>().onNext(change)
|
||||
}
|
||||
}
|
||||
|
||||
private fun EthereumSubscription<*>.dispatchError(error: Throwable) {
|
||||
val collectors = collectors[id].orEmpty()
|
||||
|
||||
collectors.forEach { it.onError(error) }
|
||||
}
|
||||
|
||||
class Builder {
|
||||
|
||||
// We do not initialize them by default to not to allocate arrays and maps when not needed
|
||||
private var subscriptions: MutableList<EthereumSubscriptionBuilder<*>>? = null
|
||||
private var collectors: MutableMap<SubscriptionId, MutableList<ResponseListener<*>>>? = null
|
||||
|
||||
private var batches: MutableMap<BatchId, PendingBatchRequestBuilder>? = null
|
||||
|
||||
fun subscribeLogs(address: String, topics: List<Topic>): Flow<LogNotification> {
|
||||
val subscriptions = ensureSubscriptions()
|
||||
val existingSubscription = subscriptions.firstOrNull { it is EthereumSubscriptionBuilder.Log && it.topics == topics }
|
||||
|
||||
val subscription = if (existingSubscription != null) {
|
||||
require(existingSubscription is EthereumSubscriptionBuilder.Log)
|
||||
existingSubscription.addresses.add(address)
|
||||
existingSubscription
|
||||
} else {
|
||||
val newSubscription = EthereumSubscriptionBuilder.Log(topics = topics, addresses = mutableListOf(address))
|
||||
subscriptions.add(newSubscription)
|
||||
newSubscription
|
||||
}
|
||||
|
||||
val collector = LogsCallback(address)
|
||||
val subscriptionCollectors = ensureCollectors().getOrPut(subscription.id, ::mutableListOf)
|
||||
subscriptionCollectors.add(collector)
|
||||
|
||||
return collector.inner.map { it.getOrThrow() }
|
||||
}
|
||||
|
||||
fun <S, T : Response<*>> batchRequest(batchId: BatchId, request: Request<S, T>): CompletableFuture<T> {
|
||||
val batches = ensureBatches()
|
||||
val batch = batches.getOrPut(batchId, ::PendingBatchRequestBuilder)
|
||||
|
||||
val callback = BatchCallback<T>()
|
||||
|
||||
batch.requests += request
|
||||
batch.callbacks[request.id.toInt()] = callback
|
||||
|
||||
return callback.future
|
||||
}
|
||||
|
||||
fun build(): EthereumRequestsAggregator {
|
||||
return EthereumRequestsAggregator(
|
||||
subscriptions = subscriptions.orEmpty().map { it.build() },
|
||||
collectors = collectors.orEmpty(),
|
||||
batches = batches.orEmpty().values.map { it.build() }
|
||||
)
|
||||
}
|
||||
|
||||
private fun ensureSubscriptions(): MutableList<EthereumSubscriptionBuilder<*>> {
|
||||
if (subscriptions == null) {
|
||||
subscriptions = mutableListOf()
|
||||
}
|
||||
|
||||
return subscriptions!!
|
||||
}
|
||||
|
||||
private fun ensureCollectors(): MutableMap<SubscriptionId, MutableList<ResponseListener<*>>> {
|
||||
if (collectors == null) {
|
||||
collectors = mutableMapOf()
|
||||
}
|
||||
|
||||
return collectors!!
|
||||
}
|
||||
|
||||
private fun ensureBatches(): MutableMap<BatchId, PendingBatchRequestBuilder> {
|
||||
if (batches == null) {
|
||||
batches = mutableMapOf()
|
||||
}
|
||||
|
||||
return batches!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EthereumSubscriptionBuilder<S> {
|
||||
|
||||
val id: SubscriptionId = UUID.randomUUID().toString()
|
||||
|
||||
abstract fun build(): EthereumSubscription<S>
|
||||
|
||||
class Log(val topics: List<Topic>, val addresses: MutableList<String> = mutableListOf()) : EthereumSubscriptionBuilder<LogNotification>() {
|
||||
|
||||
override fun build(): EthereumSubscription<LogNotification> {
|
||||
return EthereumSubscription.Log(addresses, topics, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LogsCallback(contractAddress: String) : SubscribeCallback<LogNotification>() {
|
||||
|
||||
val contractAccountId = contractAddress.asEthereumAddress().toAccountId().value
|
||||
|
||||
override fun shouldHandle(change: LogNotification): Boolean {
|
||||
val changeAddress = change.params.result.address
|
||||
val changeAccountId = changeAddress.asEthereumAddress().toAccountId().value
|
||||
|
||||
return contractAccountId.contentEquals(changeAccountId)
|
||||
}
|
||||
}
|
||||
|
||||
private class PendingBatchRequest(val requests: List<Request<*, *>>, val callbacks: Map<RequestId, BatchCallback<*>>)
|
||||
|
||||
private class PendingBatchRequestBuilder(
|
||||
val requests: MutableList<Request<*, *>> = mutableListOf(),
|
||||
val callbacks: MutableMap<RequestId, BatchCallback<*>> = mutableMapOf()
|
||||
) {
|
||||
|
||||
fun build(): PendingBatchRequest = PendingBatchRequest(requests, callbacks)
|
||||
}
|
||||
|
||||
private class BatchCallback<R> : ResponseListener<R> {
|
||||
|
||||
val future = CompletableFuture<R>()
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
future.completeExceptionally(throwable)
|
||||
}
|
||||
|
||||
override fun onNext(response: R) {
|
||||
future.complete(response)
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class SubscribeCallback<R> : ResponseListener<R> {
|
||||
|
||||
val inner = MutableSharedFlow<Result<R>>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
abstract fun shouldHandle(change: R): Boolean
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
inner.tryEmit(Result.failure(throwable))
|
||||
}
|
||||
|
||||
override fun onNext(response: R) {
|
||||
if (shouldHandle(response)) {
|
||||
inner.tryEmit(Result.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.transaction.builder
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.ContractStandard
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import org.web3j.abi.TypeReference
|
||||
import org.web3j.abi.datatypes.Type as EvmType
|
||||
|
||||
interface EvmTransactionBuilder {
|
||||
|
||||
fun nativeTransfer(amount: BalanceOf, recipient: AccountId)
|
||||
|
||||
fun contractCall(contractAddress: String, builder: EvmContractCallBuilder.() -> Unit)
|
||||
|
||||
interface EvmContractCallBuilder {
|
||||
|
||||
var function: String
|
||||
|
||||
fun <T> inputParameter(value: EvmType<T>)
|
||||
|
||||
fun <T : EvmType<*>> outputParameter(typeReference: TypeReference<T>)
|
||||
}
|
||||
}
|
||||
|
||||
fun EvmTransactionBuilder() = RealEvmTransactionBuilder()
|
||||
|
||||
inline fun <reified T : EvmType<*>> EvmTransactionBuilder.EvmContractCallBuilder.outputParameter() {
|
||||
val typeReference = object : TypeReference<T>() {}
|
||||
|
||||
outputParameter(typeReference)
|
||||
}
|
||||
|
||||
fun <T> EvmTransactionBuilder.contractCall(
|
||||
contractAddress: String,
|
||||
contractStandard: ContractStandard<*, T>,
|
||||
contractStandardAction: T.() -> Unit
|
||||
) {
|
||||
val contractTransactions = contractStandard.transact(contractAddress, this)
|
||||
|
||||
contractTransactions.contractStandardAction()
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package io.novafoundation.nova.runtime.ethereum.transaction.builder
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
|
||||
import io.novafoundation.nova.common.utils.ethereumAccountIdToAddress
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import org.web3j.abi.FunctionEncoder
|
||||
import org.web3j.abi.TypeReference
|
||||
import org.web3j.abi.datatypes.Type
|
||||
import org.web3j.crypto.RawTransaction
|
||||
import org.web3j.protocol.core.methods.request.Transaction
|
||||
import java.math.BigInteger
|
||||
import org.web3j.abi.datatypes.Function as EvmFunction
|
||||
|
||||
class RealEvmTransactionBuilder : EvmTransactionBuilder {
|
||||
|
||||
private var transactionData: EvmTransactionData? = null
|
||||
|
||||
override fun nativeTransfer(amount: BalanceOf, recipient: AccountId) {
|
||||
transactionData = EvmTransactionData.NativeTransfer(amount, recipient.ethereumAccountIdToAddress())
|
||||
}
|
||||
|
||||
override fun contractCall(contractAddress: String, builder: EvmTransactionBuilder.EvmContractCallBuilder.() -> Unit) {
|
||||
transactionData = EvmContractCallBuilder(contractAddress)
|
||||
.apply(builder)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun buildForFee(originAddress: String): Transaction {
|
||||
return when (val txData = requireNotNull(transactionData)) {
|
||||
is EvmTransactionData.ContractCall -> {
|
||||
val data = FunctionEncoder.encode(txData.function)
|
||||
|
||||
Transaction.createFunctionCallTransaction(
|
||||
originAddress,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
txData.contractAddress,
|
||||
null,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
is EvmTransactionData.NativeTransfer -> {
|
||||
Transaction.createEtherTransaction(
|
||||
originAddress,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
txData.recipientAddress,
|
||||
txData.amount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildForSign(
|
||||
nonce: BigInteger,
|
||||
gasPrice: BigInteger,
|
||||
gasLimit: BigInteger
|
||||
): RawTransaction {
|
||||
return when (val txData = requireNotNull(transactionData)) {
|
||||
is EvmTransactionData.ContractCall -> {
|
||||
val data = FunctionEncoder.encode(txData.function)
|
||||
|
||||
RawTransaction.createTransaction(
|
||||
nonce,
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
txData.contractAddress,
|
||||
null,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
is EvmTransactionData.NativeTransfer -> {
|
||||
RawTransaction.createEtherTransaction(
|
||||
nonce,
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
txData.recipientAddress,
|
||||
txData.amount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EvmContractCallBuilder(
|
||||
private val contractAddress: String
|
||||
) : EvmTransactionBuilder.EvmContractCallBuilder {
|
||||
|
||||
private var _function: String? = null
|
||||
private var input: MutableList<Type<*>> = mutableListOf()
|
||||
private var outputParameters: MutableList<TypeReference<*>> = mutableListOf()
|
||||
|
||||
override var function: String
|
||||
get() = requireNotNull(_function)
|
||||
set(value) {
|
||||
_function = value
|
||||
}
|
||||
|
||||
override fun <T> inputParameter(value: Type<T>) {
|
||||
input += value
|
||||
}
|
||||
|
||||
override fun <T : Type<*>> outputParameter(typeReference: TypeReference<T>) {
|
||||
outputParameters += typeReference
|
||||
}
|
||||
|
||||
fun build(): EvmTransactionData.ContractCall {
|
||||
return EvmTransactionData.ContractCall(
|
||||
contractAddress = contractAddress,
|
||||
function = EvmFunction(function, input, outputParameters)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EvmTransactionData {
|
||||
|
||||
class NativeTransfer(val amount: BalanceOf, val recipientAddress: String) : EvmTransactionData()
|
||||
|
||||
class ContractCall(val contractAddress: String, val function: EvmFunction) : EvmTransactionData()
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.novafoundation.nova.runtime.explorer
|
||||
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
|
||||
class BlockExplorerLinks(
|
||||
val account: String?,
|
||||
val event: String?,
|
||||
val extrinsic: String?
|
||||
)
|
||||
|
||||
interface BlockExplorerLinkFormatter {
|
||||
|
||||
fun format(link: String): BlockExplorerLinks?
|
||||
}
|
||||
|
||||
class CommonBlockExplorerLinkFormatter(
|
||||
private val formatters: List<BlockExplorerLinkFormatter>
|
||||
) : BlockExplorerLinkFormatter {
|
||||
|
||||
override fun format(link: String): BlockExplorerLinks? {
|
||||
return formatters.firstNotNullOfOrNull { it.format(link) }
|
||||
}
|
||||
}
|
||||
|
||||
class SubscanBlockExplorerLinkFormatter() : BlockExplorerLinkFormatter {
|
||||
|
||||
override fun format(link: String): BlockExplorerLinks? {
|
||||
return try {
|
||||
require(link.contains("subscan.io"))
|
||||
|
||||
val normalizedUrl = Urls.normalizeUrl(link)
|
||||
return BlockExplorerLinks(
|
||||
account = "$normalizedUrl/account/{address}",
|
||||
event = null,
|
||||
extrinsic = "$normalizedUrl/account/{hash}",
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StatescanBlockExplorerLinkFormatter() : BlockExplorerLinkFormatter {
|
||||
|
||||
override fun format(link: String): BlockExplorerLinks? {
|
||||
return try {
|
||||
require(link.contains("statescan.io"))
|
||||
|
||||
val normalizedUrl = Urls.normalizeUrl(link)
|
||||
return BlockExplorerLinks(
|
||||
account = "$normalizedUrl/#/accounts/{address}",
|
||||
event = "$normalizedUrl/#/events/{event}",
|
||||
extrinsic = "$normalizedUrl/#/extrinsics/{hash}",
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EtherscanBlockExplorerLinkFormatter() : BlockExplorerLinkFormatter {
|
||||
|
||||
override fun format(link: String): BlockExplorerLinks? {
|
||||
return try {
|
||||
require(link.contains("etherscan.io"))
|
||||
|
||||
val normalizedUrl = Urls.normalizeUrl(link)
|
||||
return BlockExplorerLinks(
|
||||
account = "$normalizedUrl/address/{address}",
|
||||
event = null,
|
||||
extrinsic = "$normalizedUrl/tx/{hash}",
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
package io.novafoundation.nova.runtime.ext
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.address.format.AddressScheme
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.MultiAddress
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindOrNull
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.TokenSymbol
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.common.utils.asTokenSymbol
|
||||
import io.novafoundation.nova.common.utils.emptyEthereumAccountId
|
||||
import io.novafoundation.nova.common.utils.emptySubstrateAccountId
|
||||
import io.novafoundation.nova.common.utils.findIsInstanceOrNull
|
||||
import io.novafoundation.nova.common.utils.formatNamed
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
import io.novafoundation.nova.common.utils.substrateAccountId
|
||||
import io.novafoundation.nova.core_db.model.AssetAndChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.ALEPH_ZERO
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.MYTHOS
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.NOMINATION_POOLS
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.PARACHAIN
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.RELAYCHAIN
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.RELAYCHAIN_AURA
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.TURING
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.UNSUPPORTED
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.Type
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ExplorerTemplateExtractor
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.NetworkType
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.StatemineAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.TypesUsage
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.hasSameId
|
||||
import io.novasama.substrate_sdk_android.encrypt.SignatureVerifier
|
||||
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
|
||||
import io.novasama.substrate_sdk_android.encrypt.Signer
|
||||
import io.novasama.substrate_sdk_android.extensions.asEthereumAccountId
|
||||
import io.novasama.substrate_sdk_android.extensions.asEthereumAddress
|
||||
import io.novasama.substrate_sdk_android.extensions.asEthereumPublicKey
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import io.novasama.substrate_sdk_android.extensions.isValid
|
||||
import io.novasama.substrate_sdk_android.extensions.requireHexPrefix
|
||||
import io.novasama.substrate_sdk_android.extensions.toAccountId
|
||||
import io.novasama.substrate_sdk_android.extensions.toAddress
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.toHexUntyped
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.addressPrefix
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAddress
|
||||
import java.math.BigInteger
|
||||
|
||||
const val EVM_DEFAULT_TOKEN_DECIMALS = 18
|
||||
|
||||
private const val EIP_155_PREFIX = "eip155"
|
||||
|
||||
val Chain.autoBalanceEnabled: Boolean
|
||||
get() = nodes.wssNodeSelectionStrategy is Chain.Nodes.NodeSelectionStrategy.AutoBalance
|
||||
|
||||
val Chain.selectedUnformattedWssNodeUrlOrNull: String?
|
||||
get() = if (nodes.wssNodeSelectionStrategy is Chain.Nodes.NodeSelectionStrategy.SelectedNode) {
|
||||
nodes.wssNodeSelectionStrategy.unformattedNodeUrl
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val Chain.isCustomNetwork: Boolean
|
||||
get() = source == Chain.Source.CUSTOM
|
||||
|
||||
val Chain.typesUsage: TypesUsage
|
||||
get() = when {
|
||||
types == null -> TypesUsage.NONE
|
||||
!types.overridesCommon && types.url != null -> TypesUsage.BOTH
|
||||
!types.overridesCommon && types.url == null -> TypesUsage.BASE
|
||||
else -> TypesUsage.OWN
|
||||
}
|
||||
|
||||
val TypesUsage.requiresBaseTypes: Boolean
|
||||
get() = this == TypesUsage.BASE || this == TypesUsage.BOTH
|
||||
|
||||
val Chain.utilityAsset
|
||||
get() = assets.first(Chain.Asset::isUtilityAsset)
|
||||
|
||||
val Chain.isSubstrateBased
|
||||
get() = !isEthereumBased
|
||||
|
||||
val Chain.commissionAsset
|
||||
get() = utilityAsset
|
||||
|
||||
val Chain.isEnabled
|
||||
get() = connectionState != Chain.ConnectionState.DISABLED
|
||||
|
||||
val Chain.isDisabled
|
||||
get() = !isEnabled
|
||||
|
||||
fun Chain.getAssetOrThrow(assetId: ChainAssetId): Chain.Asset {
|
||||
return assetsById.getValue(assetId)
|
||||
}
|
||||
|
||||
fun Chain.Asset.supportedStakingOptions(): List<Chain.Asset.StakingType> {
|
||||
if (staking.isEmpty()) return emptyList()
|
||||
|
||||
return staking.filter { it != UNSUPPORTED }
|
||||
}
|
||||
|
||||
fun Chain.networkType(): NetworkType {
|
||||
return if (hasSubstrateRuntime) {
|
||||
NetworkType.SUBSTRATE
|
||||
} else {
|
||||
NetworkType.EVM
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.evmChainIdOrNull(): BigInteger? {
|
||||
return if (id.startsWith(EIP_155_PREFIX)) {
|
||||
id.removePrefix("$EIP_155_PREFIX:")
|
||||
.toBigIntegerOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.isSwapSupported(): Boolean = swap.isNotEmpty()
|
||||
|
||||
fun List<Chain.Swap>.assetConversionSupported(): Boolean {
|
||||
return Chain.Swap.ASSET_CONVERSION in this
|
||||
}
|
||||
|
||||
fun List<Chain.Swap>.hydraDxSupported(): Boolean {
|
||||
return Chain.Swap.HYDRA_DX in this
|
||||
}
|
||||
|
||||
val Chain.ConnectionState.isFullSync: Boolean
|
||||
get() = this == Chain.ConnectionState.FULL_SYNC
|
||||
|
||||
val Chain.ConnectionState.isDisabled: Boolean
|
||||
get() = this == Chain.ConnectionState.DISABLED
|
||||
|
||||
val Chain.ConnectionState.level: Int
|
||||
get() = when (this) {
|
||||
Chain.ConnectionState.FULL_SYNC -> 2
|
||||
Chain.ConnectionState.LIGHT_SYNC -> 1
|
||||
Chain.ConnectionState.DISABLED -> 0
|
||||
}
|
||||
|
||||
fun Chain.Additional?.relaychainAsNative(): Boolean {
|
||||
return this?.relaychainAsNative ?: false
|
||||
}
|
||||
|
||||
fun Chain.Additional?.feeViaRuntimeCall(): Boolean {
|
||||
return this?.feeViaRuntimeCall ?: false
|
||||
}
|
||||
|
||||
fun Chain.Additional?.isGenericLedgerAppSupported(): Boolean {
|
||||
return this?.supportLedgerGenericApp ?: false
|
||||
}
|
||||
|
||||
fun Chain.Additional?.shouldDisableMetadataHashCheck(): Boolean {
|
||||
return this?.disabledCheckMetadataHash ?: false
|
||||
}
|
||||
|
||||
fun ChainId.chainIdHexPrefix16(): String {
|
||||
return removeHexPrefix()
|
||||
.take(32)
|
||||
.requireHexPrefix()
|
||||
}
|
||||
|
||||
enum class StakingTypeGroup {
|
||||
|
||||
RELAYCHAIN, PARACHAIN, NOMINATION_POOL, MYTHOS, UNSUPPORTED
|
||||
}
|
||||
|
||||
fun Chain.Asset.StakingType.group(): StakingTypeGroup {
|
||||
return when (this) {
|
||||
UNSUPPORTED -> StakingTypeGroup.UNSUPPORTED
|
||||
RELAYCHAIN, RELAYCHAIN_AURA, ALEPH_ZERO -> StakingTypeGroup.RELAYCHAIN
|
||||
PARACHAIN, TURING -> StakingTypeGroup.PARACHAIN
|
||||
MYTHOS -> StakingTypeGroup.MYTHOS
|
||||
NOMINATION_POOLS -> StakingTypeGroup.NOMINATION_POOL
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.Asset.StakingType.isDirectStaking(): Boolean {
|
||||
return when (group()) {
|
||||
StakingTypeGroup.RELAYCHAIN, StakingTypeGroup.PARACHAIN -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.Asset.StakingType.isPoolStaking(): Boolean {
|
||||
return group() == StakingTypeGroup.NOMINATION_POOL
|
||||
}
|
||||
|
||||
inline fun <reified T : Chain.ExternalApi> Chain.allExternalApis(): List<T> {
|
||||
return externalApis.filterIsInstance<T>()
|
||||
}
|
||||
|
||||
inline fun <reified T : Chain.ExternalApi> Chain.externalApi(): T? {
|
||||
return externalApis.findIsInstanceOrNull<T>()
|
||||
}
|
||||
|
||||
inline fun <reified T : Chain.ExternalApi> Chain.hasExternalApi(): Boolean {
|
||||
return externalApis.any { it is T }
|
||||
}
|
||||
|
||||
const val UTILITY_ASSET_ID = 0
|
||||
|
||||
val Chain.Asset.isUtilityAsset: Boolean
|
||||
get() = id == UTILITY_ASSET_ID
|
||||
|
||||
inline val Chain.Asset.isCommissionAsset: Boolean
|
||||
get() = isUtilityAsset
|
||||
|
||||
inline val FullChainAssetId.isUtility: Boolean
|
||||
get() = assetId == UTILITY_ASSET_ID
|
||||
|
||||
private const val XC_PREFIX = "xc"
|
||||
|
||||
fun Chain.Asset.normalizeSymbol(): String {
|
||||
return normalizeTokenSymbol(this.symbol.value)
|
||||
}
|
||||
|
||||
fun TokenSymbol.normalize(): TokenSymbol {
|
||||
return normalizeTokenSymbol(value).asTokenSymbol()
|
||||
}
|
||||
|
||||
fun normalizeTokenSymbol(symbol: String): String {
|
||||
return symbol.removePrefix(XC_PREFIX)
|
||||
}
|
||||
|
||||
val Chain.Node.isWss: Boolean
|
||||
get() = connectionType == Chain.Node.ConnectionType.WSS
|
||||
|
||||
val Chain.Node.isHttps: Boolean
|
||||
get() = connectionType == Chain.Node.ConnectionType.HTTPS
|
||||
|
||||
fun Chain.Nodes.wssNodes(): List<Chain.Node> {
|
||||
return nodes.filter { it.isWss }
|
||||
}
|
||||
|
||||
fun Chain.Nodes.httpNodes(): List<Chain.Node> {
|
||||
return nodes.filter { it.isHttps }
|
||||
}
|
||||
|
||||
fun Chain.Nodes.hasHttpNodes(): Boolean {
|
||||
return nodes.any { it.isHttps }
|
||||
}
|
||||
|
||||
val Chain.Asset.disabled: Boolean
|
||||
get() = !enabled
|
||||
|
||||
val Chain.genesisHash: String?
|
||||
get() = id.takeIf {
|
||||
runCatching { it.fromHex() }.isSuccess
|
||||
}
|
||||
|
||||
fun Chain.hasOnlyOneAddressFormat() = legacyAddressPrefix == null
|
||||
|
||||
fun Chain.supportsLegacyAddressFormat() = legacyAddressPrefix != null
|
||||
|
||||
fun Chain.requireGenesisHash() = requireNotNull(genesisHash)
|
||||
|
||||
fun Chain.addressOf(accountId: ByteArray): String {
|
||||
return if (isEthereumBased) {
|
||||
accountId.toEthereumAddress()
|
||||
} else {
|
||||
accountId.toAddress(addressPrefix.toShort())
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.addressOf(accountId: AccountIdKey): String {
|
||||
return addressOf(accountId.value)
|
||||
}
|
||||
|
||||
fun Chain.legacyAddressOfOrNull(accountId: ByteArray): String? {
|
||||
return if (isEthereumBased) {
|
||||
null
|
||||
} else {
|
||||
legacyAddressPrefix?.let { accountId.toAddress(it.toShort()) }
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toEthereumAddress(): String {
|
||||
return asEthereumAccountId().toAddress(withChecksum = true).value
|
||||
}
|
||||
|
||||
fun Chain.accountIdOf(address: String): ByteArray {
|
||||
return if (isEthereumBased) {
|
||||
address.asEthereumAddress().toAccountId().value
|
||||
} else {
|
||||
address.toAccountId()
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toAccountId(chain: Chain): ByteArray {
|
||||
return chain.accountIdOf(this)
|
||||
}
|
||||
|
||||
fun String.toAccountIdKey(chain: Chain): AccountIdKey {
|
||||
return chain.accountIdKeyOf(this)
|
||||
}
|
||||
|
||||
fun Chain.accountIdKeyOf(address: String): AccountIdKey {
|
||||
return accountIdOf(address).intoKey()
|
||||
}
|
||||
|
||||
fun String.anyAddressToAccountId(): ByteArray {
|
||||
return runCatching {
|
||||
// Substrate
|
||||
toAccountId()
|
||||
}.recoverCatching {
|
||||
// Evm
|
||||
asEthereumAddress().toAccountId().value
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
fun Chain.accountIdOrNull(address: String): ByteArray? {
|
||||
return runCatching { accountIdOf(address) }.getOrNull()
|
||||
}
|
||||
|
||||
fun Chain.emptyAccountId() = if (isEthereumBased) {
|
||||
emptyEthereumAccountId()
|
||||
} else {
|
||||
emptySubstrateAccountId()
|
||||
}
|
||||
|
||||
fun Chain.emptyAccountIdKey() = emptyAccountId().intoKey()
|
||||
|
||||
fun Chain.accountIdOrDefault(maybeAddress: String): ByteArray {
|
||||
return accountIdOrNull(maybeAddress) ?: emptyAccountId()
|
||||
}
|
||||
|
||||
fun Chain.accountIdOf(publicKey: ByteArray): ByteArray {
|
||||
return if (isEthereumBased) {
|
||||
publicKey.asEthereumPublicKey().toAccountId().value
|
||||
} else {
|
||||
publicKey.substrateAccountId()
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.hexAccountIdOf(address: String): String {
|
||||
return accountIdOf(address).toHexString()
|
||||
}
|
||||
|
||||
fun Chain.multiAddressOf(accountId: ByteArray): MultiAddress {
|
||||
return if (isEthereumBased) {
|
||||
MultiAddress.Address20(accountId)
|
||||
} else {
|
||||
MultiAddress.Id(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.isValidAddress(address: String): Boolean {
|
||||
return runCatching {
|
||||
if (isEthereumBased) {
|
||||
address.asEthereumAddress().isValid()
|
||||
} else {
|
||||
address.toAccountId() // verify supplied address can be converted to account id
|
||||
|
||||
addressPrefix.toShort() == address.addressPrefix() ||
|
||||
legacyAddressPrefix?.toShort() == address.addressPrefix()
|
||||
}
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
fun Chain.isValidEvmAddress(address: String): Boolean {
|
||||
return runCatching {
|
||||
if (isEthereumBased) {
|
||||
address.asEthereumAddress().isValid()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
val Chain.isParachain
|
||||
get() = parentId != null
|
||||
|
||||
fun Chain.multiAddressOf(address: String): MultiAddress = multiAddressOf(accountIdOf(address))
|
||||
|
||||
fun Chain.availableExplorersFor(field: ExplorerTemplateExtractor) = explorers.filter { field(it) != null }
|
||||
|
||||
fun Chain.Explorer.accountUrlOf(address: String): String {
|
||||
return format(Chain.Explorer::account, "address", address)
|
||||
}
|
||||
|
||||
fun Chain.Explorer.extrinsicUrlOf(extrinsicHash: String): String {
|
||||
return format(Chain.Explorer::extrinsic, "hash", extrinsicHash)
|
||||
}
|
||||
|
||||
fun Chain.Explorer.eventUrlOf(eventId: String): String {
|
||||
return format(Chain.Explorer::event, "event", eventId)
|
||||
}
|
||||
|
||||
private inline fun Chain.Explorer.format(
|
||||
templateExtractor: ExplorerTemplateExtractor,
|
||||
argumentName: String,
|
||||
argumentValue: String,
|
||||
): String {
|
||||
val template = templateExtractor(this) ?: throw Exception("Cannot find template in the chain explorer: $name")
|
||||
|
||||
return template.formatNamed(argumentName to argumentValue)
|
||||
}
|
||||
|
||||
object ChainGeneses {
|
||||
|
||||
// Pezkuwi chains (priority)
|
||||
const val PEZKUWI = "bb4a61ab0c4b8c12f5eab71d0c86c482e03a275ecdafee678dea712474d33d75"
|
||||
const val PEZKUWI_ASSET_HUB = "00d0e1d0581c3cd5c5768652d52f4520184018b44f56a2ae1e0dc9d65c00c948"
|
||||
const val PEZKUWI_PEOPLE = "58269e9c184f721e0309332d90cafc410df1519a5dc27a5fd9b3bf5fd2d129f8"
|
||||
|
||||
const val KUSAMA = "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe"
|
||||
const val POLKADOT = "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"
|
||||
|
||||
// Westend constant now points to Zagros Testnet
|
||||
const val WESTEND = "96eb58af1bb7288115b5e4ff1590422533e749293f231974536dc6672417d06f"
|
||||
|
||||
const val KUSAMA_ASSET_HUB = "48239ef607d7928874027a43a67689209727dfb3d3dc5e5b03a39bdc2eda771a"
|
||||
|
||||
const val ACALA = "fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c"
|
||||
|
||||
const val ROCOCO_ACALA = "a84b46a3e602245284bb9a72c4abd58ee979aa7a5d7f8c4dfdddfaaf0665a4ae"
|
||||
|
||||
const val STATEMINT = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"
|
||||
const val EDGEWARE = "742a2ca70c2fda6cee4f8df98d64c4c670a052d9568058982dad9d5a7a135c5b"
|
||||
|
||||
const val KARURA = "baf5aabe40646d11f0ee8abbdc64f4a4b7674925cba08e4a05ff9ebed6e2126b"
|
||||
|
||||
const val NODLE_PARACHAIN = "97da7ede98d7bad4e36b4d734b6055425a3be036da2a332ea5a7037656427a21"
|
||||
|
||||
const val MOONBEAM = "fe58ea77779b7abda7da4ec526d14db9b1e9cd40a217c34892af80a9b332b76d"
|
||||
const val MOONRIVER = "401a1f9dca3da46f5c4091016c8a2f26dcea05865116b286f60f668207d1474b"
|
||||
|
||||
const val POLYMESH = "6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063"
|
||||
|
||||
const val XX_NETWORK = "50dd5d206917bf10502c68fb4d18a59fc8aa31586f4e8856b493e43544aa82aa"
|
||||
|
||||
const val KILT = "411f057b9107718c9624d6aa4a3f23c1653898297f3d4d529d9bb6511a39dd21"
|
||||
|
||||
const val ASTAR = "9eb76c5184c4ab8679d2d5d819fdf90b9c001403e9e17da2e14b6d8aec4029c6"
|
||||
|
||||
const val ALEPH_ZERO = "70255b4d28de0fc4e1a193d7e175ad1ccef431598211c55538f1018651a0344e"
|
||||
const val TERNOA = "6859c81ca95ef624c9dfe4dc6e3381c33e5d6509e35e147092bfbc780f777c4e"
|
||||
|
||||
const val POLIMEC = "7eb9354488318e7549c722669dcbdcdc526f1fef1420e7944667212f3601fdbd"
|
||||
|
||||
const val POLKADEX = "3920bcb4960a1eef5580cd5367ff3f430eef052774f78468852f7b9cb39f8a3c"
|
||||
|
||||
const val CALAMARI = "4ac80c99289841dd946ef92765bf659a307d39189b3ce374a92b5f0415ee17a1"
|
||||
|
||||
const val TURING = "0f62b701fb12d02237a33b84818c11f621653d2b1614c777973babf4652b535d"
|
||||
|
||||
const val ZEITGEIST = "1bf2a2ecb4a868de66ea8610f2ce7c8c43706561b6476031315f6640fe38e060"
|
||||
|
||||
const val WESTMINT = "67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9"
|
||||
|
||||
const val HYDRA_DX = "afdc188f45c71dacbaa0b62e16a91f726c7b8699a9748cdf715459de6b7f366d"
|
||||
|
||||
const val AVAIL_TURING_TESTNET = "d3d2f3a3495dc597434a99d7d449ebad6616db45e4e4f178f31cc6fa14378b70"
|
||||
const val AVAIL = "b91746b45e0346cc2f815a520b9c6cb4d5c0902af848db0a80f85932d2e8276a"
|
||||
|
||||
const val VARA = "fe1b4c55fd4d668101126434206571a7838a8b6b93a6d1b95d607e78e6c53763"
|
||||
|
||||
const val POLKADOT_ASSET_HUB = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f"
|
||||
|
||||
const val UNIQUE_NETWORK = "84322d9cddbf35088f1e54e9a85c967a41a56a4f43445768125e61af166c7d31"
|
||||
|
||||
const val POLKADOT_PEOPLE = "67fa177a097bfa18f77ea95ab56e9bcdfeb0e5b8a40e46298bb93e16b6fc5008"
|
||||
const val KUSAMA_PEOPLE = "c1af4cb4eb3918e5db15086c0cc5ec17fb334f728b7c65dd44bfe1e174ff8b3f"
|
||||
}
|
||||
|
||||
object ChainIds {
|
||||
|
||||
const val ETHEREUM = "$EIP_155_PREFIX:1"
|
||||
|
||||
const val MOONBEAM = ChainGeneses.MOONBEAM
|
||||
const val MOONRIVER = ChainGeneses.MOONRIVER
|
||||
}
|
||||
|
||||
val Chain.Companion.Geneses
|
||||
get() = ChainGeneses
|
||||
|
||||
val Chain.Companion.Ids
|
||||
get() = ChainIds
|
||||
|
||||
fun Chain.Asset.requireStatemine(): Type.Statemine {
|
||||
require(type is Type.Statemine)
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
fun Chain.findStatemineAssets(): List<Chain.Asset> {
|
||||
return assets.filter { it.type is Type.Statemine }
|
||||
}
|
||||
|
||||
fun Chain.Asset.statemineOrNull(): Type.Statemine? {
|
||||
return type as? Type.Statemine
|
||||
}
|
||||
|
||||
fun Type.Statemine.palletNameOrDefault(): String {
|
||||
return palletName ?: Modules.ASSETS
|
||||
}
|
||||
|
||||
fun Chain.Asset.requireOrml(): Type.Orml {
|
||||
require(type is Type.Orml)
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
val Chain.addressScheme: AddressScheme
|
||||
get() = if (isEthereumBased) AddressScheme.EVM else AddressScheme.SUBSTRATE
|
||||
|
||||
fun Chain.Asset.ormlOrNull(): Type.Orml? {
|
||||
return type as? Type.Orml
|
||||
}
|
||||
|
||||
fun Chain.Asset.requireErc20(): Type.EvmErc20 {
|
||||
require(type is Type.EvmErc20)
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
fun Chain.Asset.requireEquilibrium(): Type.Equilibrium {
|
||||
require(type is Type.Equilibrium)
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
fun Chain.Asset.ormlCurrencyId(runtime: RuntimeSnapshot): Any? {
|
||||
return requireOrml().currencyId(runtime)
|
||||
}
|
||||
|
||||
fun Type.Orml.currencyId(runtime: RuntimeSnapshot): Any? {
|
||||
val currencyIdType = runtime.typeRegistry[currencyIdType]
|
||||
?: error("Cannot find type $currencyIdType")
|
||||
|
||||
return currencyIdType.fromHex(runtime, currencyIdScale)
|
||||
}
|
||||
|
||||
val Chain.Asset.fullId: FullChainAssetId
|
||||
get() = FullChainAssetId(chainId, id)
|
||||
|
||||
fun Chain.enabledAssets(): List<Chain.Asset> = assets.filter { it.enabled }
|
||||
|
||||
fun Chain.disabledAssets(): List<Chain.Asset> = assets.filterNot { it.enabled }
|
||||
|
||||
fun evmChainIdFrom(chainId: Int) = "$EIP_155_PREFIX:$chainId"
|
||||
|
||||
fun evmChainIdFrom(chainId: BigInteger) = "$EIP_155_PREFIX:$chainId"
|
||||
|
||||
fun Chain.findAssetByOrmlCurrencyId(runtime: RuntimeSnapshot, currencyId: Any?): Chain.Asset? {
|
||||
return assets.find { asset ->
|
||||
if (asset.type !is Type.Orml) return@find false
|
||||
val currencyType = runtime.typeRegistry[asset.type.currencyIdType] ?: return@find false
|
||||
|
||||
val currencyIdScale = bindOrNull { currencyType.toHexUntyped(runtime, currencyId) } ?: return@find false
|
||||
|
||||
currencyIdScale == asset.type.currencyIdScale
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.findAssetByStatemineAssetId(runtime: RuntimeSnapshot, assetId: Any?): Chain.Asset? {
|
||||
return assets.find { asset ->
|
||||
if (asset.type !is Type.Statemine) return@find false
|
||||
|
||||
asset.type.hasSameId(runtime, assetId)
|
||||
}
|
||||
}
|
||||
|
||||
fun Type.Orml.decodeOrNull(runtime: RuntimeSnapshot): Any? {
|
||||
val currencyType = runtime.typeRegistry[currencyIdType] ?: return null
|
||||
return currencyType.fromHexOrNull(runtime, currencyIdScale)
|
||||
}
|
||||
|
||||
val Chain.Asset.localId: AssetAndChainId
|
||||
get() = AssetAndChainId(chainId, id)
|
||||
|
||||
val Chain.Asset.onChainAssetId: String?
|
||||
get() = when (this.type) {
|
||||
is Type.Equilibrium -> this.type.toString()
|
||||
is Type.Orml -> this.type.currencyIdScale
|
||||
is Type.Statemine -> this.type.id.onChainAssetId()
|
||||
is Type.EvmErc20 -> this.type.contractAddress
|
||||
is Type.Native -> null
|
||||
is Type.EvmNative -> null
|
||||
Type.Unsupported -> error("Unsupported assetId type: ${this.type::class.simpleName}")
|
||||
}
|
||||
|
||||
fun StatemineAssetId.onChainAssetId(): String {
|
||||
return when (this) {
|
||||
is StatemineAssetId.Number -> value.toString()
|
||||
is StatemineAssetId.ScaleEncoded -> scaleHex
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.openGovIfSupported(): Chain.Governance? {
|
||||
return Chain.Governance.V2.takeIf { it in governance }
|
||||
}
|
||||
|
||||
fun Chain.Explorer.normalizedUrl(): String? {
|
||||
val url = listOfNotNull(extrinsic, account, event).firstOrNull()
|
||||
return url?.let { Urls.normalizeUrl(it) }
|
||||
}
|
||||
|
||||
fun Chain.supportTinderGov(): Boolean {
|
||||
return hasReferendaSummaryApi()
|
||||
}
|
||||
|
||||
fun Chain.hasReferendaSummaryApi(): Boolean {
|
||||
return externalApi<Chain.ExternalApi.ReferendumSummary>() != null
|
||||
}
|
||||
|
||||
fun Chain.summaryApiOrNull(): Chain.ExternalApi.ReferendumSummary? {
|
||||
return externalApi<Chain.ExternalApi.ReferendumSummary>()
|
||||
}
|
||||
|
||||
fun Chain.timelineChainId(): ChainId? {
|
||||
return additional?.timelineChain
|
||||
}
|
||||
|
||||
fun Chain.timelineChainIdOrSelf(): ChainId {
|
||||
return timelineChainId() ?: id
|
||||
}
|
||||
|
||||
fun Chain.hasTimelineChain(): Boolean {
|
||||
return additional?.timelineChain != null
|
||||
}
|
||||
|
||||
fun FullChainAssetId.Companion.utilityAssetOf(chainId: ChainId) = FullChainAssetId(chainId, UTILITY_ASSET_ID)
|
||||
|
||||
fun SignatureVerifier.verifyMultiChain(
|
||||
chain: Chain,
|
||||
signature: SignatureWrapper,
|
||||
message: ByteArray,
|
||||
publicKey: ByteArray
|
||||
): Boolean {
|
||||
return if (chain.isEthereumBased) {
|
||||
verify(signature, Signer.MessageHashing.ETHEREUM, message, publicKey)
|
||||
} else {
|
||||
verify(signature, Signer.MessageHashing.SUBSTRATE, message, publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this chain is part of the Pezkuwi ecosystem.
|
||||
* Pezkuwi chains use "bizinikiwi" signing context instead of "substrate".
|
||||
*/
|
||||
val Chain.isPezkuwiChain: Boolean
|
||||
get() = id in PEZKUWI_CHAIN_IDS
|
||||
|
||||
private val PEZKUWI_CHAIN_IDS = setOf(
|
||||
ChainGeneses.PEZKUWI,
|
||||
ChainGeneses.PEZKUWI_ASSET_HUB,
|
||||
ChainGeneses.PEZKUWI_PEOPLE
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.runtime.ext
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
val Chain.mainChainsFirstAscendingOrder
|
||||
get() = when (genesisHash) {
|
||||
// Pezkuwi ecosystem first
|
||||
Chain.Geneses.PEZKUWI -> 0
|
||||
Chain.Geneses.PEZKUWI_ASSET_HUB -> 1
|
||||
Chain.Geneses.PEZKUWI_PEOPLE -> 2
|
||||
// Then Polkadot ecosystem
|
||||
Chain.Geneses.POLKADOT -> 3
|
||||
Chain.Geneses.POLKADOT_ASSET_HUB -> 4
|
||||
// Then Kusama ecosystem
|
||||
Chain.Geneses.KUSAMA -> 5
|
||||
Chain.Geneses.KUSAMA_ASSET_HUB -> 6
|
||||
// Everything else
|
||||
else -> 7
|
||||
}
|
||||
|
||||
val Chain.testnetsLastAscendingOrder
|
||||
get() = if (isTestNet) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
val Chain.alphabeticalOrder
|
||||
get() = name
|
||||
|
||||
fun <K> Chain.Companion.defaultComparatorFrom(extractor: (K) -> Chain): Comparator<K> = Comparator.comparing(extractor, defaultComparator())
|
||||
|
||||
fun Chain.Companion.defaultComparator(): Comparator<Chain> = compareBy<Chain> { it.mainChainsFirstAscendingOrder }
|
||||
.thenBy { it.testnetsLastAscendingOrder }
|
||||
.thenBy { it.alphabeticalOrder }
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.runtime.ext
|
||||
|
||||
import io.novafoundation.nova.common.utils.TokenSymbol
|
||||
|
||||
val TokenSymbol.mainTokensFirstAscendingOrder
|
||||
get() = when (this.value) {
|
||||
"HEZ" -> 0
|
||||
"PEZ" -> 1
|
||||
"USDT" -> 2
|
||||
"DOT" -> 3
|
||||
"KSM" -> 4
|
||||
"USDC" -> 5
|
||||
else -> 6
|
||||
}
|
||||
|
||||
val TokenSymbol.alphabeticalOrder
|
||||
get() = value
|
||||
|
||||
fun <K> TokenSymbol.Companion.defaultComparatorFrom(extractor: (K) -> TokenSymbol): Comparator<K> = Comparator.comparing(extractor, defaultComparator())
|
||||
|
||||
fun TokenSymbol.Companion.defaultComparator(): Comparator<TokenSymbol> = compareBy<TokenSymbol> { it.mainTokensFirstAscendingOrder }
|
||||
.thenBy { it.alphabeticalOrder }
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.AuthorizeCall
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.ChargeAssetTxPayment
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.CheckAppId
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.CheckNonZeroSender
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.CheckWeight
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.WeightReclaim
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.TransactionExtension
|
||||
|
||||
private const val TAG = "CustomTxExtensions"
|
||||
|
||||
object CustomTransactionExtensions {
|
||||
|
||||
fun applyDefaultValues(builder: ExtrinsicBuilder) {
|
||||
defaultValues().forEach(builder::setTransactionExtension)
|
||||
}
|
||||
|
||||
fun applyDefaultValues(builder: ExtrinsicBuilder, runtime: RuntimeSnapshot) {
|
||||
defaultValues(runtime).forEach(builder::setTransactionExtension)
|
||||
}
|
||||
|
||||
fun defaultValues(): List<TransactionExtension> {
|
||||
return listOf(
|
||||
ChargeAssetTxPayment(),
|
||||
CheckAppId()
|
||||
)
|
||||
}
|
||||
|
||||
fun defaultValues(runtime: RuntimeSnapshot): List<TransactionExtension> {
|
||||
val extensions = mutableListOf<TransactionExtension>()
|
||||
val signedExtIds = runtime.metadata.extrinsic.signedExtensions.map { it.id }
|
||||
|
||||
Log.d(TAG, "Metadata signed extensions: $signedExtIds")
|
||||
|
||||
// Add extensions based on what the metadata requires
|
||||
if ("AuthorizeCall" in signedExtIds) {
|
||||
extensions.add(AuthorizeCall())
|
||||
}
|
||||
if ("CheckNonZeroSender" in signedExtIds) {
|
||||
extensions.add(CheckNonZeroSender())
|
||||
}
|
||||
if ("CheckWeight" in signedExtIds) {
|
||||
extensions.add(CheckWeight())
|
||||
}
|
||||
if ("WeightReclaim" in signedExtIds || "StorageWeightReclaim" in signedExtIds) {
|
||||
extensions.add(WeightReclaim())
|
||||
}
|
||||
if ("ChargeAssetTxPayment" in signedExtIds) {
|
||||
// Default to native fee payment (null assetId)
|
||||
extensions.add(ChargeAssetTxPayment())
|
||||
}
|
||||
if ("CheckAppId" in signedExtIds) {
|
||||
extensions.add(CheckAppId())
|
||||
}
|
||||
|
||||
Log.d(TAG, "Extensions to add: ${extensions.map { it.name }}")
|
||||
return extensions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.call
|
||||
|
||||
fun ExtrinsicBuilder.systemRemark(remark: ByteArray): ExtrinsicBuilder {
|
||||
return call(
|
||||
moduleName = "System",
|
||||
callName = "remark",
|
||||
arguments = mapOf(
|
||||
"remark" to remark
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun ExtrinsicBuilder.systemRemarkWithEvent(remark: ByteArray): ExtrinsicBuilder {
|
||||
return call(
|
||||
moduleName = "System",
|
||||
callName = "remark_with_event",
|
||||
arguments = mapOf(
|
||||
"remark" to remark
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun ExtrinsicBuilder.systemRemarkWithEvent(remark: String): ExtrinsicBuilder {
|
||||
return systemRemarkWithEvent(remark.encodeToByteArray())
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.runtime.ext.requireGenesisHash
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.PezkuwiCheckMortality
|
||||
import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.BatchMode
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.ExtrinsicVersion
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.ChargeTransactionPayment
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckGenesis
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckMortality
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckSpecVersion
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.CheckTxVersion
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.checkMetadataHash.CheckMetadataHash
|
||||
|
||||
private const val TAG = "ExtrinsicBuilderFactory"
|
||||
|
||||
class ExtrinsicBuilderFactory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val mortalityConstructor: MortalityConstructor,
|
||||
private val metadataShortenerService: MetadataShortenerService,
|
||||
) {
|
||||
|
||||
class Options(
|
||||
val batchMode: BatchMode,
|
||||
)
|
||||
|
||||
suspend fun create(
|
||||
chain: Chain,
|
||||
options: Options,
|
||||
): ExtrinsicBuilder {
|
||||
return createMulti(chain, options).first()
|
||||
}
|
||||
|
||||
suspend fun createMulti(
|
||||
chain: Chain,
|
||||
options: Options,
|
||||
): Sequence<ExtrinsicBuilder> {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
// Log metadata extensions
|
||||
val metadataExtensions = runtime.metadata.extrinsic.signedExtensions.map { it.id }
|
||||
Log.d(TAG, "Chain: ${chain.name}, Metadata extensions: $metadataExtensions")
|
||||
|
||||
val mortality = mortalityConstructor.constructMortality(chain.id)
|
||||
val metadataProof = metadataShortenerService.generateMetadataProof(chain.id)
|
||||
|
||||
// Log custom extensions
|
||||
val customExtensions = CustomTransactionExtensions.defaultValues(runtime).map { it.name }
|
||||
Log.d(TAG, "Custom extensions to add: $customExtensions")
|
||||
|
||||
val isPezkuwi = isPezkuwiChain(runtime)
|
||||
Log.d(TAG, "isPezkuwiChain: $isPezkuwi")
|
||||
|
||||
return generateSequence {
|
||||
ExtrinsicBuilder(
|
||||
runtime = runtime,
|
||||
extrinsicVersion = ExtrinsicVersion.V4,
|
||||
batchMode = options.batchMode,
|
||||
).apply {
|
||||
// Use custom CheckMortality for Pezkuwi chains to avoid type lookup issues
|
||||
if (isPezkuwi) {
|
||||
Log.d(TAG, "Using PezkuwiCheckMortality for ${chain.name}")
|
||||
setTransactionExtension(PezkuwiCheckMortality(mortality.era, mortality.blockHash.fromHex()))
|
||||
} else {
|
||||
setTransactionExtension(CheckMortality(mortality.era, mortality.blockHash.fromHex()))
|
||||
}
|
||||
setTransactionExtension(CheckGenesis(chain.requireGenesisHash().fromHex()))
|
||||
setTransactionExtension(ChargeTransactionPayment(chain.additional?.defaultTip.orZero()))
|
||||
setTransactionExtension(CheckMetadataHash(metadataProof.checkMetadataHash))
|
||||
setTransactionExtension(CheckSpecVersion(metadataProof.usedVersion.specVersion))
|
||||
setTransactionExtension(CheckTxVersion(metadataProof.usedVersion.transactionVersion))
|
||||
|
||||
CustomTransactionExtensions.defaultValues(runtime).forEach(::setTransactionExtension)
|
||||
|
||||
Log.d(TAG, "All extensions set for ${chain.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPezkuwiChain(runtime: RuntimeSnapshot): Boolean {
|
||||
val signedExtIds = runtime.metadata.extrinsic.signedExtensions.map { it.id }
|
||||
return signedExtIds.any { it == "AuthorizeCall" }
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import io.novafoundation.nova.common.utils.ByteArrayHexAdapter
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import java.lang.reflect.Type
|
||||
|
||||
private class GenericCallAdapter : JsonSerializer<GenericCall.Instance> {
|
||||
|
||||
override fun serialize(src: GenericCall.Instance, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
||||
return JsonObject().apply {
|
||||
add("module", JsonPrimitive(src.module.name))
|
||||
add("function", JsonPrimitive(src.function.name))
|
||||
add("args", context.serialize(src.arguments))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ExtrinsicSerializers {
|
||||
|
||||
fun gson() = GsonBuilder()
|
||||
.registerTypeHierarchyAdapter(ByteArray::class.java, ByteArrayHexAdapter())
|
||||
.registerTypeHierarchyAdapter(GenericCall.Instance::class.java, GenericCallAdapter())
|
||||
.setPrettyPrinting()
|
||||
.create()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import io.novasama.substrate_sdk_android.wsrpc.subscription.response.SubscriptionChange
|
||||
|
||||
sealed class ExtrinsicStatus(val extrinsicHash: String, val terminal: Boolean) {
|
||||
|
||||
class Ready(extrinsicHash: String) : ExtrinsicStatus(extrinsicHash, terminal = false)
|
||||
|
||||
class Broadcast(extrinsicHash: String) : ExtrinsicStatus(extrinsicHash, terminal = false)
|
||||
|
||||
class InBlock(val blockHash: String, extrinsicHash: String) : ExtrinsicStatus(extrinsicHash, terminal = false)
|
||||
|
||||
class Finalized(val blockHash: String, extrinsicHash: String) : ExtrinsicStatus(extrinsicHash, terminal = true)
|
||||
|
||||
class Other(extrinsicHash: String) : ExtrinsicStatus(extrinsicHash, terminal = false)
|
||||
}
|
||||
|
||||
private const val STATUS_READY = "ready"
|
||||
private const val STATUS_BROADCAST = "broadcast"
|
||||
private const val STATUS_IN_BLOCK = "inBlock"
|
||||
private const val STATUS_FINALIZED = "finalized"
|
||||
private const val STATUS_FINALITY_TIMEOUT = "finalityTimeout"
|
||||
|
||||
fun SubscriptionChange.asExtrinsicStatus(extrinsicHash: String): ExtrinsicStatus {
|
||||
return when (val result = params.result) {
|
||||
STATUS_READY -> ExtrinsicStatus.Ready(extrinsicHash)
|
||||
is Map<*, *> -> when {
|
||||
STATUS_BROADCAST in result -> ExtrinsicStatus.Broadcast(extrinsicHash)
|
||||
STATUS_IN_BLOCK in result -> ExtrinsicStatus.InBlock(extractBlockHash(result, STATUS_IN_BLOCK), extrinsicHash)
|
||||
STATUS_FINALIZED in result -> ExtrinsicStatus.Finalized(extractBlockHash(result, STATUS_FINALIZED), extrinsicHash)
|
||||
STATUS_FINALITY_TIMEOUT in result -> ExtrinsicStatus.Finalized(extractBlockHash(result, STATUS_FINALITY_TIMEOUT), extrinsicHash)
|
||||
else -> ExtrinsicStatus.Other(extrinsicHash)
|
||||
}
|
||||
|
||||
else -> ExtrinsicStatus.Other(extrinsicHash)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractBlockHash(map: Map<*, *>, key: String): String {
|
||||
return map[key] as? String ?: unknownStructure()
|
||||
}
|
||||
|
||||
private fun unknownStructure(): Nothing = throw IllegalArgumentException("Unknown extrinsic status structure")
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import io.novafoundation.nova.common.utils.formatting.TimerValue
|
||||
import io.novafoundation.nova.common.utils.formatting.remainingTime
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.common.view.startTimer
|
||||
import io.novafoundation.nova.runtime.R
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class ValidityPeriod(val period: TimerValue)
|
||||
|
||||
fun ValidityPeriod.remainingTime(): Long {
|
||||
return period.remainingTime()
|
||||
}
|
||||
|
||||
fun ValidityPeriod.closeToExpire(): Boolean {
|
||||
return remainingTime().milliseconds < 1.minutes
|
||||
}
|
||||
|
||||
fun ValidityPeriod.ended(): Boolean {
|
||||
return remainingTime() == 0L
|
||||
}
|
||||
|
||||
interface ExtrinsicValidityUseCase {
|
||||
|
||||
suspend fun extrinsicValidityPeriod(payload: InheritedImplication): ValidityPeriod
|
||||
}
|
||||
|
||||
internal class RealExtrinsicValidityUseCase(
|
||||
private val mortalityConstructor: MortalityConstructor
|
||||
) : ExtrinsicValidityUseCase {
|
||||
|
||||
override suspend fun extrinsicValidityPeriod(payload: InheritedImplication): ValidityPeriod {
|
||||
// TODO this should calculate remaining time based on Era from payload
|
||||
val timerValue = TimerValue(
|
||||
millis = mortalityConstructor.mortalPeriodMillis(),
|
||||
millisCalculatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
return ValidityPeriod(timerValue)
|
||||
}
|
||||
}
|
||||
|
||||
fun LifecycleOwner.startExtrinsicValidityTimer(
|
||||
validityPeriod: ValidityPeriod,
|
||||
@StringRes timerFormat: Int,
|
||||
timerView: TextView,
|
||||
onTimerFinished: () -> Unit
|
||||
) {
|
||||
timerView.startTimer(
|
||||
value = validityPeriod.period,
|
||||
customMessageFormat = timerFormat,
|
||||
lifecycle = lifecycle,
|
||||
onTick = { view, _ ->
|
||||
val textColorRes = if (validityPeriod.closeToExpire()) R.color.text_negative else R.color.text_secondary
|
||||
|
||||
view.setTextColorRes(textColorRes)
|
||||
},
|
||||
onFinish = { onTimerFinished() }
|
||||
)
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import io.novafoundation.nova.common.utils.atLeastZero
|
||||
import io.novafoundation.nova.common.utils.invoke
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
|
||||
import io.novafoundation.nova.runtime.repository.ChainStateRepository
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Era
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Integer.min
|
||||
|
||||
private const val FALLBACK_MAX_HASH_COUNT = 250
|
||||
private const val MAX_FINALITY_LAG = 5
|
||||
private const val MORTAL_PERIOD = 5 * 60 * 1000
|
||||
|
||||
class Mortality(val era: Era.Mortal, val blockHash: String)
|
||||
|
||||
class MortalityConstructor(
|
||||
private val rpcCalls: RpcCalls,
|
||||
private val chainStateRepository: ChainStateRepository,
|
||||
) {
|
||||
|
||||
fun mortalPeriodMillis(): Long = MORTAL_PERIOD.toLong()
|
||||
|
||||
suspend fun constructMortality(chainId: ChainId): Mortality = withContext(Dispatchers.IO) {
|
||||
val finalizedHash = async { rpcCalls.getFinalizedHead(chainId) }
|
||||
|
||||
val bestHeader = async { rpcCalls.getBlockHeader(chainId) }
|
||||
val finalizedHeader = async { rpcCalls.getBlockHeader(chainId, finalizedHash()) }
|
||||
|
||||
val currentHeader = async { bestHeader().parentHash?.let { rpcCalls.getBlockHeader(chainId, it) } ?: bestHeader() }
|
||||
|
||||
val currentNumber = currentHeader().number
|
||||
val finalizedNumber = finalizedHeader().number
|
||||
|
||||
val finalityLag = (currentNumber - finalizedNumber).atLeastZero()
|
||||
|
||||
val startBlockNumber = finalizedNumber
|
||||
|
||||
val blockHashCount = chainStateRepository.blockHashCount(chainId)?.toInt()
|
||||
|
||||
val blockTime = chainStateRepository.predictedBlockTime(chainId).toInt()
|
||||
|
||||
val mortalPeriod = MORTAL_PERIOD / blockTime + finalityLag
|
||||
|
||||
val unmappedPeriod = min(blockHashCount ?: FALLBACK_MAX_HASH_COUNT, mortalPeriod)
|
||||
|
||||
val era = Era.getEraFromBlockPeriod(startBlockNumber, unmappedPeriod)
|
||||
val eraBlockNumber = ((startBlockNumber - era.phase) / era.period) * era.period + era.phase
|
||||
|
||||
val eraBlockHash = rpcCalls.getBlockHash(chainId, eraBlockNumber.toBigInteger())
|
||||
|
||||
Mortality(era, eraBlockHash)
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
|
||||
/**
|
||||
* Signed extension for PezkuwiChain that authorizes calls.
|
||||
* This extension uses PhantomData internally, so it has no payload (empty encoding).
|
||||
*
|
||||
* In the runtime, AuthorizeCall is defined as:
|
||||
* pub struct AuthorizeCall<T>(core::marker::PhantomData<T>);
|
||||
*
|
||||
* It's placed first in the TxExtension tuple for PezkuwiChain.
|
||||
*/
|
||||
class AuthorizeCall : FixedValueTransactionExtension(
|
||||
name = "AuthorizeCall",
|
||||
implicit = null,
|
||||
explicit = null // PhantomData encodes to nothing
|
||||
)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novafoundation.nova.common.utils.structOf
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
import java.math.BigInteger
|
||||
|
||||
class ChargeAssetTxPayment(
|
||||
val assetId: Any? = null,
|
||||
val tip: BigInteger = BigInteger.ZERO
|
||||
) : FixedValueTransactionExtension(
|
||||
name = ID,
|
||||
implicit = null,
|
||||
explicit = assetTxPaymentPayload(assetId, tip)
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
val ID = "ChargeAssetTxPayment"
|
||||
|
||||
private fun assetTxPaymentPayload(assetId: Any?, tip: BigInteger = BigInteger.ZERO): Any {
|
||||
return structOf(
|
||||
"tip" to tip,
|
||||
"assetId" to assetId
|
||||
)
|
||||
}
|
||||
|
||||
fun ExtrinsicBuilder.chargeAssetTxPayment(assetId: Any?, tip: BigInteger = BigInteger.ZERO) {
|
||||
setTransactionExtension(ChargeAssetTxPayment(assetId, tip))
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
import java.math.BigInteger
|
||||
|
||||
// Signed extension for Avail related to Data Availability Transactions.
|
||||
// We set it to 0 which is the default value provided by Avail team
|
||||
class CheckAppId(appId: BigInteger = BigInteger.ZERO) : FixedValueTransactionExtension(
|
||||
name = "CheckAppId",
|
||||
implicit = null,
|
||||
explicit = appId
|
||||
)
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
|
||||
/**
|
||||
* Signed extension for PezkuwiChain that checks for non-zero sender.
|
||||
* This extension ensures the sender is not the zero address.
|
||||
*
|
||||
* In the runtime, CheckNonZeroSender is defined as:
|
||||
* pub struct CheckNonZeroSender<T>(core::marker::PhantomData<T>);
|
||||
*
|
||||
* It uses PhantomData internally, so it has no payload (empty encoding).
|
||||
*/
|
||||
class CheckNonZeroSender : FixedValueTransactionExtension(
|
||||
name = "CheckNonZeroSender",
|
||||
implicit = null,
|
||||
explicit = null // PhantomData encodes to nothing
|
||||
)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
|
||||
/**
|
||||
* Signed extension that checks weight limits.
|
||||
* This extension uses PhantomData internally, so it has no payload (empty encoding).
|
||||
*
|
||||
* In the runtime, CheckWeight is defined as:
|
||||
* pub struct CheckWeight<T>(core::marker::PhantomData<T>);
|
||||
*/
|
||||
class CheckWeight : FixedValueTransactionExtension(
|
||||
name = "CheckWeight",
|
||||
implicit = null,
|
||||
explicit = null // PhantomData encodes to nothing
|
||||
)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
|
||||
/**
|
||||
* Custom CheckMortality extension for Pezkuwi chains using IMMORTAL era.
|
||||
*
|
||||
* Pezkuwi uses pezsp_runtime.generic.era.Era which is a DictEnum with variants:
|
||||
* - Immortal (encoded as 0x00)
|
||||
* - Mortal1(u8), Mortal2(u8), ..., Mortal255(u8)
|
||||
*
|
||||
* This extension uses Immortal era with genesis hash, which matches how @pezkuwi/api signs.
|
||||
*
|
||||
* @param genesisHash The chain's genesis hash (32 bytes) for the signer payload
|
||||
*/
|
||||
class PezkuwiCheckImmortal(
|
||||
genesisHash: ByteArray
|
||||
) : FixedValueTransactionExtension(
|
||||
name = "CheckMortality",
|
||||
implicit = genesisHash, // Genesis hash goes into signer payload for immortal transactions
|
||||
explicit = DictEnum.Entry<Any?>("Immortal", null) // Immortal variant - unit type with no value
|
||||
)
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Era
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* Custom CheckMortality extension for Pezkuwi chains.
|
||||
*
|
||||
* Pezkuwi uses pezsp_runtime.generic.era.Era which is a DictEnum with variants:
|
||||
* - Immortal
|
||||
* - Mortal1(u8), Mortal2(u8), ..., Mortal255(u8)
|
||||
*
|
||||
* The variant name is "MortalX" where X is the first byte of the encoded era,
|
||||
* and the variant's value is the second byte (u8).
|
||||
*
|
||||
* @param era The mortal era from MortalityConstructor
|
||||
* @param blockHash The block hash (32 bytes) for the signer payload
|
||||
*/
|
||||
class PezkuwiCheckMortality(
|
||||
era: Era.Mortal,
|
||||
blockHash: ByteArray
|
||||
) : FixedValueTransactionExtension(
|
||||
name = "CheckMortality",
|
||||
implicit = blockHash, // blockHash goes into signer payload
|
||||
explicit = createEraEntry(era) // Era as DictEnum.Entry
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Creates a DictEnum.Entry for the Era.
|
||||
*
|
||||
* Standard Era encoding produces 2 bytes:
|
||||
* - First byte determines the variant name (Mortal1, Mortal2, ..., Mortal255)
|
||||
* - Second byte is the variant's value (u8)
|
||||
*/
|
||||
private fun createEraEntry(era: Era.Mortal): DictEnum.Entry<BigInteger> {
|
||||
val period = era.period.toLong()
|
||||
val phase = era.phase.toLong()
|
||||
val quantizeFactor = maxOf(period shr 12, 1)
|
||||
|
||||
// Calculate the two-byte encoding
|
||||
val encoded = ((countTrailingZeroBits(period) - 1).coerceIn(1, 15)) or
|
||||
((phase / quantizeFactor).toInt() shl 4)
|
||||
|
||||
val firstByte = encoded and 0xFF
|
||||
val secondByte = (encoded shr 8) and 0xFF
|
||||
|
||||
// DictEnum variant: "MortalX" where X is the first byte
|
||||
// Variant value: second byte as u8 (BigInteger)
|
||||
return DictEnum.Entry(
|
||||
name = "Mortal$firstByte",
|
||||
value = BigInteger.valueOf(secondByte.toLong())
|
||||
)
|
||||
}
|
||||
|
||||
private fun countTrailingZeroBits(value: Long): Int {
|
||||
if (value == 0L) return 64
|
||||
var n = 0
|
||||
var x = value
|
||||
while ((x and 1L) == 0L) {
|
||||
n++
|
||||
x = x shr 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.extensions
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.FixedValueTransactionExtension
|
||||
|
||||
/**
|
||||
* Signed extension for PezkuwiChain that handles weight reclamation.
|
||||
* This extension reclaims unused weight after transaction execution.
|
||||
*
|
||||
* In the runtime, WeightReclaim is defined as:
|
||||
* pub struct WeightReclaim<T>(core::marker::PhantomData<T>);
|
||||
*
|
||||
* It uses PhantomData internally, so it has no payload (empty encoding).
|
||||
*/
|
||||
class WeightReclaim : FixedValueTransactionExtension(
|
||||
name = "WeightReclaim",
|
||||
implicit = null,
|
||||
explicit = null // PhantomData encodes to nothing
|
||||
)
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.metadata
|
||||
|
||||
@JvmInline
|
||||
value class ExtrinsicProof(val value: ByteArray)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.metadata
|
||||
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.checkMetadataHash.CheckMetadataHashMode
|
||||
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.chain.RuntimeVersion
|
||||
|
||||
class MetadataProof(
|
||||
val checkMetadataHash: CheckMetadataHashMode,
|
||||
val usedVersion: RuntimeVersion
|
||||
)
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.metadata
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.hasSignedExtension
|
||||
import io.novafoundation.nova.metadata_shortener.MetadataShortener
|
||||
import io.novafoundation.nova.runtime.ext.shouldDisableMetadataHashCheck
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRawMetadata
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.DefaultSignedExtensions
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.checkMetadataHash.CheckMetadataHashMode
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.getGenesisHashOrThrow
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
|
||||
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.chain.RuntimeVersionFull
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
interface MetadataShortenerService {
|
||||
|
||||
suspend fun isCheckMetadataHashAvailable(chainId: ChainId): Boolean
|
||||
|
||||
suspend fun generateExtrinsicProof(inheritedImplication: InheritedImplication): ExtrinsicProof
|
||||
|
||||
suspend fun generateMetadataProof(chainId: ChainId): MetadataProof
|
||||
|
||||
suspend fun generateDisabledMetadataProof(chainId: ChainId): MetadataProof
|
||||
}
|
||||
|
||||
private const val MINIMUM_METADATA_VERSION_TO_CALCULATE_HASH = 15
|
||||
|
||||
internal class RealMetadataShortenerService(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val rpcCalls: RpcCalls,
|
||||
) : MetadataShortenerService {
|
||||
|
||||
private val cache = MetadataProofCache()
|
||||
override suspend fun isCheckMetadataHashAvailable(chainId: ChainId): Boolean {
|
||||
val runtime = chainRegistry.getRuntime(chainId)
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
|
||||
return shouldCalculateMetadataHash(runtime.metadata, chain)
|
||||
}
|
||||
|
||||
override suspend fun generateExtrinsicProof(inheritedImplication: InheritedImplication): ExtrinsicProof {
|
||||
val chainId = inheritedImplication.getGenesisHashOrThrow().toHexString(withPrefix = false)
|
||||
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
val mainAsset = chain.utilityAsset
|
||||
|
||||
val call = inheritedImplication.encodedCall()
|
||||
val signedExtras = inheritedImplication.encodedExplicits()
|
||||
val additionalSigned = inheritedImplication.encodedImplicits()
|
||||
|
||||
val metadataVersion = rpcCalls.getRuntimeVersion(chainId)
|
||||
|
||||
val metadata = chainRegistry.getRawMetadata(chainId)
|
||||
|
||||
val proof = MetadataShortener.generate_extrinsic_proof(
|
||||
call,
|
||||
signedExtras,
|
||||
additionalSigned,
|
||||
metadata.metadataContent,
|
||||
metadataVersion.specVersion,
|
||||
metadataVersion.specName,
|
||||
chain.addressPrefix,
|
||||
mainAsset.precision.value,
|
||||
mainAsset.symbol.value
|
||||
)
|
||||
|
||||
return ExtrinsicProof(proof)
|
||||
}
|
||||
|
||||
override suspend fun generateMetadataProof(chainId: ChainId): MetadataProof {
|
||||
val runtimeVersion = rpcCalls.getRuntimeVersion(chainId)
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
|
||||
return cache.getOrCompute(chainId, expectedSpecVersion = runtimeVersion.specVersion) {
|
||||
val runtimeMetadata = chainRegistry.getRuntime(chainId).metadata
|
||||
val shouldIncludeHash = shouldCalculateMetadataHash(runtimeMetadata, chain)
|
||||
|
||||
if (shouldIncludeHash) {
|
||||
generateMetadataProofOrDisabled(chain, runtimeVersion)
|
||||
} else {
|
||||
MetadataProof(
|
||||
checkMetadataHash = CheckMetadataHashMode.Disabled,
|
||||
usedVersion = runtimeVersion
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun generateDisabledMetadataProof(chainId: ChainId): MetadataProof {
|
||||
val runtimeVersion = rpcCalls.getRuntimeVersion(chainId)
|
||||
|
||||
return MetadataProof(
|
||||
checkMetadataHash = CheckMetadataHashMode.Disabled,
|
||||
usedVersion = runtimeVersion,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun generateMetadataProofOrDisabled(
|
||||
chain: Chain,
|
||||
runtimeVersion: RuntimeVersionFull
|
||||
): MetadataProof {
|
||||
return runCatching {
|
||||
val hash = generateMetadataHash(chain, runtimeVersion)
|
||||
|
||||
MetadataProof(
|
||||
checkMetadataHash = CheckMetadataHashMode.Enabled(hash),
|
||||
usedVersion = runtimeVersion
|
||||
)
|
||||
}.getOrElse { // Fallback to disabled in case something went wrong during hash generation
|
||||
Log.w("MetadataShortenerService", "Failed to generate metadata hash", it)
|
||||
|
||||
MetadataProof(
|
||||
checkMetadataHash = CheckMetadataHashMode.Disabled,
|
||||
usedVersion = runtimeVersion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun generateMetadataHash(
|
||||
chain: Chain,
|
||||
runtimeVersion: RuntimeVersionFull
|
||||
): ByteArray {
|
||||
val rawMetadata = chainRegistry.getRawMetadata(chain.id)
|
||||
val mainAsset = chain.utilityAsset
|
||||
|
||||
return MetadataShortener.generate_metadata_digest(
|
||||
rawMetadata.metadataContent,
|
||||
runtimeVersion.specVersion,
|
||||
runtimeVersion.specName,
|
||||
chain.addressPrefix,
|
||||
mainAsset.precision.value,
|
||||
mainAsset.symbol.value
|
||||
)
|
||||
}
|
||||
|
||||
private class MetadataProofCache {
|
||||
|
||||
// Quote simple synchronization - we block all chains
|
||||
// Shouldn't be a problem at the moment since we don't expect a lot of multi-chain txs/fee estimations to happen at once
|
||||
private val cacheAccessMutex: Mutex = Mutex()
|
||||
private val cache = mutableMapOf<ChainId, MetadataProof>()
|
||||
|
||||
suspend fun getOrCompute(
|
||||
chainId: ChainId,
|
||||
expectedSpecVersion: Int,
|
||||
compute: suspend () -> MetadataProof
|
||||
): MetadataProof = cacheAccessMutex.withLock {
|
||||
val cachedProof = cache[chainId]
|
||||
|
||||
if (cachedProof == null || cachedProof.usedVersion.specVersion != expectedSpecVersion) {
|
||||
val newValue = compute()
|
||||
cache[chainId] = newValue
|
||||
|
||||
newValue
|
||||
} else {
|
||||
cachedProof
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldCalculateMetadataHash(runtimeMetadata: RuntimeMetadata, chain: Chain): Boolean {
|
||||
val disabledByConfig = chain.additional.shouldDisableMetadataHashCheck()
|
||||
val canBeEnabled = disabledByConfig.not()
|
||||
val atLeastMinimumVersion = runtimeMetadata.metadataVersion >= MINIMUM_METADATA_VERSION_TO_CALCULATE_HASH
|
||||
val hasSignedExtension = runtimeMetadata.extrinsic.hasSignedExtension(DefaultSignedExtensions.CHECK_METADATA_HASH)
|
||||
|
||||
Log.d(
|
||||
"MetadataShortenerService",
|
||||
"Chain: ${chain.name}, disabledByConfig=$disabledByConfig, canBeEnabled=$canBeEnabled, " +
|
||||
"atLeastMinimumVersion=$atLeastMinimumVersion, hasSignedExtension=$hasSignedExtension"
|
||||
)
|
||||
Log.d("MetadataShortenerService", "chain.additional: ${chain.additional}, disabledCheckMetadataHash=${chain.additional?.disabledCheckMetadataHash}")
|
||||
|
||||
val result = canBeEnabled && atLeastMinimumVersion && hasSignedExtension
|
||||
Log.d("MetadataShortenerService", "shouldCalculateMetadataHash result: $result (will use ${if (result) "ENABLED" else "DISABLED"} mode)")
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.multi
|
||||
|
||||
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.metadata.call
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module
|
||||
|
||||
interface CallBuilder {
|
||||
|
||||
val runtime: RuntimeSnapshot
|
||||
|
||||
val calls: List<GenericCall.Instance>
|
||||
|
||||
fun addCall(
|
||||
moduleName: String,
|
||||
callName: String,
|
||||
arguments: Map<String, Any?>
|
||||
): CallBuilder
|
||||
}
|
||||
|
||||
class SimpleCallBuilder(override val runtime: RuntimeSnapshot) : CallBuilder {
|
||||
|
||||
override val calls = mutableListOf<GenericCall.Instance>()
|
||||
|
||||
override fun addCall(moduleName: String, callName: String, arguments: Map<String, Any?>): CallBuilder {
|
||||
val module = runtime.metadata.module(moduleName)
|
||||
val function = module.call(callName)
|
||||
|
||||
calls.add(GenericCall.Instance(module, function, arguments))
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.signer
|
||||
|
||||
import java.math.BigInteger
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* SR25519 signing implementation for PezkuwiChain using "bizinikiwi" signing context.
|
||||
*
|
||||
* This is a port of @pezkuwi/scure-sr25519 which uses Merlin transcripts
|
||||
* (built on Strobe128) with a custom signing context.
|
||||
*
|
||||
* Standard Substrate uses "substrate" context, but Pezkuwi uses "bizinikiwi".
|
||||
*/
|
||||
object BizinikiwSr25519Signer {
|
||||
|
||||
private val BIZINIKIWI_CONTEXT = "bizinikiwi".toByteArray(Charsets.UTF_8)
|
||||
|
||||
// Ed25519 curve order
|
||||
private val CURVE_ORDER = BigInteger("7237005577332262213973186563042994240857116359379907606001950938285454250989")
|
||||
|
||||
// Strobe128 constants
|
||||
private const val STROBE_R = 166
|
||||
|
||||
/**
|
||||
* Sign a message using SR25519 with "bizinikiwi" context.
|
||||
*
|
||||
* @param secretKey 64-byte secret key (32-byte scalar + 32-byte nonce)
|
||||
* @param message The message to sign
|
||||
* @return 64-byte signature with Schnorrkel marker
|
||||
*/
|
||||
fun sign(secretKey: ByteArray, message: ByteArray): ByteArray {
|
||||
require(secretKey.size == 64) { "Secret key must be 64 bytes" }
|
||||
|
||||
// Create signing transcript
|
||||
val transcript = SigningContext("SigningContext")
|
||||
transcript.label(BIZINIKIWI_CONTEXT)
|
||||
transcript.bytes(message)
|
||||
|
||||
// Extract key components
|
||||
val keyScalar = decodeScalar(secretKey.copyOfRange(0, 32))
|
||||
val nonce = secretKey.copyOfRange(32, 64)
|
||||
val publicKey = getPublicKey(secretKey)
|
||||
val pubPoint = RistrettoPoint.fromBytes(publicKey)
|
||||
|
||||
// Schnorrkel signing protocol
|
||||
transcript.protoName("Schnorr-sig")
|
||||
transcript.commitPoint("sign:pk", pubPoint)
|
||||
|
||||
val r = transcript.witnessScalar("signing", nonce)
|
||||
val R = RistrettoPoint.BASE.multiply(r)
|
||||
|
||||
transcript.commitPoint("sign:R", R)
|
||||
val k = transcript.challengeScalar("sign:c")
|
||||
|
||||
val s = (k.multiply(keyScalar).add(r)).mod(CURVE_ORDER)
|
||||
|
||||
// Build signature
|
||||
val signature = ByteArray(64)
|
||||
System.arraycopy(R.toBytes(), 0, signature, 0, 32)
|
||||
System.arraycopy(scalarToBytes(s), 0, signature, 32, 32)
|
||||
|
||||
// Add Schnorrkel marker
|
||||
signature[63] = (signature[63].toInt() or 0x80).toByte()
|
||||
|
||||
return signature
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key from secret key.
|
||||
*/
|
||||
fun getPublicKey(secretKey: ByteArray): ByteArray {
|
||||
require(secretKey.size == 64) { "Secret key must be 64 bytes" }
|
||||
val scalar = decodeScalar(secretKey.copyOfRange(0, 32))
|
||||
return RistrettoPoint.BASE.multiply(scalar).toBytes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signature.
|
||||
*/
|
||||
fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean {
|
||||
require(signature.size == 64) { "Signature must be 64 bytes" }
|
||||
require(publicKey.size == 32) { "Public key must be 32 bytes" }
|
||||
|
||||
// Check Schnorrkel marker
|
||||
if ((signature[63].toInt() and 0x80) == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract R and s from signature
|
||||
val sBytes = signature.copyOfRange(32, 64)
|
||||
sBytes[31] = (sBytes[31].toInt() and 0x7F).toByte() // Remove marker
|
||||
|
||||
val R = RistrettoPoint.fromBytes(signature.copyOfRange(0, 32))
|
||||
val s = bytesToScalar(sBytes)
|
||||
|
||||
// Reconstruct transcript
|
||||
val transcript = SigningContext("SigningContext")
|
||||
transcript.label(BIZINIKIWI_CONTEXT)
|
||||
transcript.bytes(message)
|
||||
|
||||
val pubPoint = RistrettoPoint.fromBytes(publicKey)
|
||||
if (pubPoint.isZero()) return false
|
||||
|
||||
transcript.protoName("Schnorr-sig")
|
||||
transcript.commitPoint("sign:pk", pubPoint)
|
||||
transcript.commitPoint("sign:R", R)
|
||||
|
||||
val k = transcript.challengeScalar("sign:c")
|
||||
|
||||
// Verify: R + k*P == s*G
|
||||
val left = R.add(pubPoint.multiply(k))
|
||||
val right = RistrettoPoint.BASE.multiply(s)
|
||||
|
||||
return left == right
|
||||
}
|
||||
|
||||
private fun decodeScalar(bytes: ByteArray): BigInteger {
|
||||
require(bytes.size == 32) { "Scalar must be 32 bytes" }
|
||||
// Little-endian
|
||||
val reversed = bytes.reversedArray()
|
||||
return BigInteger(1, reversed).mod(CURVE_ORDER)
|
||||
}
|
||||
|
||||
private fun bytesToScalar(bytes: ByteArray): BigInteger {
|
||||
val reversed = bytes.reversedArray()
|
||||
return BigInteger(1, reversed).mod(CURVE_ORDER)
|
||||
}
|
||||
|
||||
private fun scalarToBytes(scalar: BigInteger): ByteArray {
|
||||
val bytes = scalar.toByteArray()
|
||||
val result = ByteArray(32)
|
||||
|
||||
// Handle sign byte and padding
|
||||
val start = if (bytes[0] == 0.toByte() && bytes.size > 32) 1 else 0
|
||||
val length = minOf(bytes.size - start, 32)
|
||||
val offset = 32 - length
|
||||
|
||||
System.arraycopy(bytes, start, result, offset, length)
|
||||
|
||||
// Convert to little-endian
|
||||
return result.reversedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Strobe128 implementation for Merlin transcripts.
|
||||
*/
|
||||
private class Strobe128(protocolLabel: String) {
|
||||
private val state = ByteArray(200)
|
||||
private var pos = 0
|
||||
private var posBegin = 0
|
||||
private var curFlags = 0
|
||||
|
||||
init {
|
||||
// Initialize state
|
||||
state[0] = 1
|
||||
state[1] = (STROBE_R + 2).toByte()
|
||||
state[2] = 1
|
||||
state[3] = 0
|
||||
state[4] = 1
|
||||
state[5] = 96
|
||||
|
||||
val strobeVersion = "STROBEv1.0.2".toByteArray(Charsets.UTF_8)
|
||||
System.arraycopy(strobeVersion, 0, state, 6, strobeVersion.size)
|
||||
|
||||
keccakF1600()
|
||||
metaAD(protocolLabel.toByteArray(Charsets.UTF_8), false)
|
||||
}
|
||||
|
||||
private fun keccakF1600() {
|
||||
// Keccak-f[1600] permutation
|
||||
// Using BouncyCastle's Keccak implementation would be more efficient,
|
||||
// but for now we'll use a simplified version
|
||||
val keccak = org.bouncycastle.crypto.digests.KeccakDigest(1600)
|
||||
keccak.update(state, 0, state.size)
|
||||
// This is a placeholder - need proper Keccak-p implementation
|
||||
}
|
||||
|
||||
fun metaAD(data: ByteArray, more: Boolean) {
|
||||
absorb(data)
|
||||
}
|
||||
|
||||
fun AD(data: ByteArray, more: Boolean) {
|
||||
absorb(data)
|
||||
}
|
||||
|
||||
fun PRF(length: Int): ByteArray {
|
||||
val result = ByteArray(length)
|
||||
squeeze(result)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun absorb(data: ByteArray) {
|
||||
for (byte in data) {
|
||||
state[pos] = (state[pos].toInt() xor byte.toInt()).toByte()
|
||||
pos++
|
||||
if (pos == STROBE_R) {
|
||||
runF()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun squeeze(out: ByteArray) {
|
||||
for (i in out.indices) {
|
||||
out[i] = state[pos]
|
||||
state[pos] = 0
|
||||
pos++
|
||||
if (pos == STROBE_R) {
|
||||
runF()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runF() {
|
||||
state[pos] = (state[pos].toInt() xor posBegin).toByte()
|
||||
state[pos + 1] = (state[pos + 1].toInt() xor 0x04).toByte()
|
||||
state[STROBE_R + 1] = (state[STROBE_R + 1].toInt() xor 0x80).toByte()
|
||||
keccakF1600()
|
||||
pos = 0
|
||||
posBegin = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merlin signing context/transcript.
|
||||
*/
|
||||
private class SigningContext(label: String) {
|
||||
private val strobe = Strobe128(label)
|
||||
|
||||
fun label(data: ByteArray) {
|
||||
val lengthBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data.size).array()
|
||||
strobe.metaAD(lengthBytes, false)
|
||||
strobe.metaAD(data, true)
|
||||
}
|
||||
|
||||
fun bytes(data: ByteArray) {
|
||||
val lengthBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data.size).array()
|
||||
strobe.metaAD(lengthBytes, false)
|
||||
strobe.AD(data, false)
|
||||
}
|
||||
|
||||
fun protoName(name: String) {
|
||||
val data = name.toByteArray(Charsets.UTF_8)
|
||||
strobe.metaAD(data, false)
|
||||
}
|
||||
|
||||
fun commitPoint(label: String, point: RistrettoPoint) {
|
||||
strobe.metaAD(label.toByteArray(Charsets.UTF_8), false)
|
||||
strobe.AD(point.toBytes(), false)
|
||||
}
|
||||
|
||||
fun witnessScalar(label: String, nonce: ByteArray): BigInteger {
|
||||
strobe.metaAD(label.toByteArray(Charsets.UTF_8), false)
|
||||
strobe.AD(nonce, false)
|
||||
val bytes = strobe.PRF(64)
|
||||
return bytesToWideScalar(bytes)
|
||||
}
|
||||
|
||||
fun challengeScalar(label: String): BigInteger {
|
||||
strobe.metaAD(label.toByteArray(Charsets.UTF_8), false)
|
||||
val bytes = strobe.PRF(64)
|
||||
return bytesToWideScalar(bytes)
|
||||
}
|
||||
|
||||
private fun bytesToWideScalar(bytes: ByteArray): BigInteger {
|
||||
// Reduce 64 bytes to a scalar modulo curve order
|
||||
val reversed = bytes.reversedArray()
|
||||
return BigInteger(1, reversed).mod(CURVE_ORDER)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ristretto255 point operations.
|
||||
* This is a placeholder - full implementation requires Ed25519 curve math.
|
||||
*/
|
||||
private class RistrettoPoint private constructor(private val bytes: ByteArray) {
|
||||
|
||||
companion object {
|
||||
val BASE: RistrettoPoint by lazy {
|
||||
// Ed25519 base point in Ristretto encoding
|
||||
val baseBytes = ByteArray(32)
|
||||
// TODO: Set proper base point bytes
|
||||
RistrettoPoint(baseBytes)
|
||||
}
|
||||
|
||||
fun fromBytes(bytes: ByteArray): RistrettoPoint {
|
||||
require(bytes.size == 32) { "Point must be 32 bytes" }
|
||||
return RistrettoPoint(bytes.copyOf())
|
||||
}
|
||||
}
|
||||
|
||||
fun toBytes(): ByteArray = bytes.copyOf()
|
||||
|
||||
fun multiply(scalar: BigInteger): RistrettoPoint {
|
||||
// TODO: Implement scalar multiplication
|
||||
return this
|
||||
}
|
||||
|
||||
fun add(other: RistrettoPoint): RistrettoPoint {
|
||||
// TODO: Implement point addition
|
||||
return this
|
||||
}
|
||||
|
||||
fun isZero(): Boolean {
|
||||
return bytes.all { it == 0.toByte() }
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is RistrettoPoint) return false
|
||||
return bytes.contentEquals(other.bytes)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = bytes.contentHashCode()
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.signer
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.sr25519.BizinikiwSr25519
|
||||
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignedRaw
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.extensions.verifySignature.GeneralTransactionSigner
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.signingPayload
|
||||
|
||||
/**
|
||||
* SR25519 signer for Pezkuwi chains using "bizinikiwi" signing context.
|
||||
*
|
||||
* This signer is used instead of the standard KeyPairSigner for Pezkuwi ecosystem chains
|
||||
* (Pezkuwi, Pezkuwi Asset Hub, Pezkuwi People) which require signatures with "bizinikiwi"
|
||||
* context instead of the standard "substrate" context used by Polkadot ecosystem chains.
|
||||
*/
|
||||
class PezkuwiKeyPairSigner private constructor(
|
||||
private val secretKey: ByteArray,
|
||||
private val publicKey: ByteArray
|
||||
) : GeneralTransactionSigner {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a PezkuwiKeyPairSigner from a 32-byte seed.
|
||||
* The seed is expanded to a full keypair using BizinikiwSr25519.
|
||||
*/
|
||||
fun fromSeed(seed: ByteArray): PezkuwiKeyPairSigner {
|
||||
require(seed.size == 32) { "Seed must be 32 bytes, got ${seed.size}" }
|
||||
|
||||
Log.d("PezkuwiSigner", "Creating signer from seed")
|
||||
|
||||
// Expand seed to 96-byte keypair
|
||||
val expandedKeypair = BizinikiwSr25519.keypairFromSeed(seed)
|
||||
Log.d("PezkuwiSigner", "Expanded keypair size: ${expandedKeypair.size}")
|
||||
|
||||
// Extract 64-byte secret key and 32-byte public key
|
||||
val secretKey = BizinikiwSr25519.secretKeyFromKeypair(expandedKeypair)
|
||||
val publicKey = BizinikiwSr25519.publicKeyFromKeypair(expandedKeypair)
|
||||
|
||||
Log.d("PezkuwiSigner", "Secret key size: ${secretKey.size}")
|
||||
Log.d("PezkuwiSigner", "Public key: ${publicKey.toHex()}")
|
||||
|
||||
return PezkuwiKeyPairSigner(secretKey, publicKey)
|
||||
}
|
||||
|
||||
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
override suspend fun signInheritedImplication(
|
||||
inheritedImplication: InheritedImplication,
|
||||
accountId: AccountId
|
||||
): SignatureWrapper {
|
||||
val payload = inheritedImplication.signingPayload()
|
||||
|
||||
Log.d("PezkuwiSigner", "=== SIGNING WITH BIZINIKIWI ===")
|
||||
Log.d("PezkuwiSigner", "Payload size: ${payload.size}")
|
||||
Log.d("PezkuwiSigner", "Payload: ${payload.toHex()}")
|
||||
|
||||
// Use BizinikiwSr25519 native library with "bizinikiwi" signing context
|
||||
val signature = BizinikiwSr25519.sign(
|
||||
publicKey = publicKey,
|
||||
secretKey = secretKey,
|
||||
message = payload
|
||||
)
|
||||
|
||||
Log.d("PezkuwiSigner", "Signature: ${signature.toHex()}")
|
||||
|
||||
// Verify locally
|
||||
val verified = BizinikiwSr25519.verify(signature, payload, publicKey)
|
||||
Log.d("PezkuwiSigner", "Local verification: $verified")
|
||||
|
||||
return SignatureWrapper.Sr25519(signature)
|
||||
}
|
||||
|
||||
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
||||
|
||||
suspend fun signRaw(payload: SignerPayloadRaw): SignedRaw {
|
||||
// Use BizinikiwSr25519 native library with "bizinikiwi" signing context
|
||||
val signature = BizinikiwSr25519.sign(
|
||||
publicKey = publicKey,
|
||||
secretKey = secretKey,
|
||||
message = payload.message
|
||||
)
|
||||
|
||||
return SignedRaw(payload, SignatureWrapper.Sr25519(signature))
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.signer
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
|
||||
|
||||
class SignerPayloadRawWithChain(
|
||||
val chainId: ChainId,
|
||||
val message: ByteArray,
|
||||
val accountId: AccountId,
|
||||
)
|
||||
|
||||
fun SignerPayloadRawWithChain.withoutChain(): SignerPayloadRaw {
|
||||
return SignerPayloadRaw(message, accountId)
|
||||
}
|
||||
|
||||
fun SignerPayloadRaw.withChain(chainId: ChainId): SignerPayloadRawWithChain {
|
||||
return SignerPayloadRawWithChain(chainId = chainId, message = message, accountId = accountId)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor
|
||||
|
||||
import android.util.Log
|
||||
|
||||
internal interface ExtrinsicVisitorLogger {
|
||||
|
||||
fun info(message: String)
|
||||
|
||||
fun error(message: String)
|
||||
}
|
||||
|
||||
internal class IndentVisitorLogger(
|
||||
private val tag: String = "ExtrinsicVisitor",
|
||||
private val indent: Int = 0
|
||||
) : ExtrinsicVisitorLogger {
|
||||
|
||||
private val indentPrefix = " ".repeat(indent)
|
||||
|
||||
override fun info(message: String) {
|
||||
Log.d(tag, indentPrefix + message)
|
||||
}
|
||||
|
||||
override fun error(message: String) {
|
||||
Log.e(tag, indentPrefix + message)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.api
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
interface CallTraversal {
|
||||
|
||||
fun traverse(
|
||||
source: GenericCall.Instance,
|
||||
initialOrigin: AccountIdKey,
|
||||
visitor: CallVisitor
|
||||
)
|
||||
}
|
||||
|
||||
fun interface CallVisitor {
|
||||
|
||||
fun visit(visit: CallVisit)
|
||||
}
|
||||
|
||||
fun CallTraversal.collect(
|
||||
source: GenericCall.Instance,
|
||||
initialOrigin: AccountIdKey,
|
||||
): List<CallVisit> {
|
||||
return buildList {
|
||||
traverse(source, initialOrigin) {
|
||||
add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.api
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
interface CallVisit {
|
||||
/**
|
||||
* Call that is currently visiting
|
||||
*/
|
||||
val call: GenericCall.Instance
|
||||
|
||||
/**
|
||||
* Origin's account id that this call has been dispatched with
|
||||
*/
|
||||
val callOrigin: AccountIdKey
|
||||
}
|
||||
|
||||
class LeafCallVisit(
|
||||
override val call: GenericCall.Instance,
|
||||
override val callOrigin: AccountIdKey
|
||||
) : CallVisit
|
||||
|
||||
val CallVisit.isLeaf: Boolean
|
||||
get() = this is LeafCallVisit
|
||||
|
||||
interface BatchCallVisit : CallVisit {
|
||||
|
||||
val batchedCalls: List<GenericCall.Instance>
|
||||
}
|
||||
|
||||
interface MultisigCallVisit : CallVisit {
|
||||
|
||||
val signatory: AccountIdKey
|
||||
get() = callOrigin
|
||||
|
||||
val otherSignatories: List<AccountIdKey>
|
||||
|
||||
val threshold: Int
|
||||
|
||||
val multisig: AccountIdKey
|
||||
|
||||
val nestedCall: GenericCall.Instance
|
||||
}
|
||||
|
||||
interface ProxyCallVisit : CallVisit {
|
||||
|
||||
val proxy: AccountIdKey
|
||||
get() = callOrigin
|
||||
|
||||
val proxied: AccountIdKey
|
||||
|
||||
val nestedCall: GenericCall.Instance
|
||||
}
|
||||
|
||||
fun CallVisit.requireLeafOrNull(): LeafCallVisit? {
|
||||
return this as? LeafCallVisit
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.impl
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.ExtrinsicVisitorLogger
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallVisit
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
internal interface NestedCallVisitNode {
|
||||
|
||||
fun canVisit(call: GenericCall.Instance): Boolean
|
||||
|
||||
fun visit(call: GenericCall.Instance, context: CallVisitingContext)
|
||||
}
|
||||
|
||||
internal interface CallVisitingContext {
|
||||
|
||||
val origin: AccountIdKey
|
||||
|
||||
val logger: ExtrinsicVisitorLogger
|
||||
|
||||
/**
|
||||
* Request parent to perform recursive visit of the given call
|
||||
*/
|
||||
fun nestedVisit(visit: NestedCallVisit)
|
||||
|
||||
/**
|
||||
* Call the supplied visitor with the given argument
|
||||
*/
|
||||
fun visit(visit: CallVisit)
|
||||
}
|
||||
|
||||
internal fun CallVisitingContext.nestedVisit(call: GenericCall.Instance, origin: AccountIdKey) {
|
||||
nestedVisit(NestedCallVisit(call, origin))
|
||||
}
|
||||
|
||||
/**
|
||||
* Version of [CallVisit] intended for nested usage
|
||||
*
|
||||
* @see [CallVisit]
|
||||
*/
|
||||
internal class NestedCallVisit(
|
||||
val call: GenericCall.Instance,
|
||||
|
||||
val origin: AccountIdKey
|
||||
)
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.impl
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.ExtrinsicVisitorLogger
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.IndentVisitorLogger
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallTraversal
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.CallVisitor
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.LeafCallVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.batch.BatchAllCallNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.batch.BatchCallNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.batch.ForceBatchCallNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.multisig.MultisigCallNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.proxy.ProxyCallNode
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAddress
|
||||
|
||||
internal class RealCallTraversal(
|
||||
private val knownNodes: List<NestedCallVisitNode> = defaultNodes(),
|
||||
) : CallTraversal {
|
||||
|
||||
companion object {
|
||||
|
||||
fun defaultNodes(): List<NestedCallVisitNode> = listOf(
|
||||
BatchCallNode(),
|
||||
BatchAllCallNode(),
|
||||
ForceBatchCallNode(),
|
||||
|
||||
ProxyCallNode(),
|
||||
|
||||
MultisigCallNode()
|
||||
)
|
||||
}
|
||||
|
||||
override fun traverse(
|
||||
source: GenericCall.Instance,
|
||||
initialOrigin: AccountIdKey,
|
||||
visitor: CallVisitor
|
||||
) {
|
||||
val rootVisit = NestedCallVisit(
|
||||
call = source,
|
||||
origin = initialOrigin
|
||||
)
|
||||
|
||||
nestedVisit(visitor, rootVisit)
|
||||
}
|
||||
|
||||
private fun nestedVisit(
|
||||
visitor: CallVisitor,
|
||||
visitedCall: NestedCallVisit
|
||||
) {
|
||||
val nestedNode = findNestedNode(visitedCall.call)
|
||||
|
||||
if (nestedNode == null) {
|
||||
val publicVisit = visitedCall.toLeafVisit()
|
||||
|
||||
val call = visitedCall.call
|
||||
val display = "${call.module.name}.${call.function.name}"
|
||||
val origin = visitedCall.origin
|
||||
val newLogger = IndentVisitorLogger()
|
||||
|
||||
newLogger.info("Visited leaf: $display, origin: ${origin.value.toAddress(42)}")
|
||||
|
||||
visitor.visit(publicVisit)
|
||||
} else {
|
||||
val newLogger = IndentVisitorLogger()
|
||||
|
||||
val context = RealCallVisitingContext(
|
||||
origin = visitedCall.origin,
|
||||
visitor = visitor,
|
||||
logger = newLogger,
|
||||
)
|
||||
|
||||
nestedNode.visit(visitedCall.call, context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findNestedNode(call: GenericCall.Instance): NestedCallVisitNode? {
|
||||
return knownNodes.find { it.canVisit(call) }
|
||||
}
|
||||
|
||||
private fun NestedCallVisit.toLeafVisit(): CallVisit {
|
||||
return LeafCallVisit(call, origin)
|
||||
}
|
||||
|
||||
private inner class RealCallVisitingContext(
|
||||
override val origin: AccountIdKey,
|
||||
override val logger: ExtrinsicVisitorLogger,
|
||||
private val visitor: CallVisitor
|
||||
) : CallVisitingContext {
|
||||
|
||||
override fun nestedVisit(visit: NestedCallVisit) {
|
||||
return this@RealCallTraversal.nestedVisit(visitor, visit)
|
||||
}
|
||||
|
||||
override fun visit(visit: CallVisit) {
|
||||
visitor.visit(visit)
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.batch
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCallList
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.BatchCallVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.CallVisitingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.NestedCallVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.NestedCallVisitNode
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
internal abstract class BaseBatchNode : NestedCallVisitNode {
|
||||
|
||||
override fun visit(call: GenericCall.Instance, context: CallVisitingContext) {
|
||||
val innerCalls = bindGenericCallList(call.arguments["calls"])
|
||||
|
||||
context.logger.info("Visiting ${this::class.simpleName} with ${innerCalls.size} inner calls")
|
||||
|
||||
val batchVisit = RealBatchCallVisit(call, innerCalls, context.origin)
|
||||
context.visit(batchVisit)
|
||||
|
||||
innerCalls.forEach { inner ->
|
||||
val nestedVisit = NestedCallVisit(call = inner, origin = context.origin)
|
||||
context.nestedVisit(nestedVisit)
|
||||
}
|
||||
}
|
||||
|
||||
private class RealBatchCallVisit(
|
||||
override val call: GenericCall.Instance,
|
||||
override val batchedCalls: List<GenericCall.Instance>,
|
||||
override val callOrigin: AccountIdKey
|
||||
) : BatchCallVisit
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.batch
|
||||
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
internal class BatchAllCallNode : BaseBatchNode() {
|
||||
|
||||
override fun canVisit(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.UTILITY && call.function.name == "batch_all"
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.batch
|
||||
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
internal class BatchCallNode : BaseBatchNode() {
|
||||
|
||||
override fun canVisit(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.UTILITY && call.function.name == "batch"
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.batch
|
||||
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
internal class ForceBatchCallNode : BaseBatchNode() {
|
||||
|
||||
override fun canVisit(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.UTILITY && call.function.name == "force_batch"
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.multisig
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCall
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindInt
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.MultisigCallVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.CallVisitingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.NestedCallVisitNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nestedVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.multisig.generateMultisigAddress
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
internal class MultisigCallNode : NestedCallVisitNode {
|
||||
|
||||
override fun canVisit(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.MULTISIG && call.function.name == "as_multi"
|
||||
}
|
||||
|
||||
override fun visit(call: GenericCall.Instance, context: CallVisitingContext) {
|
||||
context.logger.info("Visiting multisig")
|
||||
|
||||
val innerOriginInfo = extractMultisigOriginInfo(call, context.origin)
|
||||
val innerCall = extractInnerMultisigCall(call)
|
||||
|
||||
val multisigVisit = RealMultisigCallVisit(
|
||||
call = call,
|
||||
callOrigin = context.origin,
|
||||
otherSignatories = innerOriginInfo.otherSignatories,
|
||||
threshold = innerOriginInfo.threshold,
|
||||
nestedCall = innerCall
|
||||
)
|
||||
|
||||
context.visit(multisigVisit)
|
||||
context.nestedVisit(multisigVisit.nestedCall, multisigVisit.multisig)
|
||||
}
|
||||
|
||||
private fun extractInnerMultisigCall(multisigCall: GenericCall.Instance): GenericCall.Instance {
|
||||
return bindGenericCall(multisigCall.arguments["call"])
|
||||
}
|
||||
|
||||
private fun extractMultisigOriginInfo(call: GenericCall.Instance, parentOrigin: AccountIdKey): MultisigOriginInfo {
|
||||
val threshold = bindInt(call.arguments["threshold"])
|
||||
val otherSignatories = bindList(call.arguments["other_signatories"], ::bindAccountIdKey)
|
||||
|
||||
return MultisigOriginInfo(threshold, otherSignatories)
|
||||
}
|
||||
|
||||
private class MultisigOriginInfo(
|
||||
val threshold: Int,
|
||||
val otherSignatories: List<AccountIdKey>,
|
||||
)
|
||||
|
||||
private class RealMultisigCallVisit(
|
||||
override val call: GenericCall.Instance,
|
||||
override val callOrigin: AccountIdKey,
|
||||
override val otherSignatories: List<AccountIdKey>,
|
||||
override val threshold: Int,
|
||||
override val nestedCall: GenericCall.Instance,
|
||||
) : MultisigCallVisit {
|
||||
|
||||
override val multisig: AccountIdKey = generateMultisigAddress(
|
||||
signatory = callOrigin,
|
||||
otherSignatories = otherSignatories,
|
||||
threshold = threshold
|
||||
)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nodes.proxy
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCall
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.api.ProxyCallVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.CallVisitingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.NestedCallVisitNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.call.impl.nestedVisit
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
internal class ProxyCallNode : NestedCallVisitNode {
|
||||
|
||||
private val proxyCalls = arrayOf("proxy", "proxyAnnounced")
|
||||
|
||||
override fun canVisit(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.PROXY && call.function.name in proxyCalls
|
||||
}
|
||||
|
||||
override fun visit(call: GenericCall.Instance, context: CallVisitingContext) {
|
||||
context.logger.info("Visiting proxy")
|
||||
|
||||
val proxyVisit = RealProxyVisit(
|
||||
call = call,
|
||||
proxied = innerOrigin(call),
|
||||
nestedCall = innerCall(call),
|
||||
callOrigin = context.origin
|
||||
)
|
||||
|
||||
context.visit(proxyVisit)
|
||||
context.nestedVisit(proxyVisit.nestedCall, proxyVisit.proxied)
|
||||
}
|
||||
|
||||
private fun innerOrigin(proxyCall: GenericCall.Instance): AccountIdKey {
|
||||
return bindAccountIdentifier(proxyCall.arguments["real"]).intoKey()
|
||||
}
|
||||
|
||||
private fun innerCall(proxyCall: GenericCall.Instance): GenericCall.Instance {
|
||||
return bindGenericCall(proxyCall.arguments["call"])
|
||||
}
|
||||
|
||||
private class RealProxyVisit(
|
||||
override val call: GenericCall.Instance,
|
||||
override val proxied: AccountIdKey,
|
||||
override val nestedCall: GenericCall.Instance,
|
||||
override val callOrigin: AccountIdKey
|
||||
) : ProxyCallVisit
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
|
||||
interface ExtrinsicWalk {
|
||||
|
||||
suspend fun walk(
|
||||
source: ExtrinsicWithEvents,
|
||||
chainId: ChainId,
|
||||
visitor: ExtrinsicVisitor
|
||||
)
|
||||
}
|
||||
|
||||
fun interface ExtrinsicVisitor {
|
||||
|
||||
fun visit(visit: ExtrinsicVisit)
|
||||
}
|
||||
|
||||
class ExtrinsicVisit(
|
||||
|
||||
/**
|
||||
* Whole extrinsic object. Useful for accessing data outside if the current visit scope, e.g. some top-level events
|
||||
*/
|
||||
val rootExtrinsic: ExtrinsicWithEvents,
|
||||
|
||||
/**
|
||||
* Call that is currently visiting
|
||||
*/
|
||||
val call: GenericCall.Instance,
|
||||
|
||||
/**
|
||||
* Whether call succeeded or not.
|
||||
* Call is considered successful when it succeeds itself as well as its outer parents succeeds
|
||||
*/
|
||||
val success: Boolean,
|
||||
|
||||
/**
|
||||
* All events that are related to this specific call
|
||||
*/
|
||||
val events: List<GenericEvent.Instance>,
|
||||
|
||||
/**
|
||||
* Origin's account id that this call has been dispatched with
|
||||
*/
|
||||
val origin: AccountId,
|
||||
|
||||
/**
|
||||
* Whether this visit is related to a registered node or not
|
||||
*/
|
||||
val hasRegisteredNode: Boolean = false
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents
|
||||
|
||||
suspend fun ExtrinsicWalk.walkToList(source: ExtrinsicWithEvents, chainId: ChainId): List<ExtrinsicVisit> {
|
||||
return buildList {
|
||||
walk(source, chainId) { visitedCall ->
|
||||
add(visitedCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl
|
||||
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
|
||||
|
||||
internal interface MutableEventQueue : EventQueue {
|
||||
|
||||
/**
|
||||
* Removes last event matching one of eventTypes
|
||||
*/
|
||||
fun popFromEnd(vararg eventTypes: Event)
|
||||
|
||||
/**
|
||||
* Takes and removes all events that go after last event matching one of eventTypes. If no matched event found,
|
||||
* all available events are returned
|
||||
*/
|
||||
fun takeTail(vararg eventTypes: Event): List<GenericEvent.Instance>
|
||||
|
||||
/**
|
||||
* Takes and removes all events that go after specified inclusive index
|
||||
* @param endInclusive
|
||||
*/
|
||||
fun takeAllAfterInclusive(endInclusive: Int): List<GenericEvent.Instance>
|
||||
|
||||
/**
|
||||
* Takes and removes last event matching one of eventTypes
|
||||
*/
|
||||
fun takeFromEnd(vararg eventTypes: Event): GenericEvent.Instance?
|
||||
}
|
||||
|
||||
internal interface EventQueue {
|
||||
|
||||
fun all(): List<GenericEvent.Instance>
|
||||
|
||||
fun peekItemFromEnd(vararg eventTypes: Event, endExclusive: Int): EventWithIndex?
|
||||
|
||||
fun indexOfLast(vararg eventTypes: Event, endExclusive: Int): Int?
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
internal inline fun EventQueue.peekItemFromEndOrThrow(vararg eventTypes: Event, endExclusive: Int): EventWithIndex {
|
||||
return requireNotNull(peekItemFromEnd(*eventTypes, endExclusive = endExclusive)) {
|
||||
"No required event found for types ${eventTypes.joinToString { it.name }}"
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
internal inline fun MutableEventQueue.takeFromEndOrThrow(vararg eventTypes: Event): GenericEvent.Instance {
|
||||
return requireNotNull(takeFromEnd(*eventTypes)) {
|
||||
"No required event found for types ${eventTypes.joinToString { it.name }}"
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
internal inline fun EventQueue.indexOfLastOrThrow(vararg eventTypes: Event, endExclusive: Int): Int {
|
||||
return requireNotNull(indexOfLast(*eventTypes, endExclusive = endExclusive)) {
|
||||
"No required event found for types ${eventTypes.joinToString { it.name }}"
|
||||
}
|
||||
}
|
||||
|
||||
data class EventWithIndex(val event: GenericEvent.Instance, val eventIndex: Int)
|
||||
|
||||
class RealEventQueue(event: List<GenericEvent.Instance>) : MutableEventQueue {
|
||||
|
||||
private val events: MutableList<GenericEvent.Instance> = event.toMutableList()
|
||||
|
||||
override fun all(): List<GenericEvent.Instance> {
|
||||
return events
|
||||
}
|
||||
|
||||
override fun peekItemFromEnd(vararg eventTypes: Event, endExclusive: Int): EventWithIndex? {
|
||||
return findEventAndIndex(eventTypes, endExclusive)
|
||||
}
|
||||
|
||||
override fun indexOfLast(vararg eventTypes: Event, endExclusive: Int): Int? {
|
||||
return findEventAndIndex(eventTypes, endExclusive)?.eventIndex
|
||||
}
|
||||
|
||||
override fun popFromEnd(vararg eventTypes: Event) {
|
||||
takeFromEnd(*eventTypes)
|
||||
}
|
||||
|
||||
override fun takeTail(vararg eventTypes: Event): List<GenericEvent.Instance> {
|
||||
val eventWithIndex = this.findEventAndIndex(eventTypes)
|
||||
|
||||
return if (eventWithIndex != null) {
|
||||
this.removeAllAfterExclusive(eventWithIndex.eventIndex)
|
||||
} else {
|
||||
this.removeAllAfterInclusive(0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun takeAllAfterInclusive(endInclusive: Int): List<GenericEvent.Instance> {
|
||||
return removeAllAfterInclusive(endInclusive)
|
||||
}
|
||||
|
||||
override fun takeFromEnd(vararg eventTypes: Event): GenericEvent.Instance? {
|
||||
return findEventAndIndex(eventTypes)?.let { (event, index) ->
|
||||
removeAllAfterInclusive(index)
|
||||
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
private fun findEventAndIndex(eventTypes: Array<out Event>, endExclusive: Int = this.events.size): EventWithIndex? {
|
||||
val eventsQueue = this.events
|
||||
val limit = endExclusive.coerceAtMost(eventsQueue.size)
|
||||
|
||||
for (i in (limit - 1) downTo 0) {
|
||||
val nextEvent = eventsQueue[i]
|
||||
|
||||
eventTypes.forEach { event ->
|
||||
if (nextEvent.instanceOf(event)) return EventWithIndex(nextEvent, i)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun removeAllAfterInclusive(index: Int): List<GenericEvent.Instance> {
|
||||
if (index > this.events.size) return emptyList()
|
||||
|
||||
val subList = this.events.subList(index, this.events.size)
|
||||
val subListCopy = subList.toList()
|
||||
|
||||
subList.clear()
|
||||
|
||||
return subListCopy
|
||||
}
|
||||
|
||||
private fun removeAllAfterExclusive(index: Int): List<GenericEvent.Instance> {
|
||||
val subList = this.events.subList(index + 1, this.events.size)
|
||||
val subListCopy = subList.toList()
|
||||
|
||||
subList.clear()
|
||||
|
||||
return subListCopy
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl
|
||||
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.ExtrinsicVisitorLogger
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents
|
||||
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.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
|
||||
internal interface NestedCallNode {
|
||||
|
||||
fun canVisit(call: GenericCall.Instance): Boolean
|
||||
|
||||
/**
|
||||
* Calculates exclusive end index that is needed to skip all internal events related to this nested call
|
||||
* For example, utility.batch supposed to skip BatchCompleted/BatchInterrupted and all ItemCompleted events
|
||||
* This function is used by `visit` to skip internal events of nested nodes of the same type (batch inside batch or proxy inside proxy)
|
||||
* so they wont interfere
|
||||
* Should not be called on failed nested calls since they emit no events and its trivial to proceed
|
||||
*/
|
||||
fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, context: EventCountingContext): Int
|
||||
|
||||
fun visit(call: GenericCall.Instance, context: VisitingContext)
|
||||
}
|
||||
|
||||
internal interface VisitingContext {
|
||||
|
||||
val rootExtrinsic: ExtrinsicWithEvents
|
||||
|
||||
val runtime: RuntimeSnapshot
|
||||
|
||||
val origin: AccountId
|
||||
|
||||
val callSucceeded: Boolean
|
||||
|
||||
val logger: ExtrinsicVisitorLogger
|
||||
|
||||
val eventQueue: MutableEventQueue
|
||||
|
||||
fun nestedVisit(visit: NestedExtrinsicVisit)
|
||||
|
||||
fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance): Int
|
||||
}
|
||||
|
||||
/**
|
||||
* Version of [ExtrinsicVisit] intended for nested usage
|
||||
*
|
||||
* @see [ExtrinsicVisit]
|
||||
*/
|
||||
internal class NestedExtrinsicVisit(
|
||||
val rootExtrinsic: ExtrinsicWithEvents,
|
||||
val call: GenericCall.Instance,
|
||||
val success: Boolean,
|
||||
val events: List<GenericEvent.Instance>,
|
||||
val origin: AccountId,
|
||||
)
|
||||
|
||||
internal interface EventCountingContext {
|
||||
|
||||
val runtime: RuntimeSnapshot
|
||||
|
||||
val eventQueue: EventQueue
|
||||
|
||||
val endExclusive: Int
|
||||
|
||||
fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, endExclusive: Int): Int
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl
|
||||
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.ExtrinsicVisitorLogger
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.IndentVisitorLogger
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisitor
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicWalk
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.batch.BatchAllNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.batch.ForceBatchNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.multisig.MultisigNode
|
||||
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.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.isSuccess
|
||||
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.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.signer
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAddress
|
||||
|
||||
internal class RealExtrinsicWalk(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val knownNodes: List<NestedCallNode> = defaultNodes(),
|
||||
) : ExtrinsicWalk {
|
||||
|
||||
companion object {
|
||||
|
||||
fun defaultNodes() = listOf(
|
||||
ProxyNode(),
|
||||
|
||||
BatchAllNode(),
|
||||
ForceBatchNode(),
|
||||
|
||||
MultisigNode()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun walk(
|
||||
source: ExtrinsicWithEvents,
|
||||
chainId: ChainId,
|
||||
visitor: ExtrinsicVisitor
|
||||
) {
|
||||
val runtime = chainRegistry.getRuntime(chainId)
|
||||
|
||||
val rootVisit = NestedExtrinsicVisit(
|
||||
rootExtrinsic = source,
|
||||
call = source.extrinsic.call,
|
||||
success = source.isSuccess(),
|
||||
events = source.events,
|
||||
origin = source.extrinsic.signer() ?: error("Unsigned extrinsic"),
|
||||
)
|
||||
|
||||
nestedVisit(runtime, visitor, rootVisit, depth = 0)
|
||||
}
|
||||
|
||||
private fun nestedVisit(
|
||||
runtime: RuntimeSnapshot,
|
||||
visitor: ExtrinsicVisitor,
|
||||
visitedCall: NestedExtrinsicVisit,
|
||||
depth: Int,
|
||||
) {
|
||||
val nestedNode = findNestedNode(visitedCall.call)
|
||||
val publicVisit = visitedCall.toVisit(hasRegisteredNode = nestedNode != null)
|
||||
|
||||
visitor.visit(publicVisit)
|
||||
|
||||
if (nestedNode == null) {
|
||||
val call = visitedCall.call
|
||||
val display = "${call.module.name}.${call.function.name}"
|
||||
val origin = visitedCall.origin
|
||||
val newLogger = IndentVisitorLogger(indent = depth + 1)
|
||||
|
||||
newLogger.info("Visited leaf: $display, success: ${visitedCall.success}, origin: ${origin.toAddress(42)}")
|
||||
} else {
|
||||
val eventQueue = RealEventQueue(visitedCall.events)
|
||||
val newLogger = IndentVisitorLogger(indent = depth)
|
||||
|
||||
val context = RealVisitingContext(
|
||||
rootExtrinsic = visitedCall.rootExtrinsic,
|
||||
eventsSize = visitedCall.events.size,
|
||||
depth = depth,
|
||||
runtime = runtime,
|
||||
origin = visitedCall.origin,
|
||||
callSucceeded = visitedCall.success,
|
||||
visitor = visitor,
|
||||
logger = newLogger,
|
||||
eventQueue = eventQueue
|
||||
)
|
||||
|
||||
nestedNode.visit(visitedCall.call, context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun endExclusiveToSkipInternalEvents(
|
||||
runtime: RuntimeSnapshot,
|
||||
call: GenericCall.Instance,
|
||||
eventQueue: EventQueue,
|
||||
endExclusive: Int,
|
||||
): Int {
|
||||
val nestedNode = this.findNestedNode(call)
|
||||
|
||||
return if (nestedNode != null) {
|
||||
val context: EventCountingContext = RealEventCountingContext(runtime, eventQueue, endExclusive)
|
||||
|
||||
nestedNode.endExclusiveToSkipInternalEvents(call, context)
|
||||
} else {
|
||||
// no internal events to skip since its a leaf
|
||||
endExclusive
|
||||
}
|
||||
}
|
||||
|
||||
private fun findNestedNode(call: GenericCall.Instance): NestedCallNode? {
|
||||
return knownNodes.find { it.canVisit(call) }
|
||||
}
|
||||
|
||||
private fun NestedExtrinsicVisit.toVisit(hasRegisteredNode: Boolean): ExtrinsicVisit {
|
||||
return ExtrinsicVisit(rootExtrinsic, call, success, events, origin, hasRegisteredNode)
|
||||
}
|
||||
|
||||
private inner class RealVisitingContext(
|
||||
private val eventsSize: Int,
|
||||
private val depth: Int,
|
||||
override val rootExtrinsic: ExtrinsicWithEvents,
|
||||
override val runtime: RuntimeSnapshot,
|
||||
override val origin: AccountId,
|
||||
override val callSucceeded: Boolean,
|
||||
override val logger: ExtrinsicVisitorLogger,
|
||||
override val eventQueue: MutableEventQueue,
|
||||
private val visitor: ExtrinsicVisitor
|
||||
) : VisitingContext {
|
||||
|
||||
override fun nestedVisit(visit: NestedExtrinsicVisit) {
|
||||
return this@RealExtrinsicWalk.nestedVisit(runtime, visitor, visit, depth + 1)
|
||||
}
|
||||
|
||||
override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance): Int {
|
||||
return this@RealExtrinsicWalk.endExclusiveToSkipInternalEvents(runtime, call, eventQueue, endExclusive = eventsSize)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class RealEventCountingContext(
|
||||
override val runtime: RuntimeSnapshot,
|
||||
override val eventQueue: EventQueue,
|
||||
override val endExclusive: Int
|
||||
) : EventCountingContext {
|
||||
|
||||
override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, endExclusive: Int): Int {
|
||||
return this@RealExtrinsicWalk.endExclusiveToSkipInternalEvents(runtime, call, eventQueue, endExclusive)
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.batch
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCallList
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.EventCountingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.NestedCallNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.NestedExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.VisitingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.indexOfLastOrThrow
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
internal class BatchAllNode : NestedCallNode {
|
||||
|
||||
override fun canVisit(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.UTILITY && call.function.name == "batch_all"
|
||||
}
|
||||
|
||||
override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, context: EventCountingContext): Int {
|
||||
val innerCalls = bindGenericCallList(call.arguments["calls"])
|
||||
|
||||
val batchCompletedEventType = context.runtime.batchCompletedEvent()
|
||||
val itemCompletedEventType = context.runtime.itemCompletedEvent()
|
||||
|
||||
var endExclusive = context.endExclusive
|
||||
|
||||
// Safe since `endExclusiveToSkipInternalEvents` should not be called on failed items
|
||||
val indexOfCompletedEvent = context.eventQueue.indexOfLastOrThrow(batchCompletedEventType, endExclusive = endExclusive)
|
||||
endExclusive = indexOfCompletedEvent
|
||||
|
||||
innerCalls.reversed().forEach { innerCall ->
|
||||
val itemIdx = context.eventQueue.indexOfLastOrThrow(itemCompletedEventType, endExclusive = endExclusive)
|
||||
endExclusive = context.endExclusiveToSkipInternalEvents(innerCall, itemIdx)
|
||||
}
|
||||
|
||||
return endExclusive
|
||||
}
|
||||
|
||||
override fun visit(call: GenericCall.Instance, context: VisitingContext) {
|
||||
val innerCalls = bindGenericCallList(call.arguments["calls"])
|
||||
val itemCompletedEventType = context.runtime.itemCompletedEvent()
|
||||
|
||||
context.logger.info("Visiting utility.batchAll with ${innerCalls.size} inner calls")
|
||||
|
||||
if (context.callSucceeded) {
|
||||
context.logger.info("BatchAll succeeded")
|
||||
} else {
|
||||
context.logger.info("BatchAll failed")
|
||||
}
|
||||
|
||||
val subItemsToVisit = innerCalls.reversed().map { innerCall ->
|
||||
if (context.callSucceeded) {
|
||||
context.eventQueue.popFromEnd(itemCompletedEventType)
|
||||
val alNestedEvents = context.takeCompletedBatchItemEvents(innerCall)
|
||||
|
||||
NestedExtrinsicVisit(
|
||||
rootExtrinsic = context.rootExtrinsic,
|
||||
call = innerCall,
|
||||
success = true,
|
||||
events = alNestedEvents,
|
||||
origin = context.origin
|
||||
)
|
||||
} else {
|
||||
NestedExtrinsicVisit(
|
||||
rootExtrinsic = context.rootExtrinsic,
|
||||
call = innerCall,
|
||||
success = false,
|
||||
events = emptyList(),
|
||||
origin = context.origin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
subItemsToVisit.forEach { subItem ->
|
||||
context.nestedVisit(subItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.batch
|
||||
|
||||
import io.novafoundation.nova.common.utils.utility
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.VisitingContext
|
||||
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.event
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
|
||||
|
||||
internal fun RuntimeSnapshot.batchCompletedEvent(): Event {
|
||||
return metadata.utility().event("BatchCompleted")
|
||||
}
|
||||
|
||||
internal fun RuntimeSnapshot.batchCompletedWithErrorsEvent(): Event {
|
||||
return metadata.utility().event("BatchCompletedWithErrors")
|
||||
}
|
||||
|
||||
internal fun RuntimeSnapshot.itemCompletedEvent(): Event {
|
||||
return metadata.utility().event("ItemCompleted")
|
||||
}
|
||||
|
||||
internal fun RuntimeSnapshot.itemFailedEvent(): Event {
|
||||
return metadata.utility().event("ItemFailed")
|
||||
}
|
||||
|
||||
internal fun VisitingContext.takeCompletedBatchItemEvents(call: GenericCall.Instance): List<GenericEvent.Instance> {
|
||||
val internalEventsEndExclusive = endExclusiveToSkipInternalEvents(call)
|
||||
|
||||
// internalEnd is exclusive => it holds index of last internal event
|
||||
// thus, we delete them inclusively
|
||||
val someOfNestedEvents = eventQueue.takeAllAfterInclusive(internalEventsEndExclusive)
|
||||
|
||||
// now it is safe to go until ItemCompleted\ItemFailed since we removed all potential nested events above
|
||||
val remainingNestedEvents = eventQueue.takeTail(runtime.itemCompletedEvent(), runtime.itemFailedEvent())
|
||||
|
||||
return remainingNestedEvents + someOfNestedEvents
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.batch
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCallList
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.EventCountingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.NestedCallNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.NestedExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.VisitingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.indexOfLastOrThrow
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.peekItemFromEndOrThrow
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.takeFromEndOrThrow
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
internal class ForceBatchNode : NestedCallNode {
|
||||
|
||||
override fun canVisit(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.UTILITY && call.function.name == "force_batch"
|
||||
}
|
||||
|
||||
override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, context: EventCountingContext): Int {
|
||||
val innerCalls = bindGenericCallList(call.arguments["calls"])
|
||||
|
||||
val batchCompletedEventType = context.runtime.batchCompletedEvent()
|
||||
val batchCompletedWithErrorsType = context.runtime.batchCompletedWithErrorsEvent()
|
||||
|
||||
val itemCompletedEventType = context.runtime.itemCompletedEvent()
|
||||
val itemFailedEventType = context.runtime.itemFailedEvent()
|
||||
|
||||
var endExclusive = context.endExclusive
|
||||
|
||||
// Safe since batch all always completes
|
||||
val indexOfCompletedEvent = context.eventQueue.indexOfLastOrThrow(batchCompletedEventType, batchCompletedWithErrorsType, endExclusive = endExclusive)
|
||||
endExclusive = indexOfCompletedEvent
|
||||
|
||||
innerCalls.reversed().forEach { innerCall ->
|
||||
val (itemEvent, itemEventIdx) = context.eventQueue.peekItemFromEndOrThrow(itemCompletedEventType, itemFailedEventType, endExclusive = endExclusive)
|
||||
|
||||
endExclusive = if (itemEvent.instanceOf(itemCompletedEventType)) {
|
||||
// only completed items emit nested events
|
||||
context.endExclusiveToSkipInternalEvents(innerCall, itemEventIdx)
|
||||
} else {
|
||||
itemEventIdx
|
||||
}
|
||||
}
|
||||
|
||||
return endExclusive
|
||||
}
|
||||
|
||||
override fun visit(call: GenericCall.Instance, context: VisitingContext) {
|
||||
val innerCalls = bindGenericCallList(call.arguments["calls"])
|
||||
|
||||
val batchCompletedEventType = context.runtime.batchCompletedEvent()
|
||||
val batchCompletedWithErrorsType = context.runtime.batchCompletedWithErrorsEvent()
|
||||
|
||||
val itemCompletedEventType = context.runtime.itemCompletedEvent()
|
||||
val itemFailedEventType = context.runtime.itemFailedEvent()
|
||||
|
||||
context.logger.info("Visiting utility.forceBatch with ${innerCalls.size} inner calls")
|
||||
|
||||
if (context.callSucceeded) {
|
||||
context.logger.info("ForceBatch succeeded")
|
||||
|
||||
context.eventQueue.popFromEnd(batchCompletedEventType, batchCompletedWithErrorsType)
|
||||
} else {
|
||||
context.logger.info("ForceBatch failed")
|
||||
}
|
||||
|
||||
val subItemsToVisit = innerCalls.reversed().map { innerCall ->
|
||||
if (context.callSucceeded) {
|
||||
val itemEvent = context.eventQueue.takeFromEndOrThrow(itemCompletedEventType, itemFailedEventType)
|
||||
|
||||
if (itemEvent.instanceOf(itemCompletedEventType)) {
|
||||
val allEvents = context.takeCompletedBatchItemEvents(innerCall)
|
||||
|
||||
return@map NestedExtrinsicVisit(
|
||||
rootExtrinsic = context.rootExtrinsic,
|
||||
call = innerCall,
|
||||
success = true,
|
||||
events = allEvents,
|
||||
origin = context.origin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NestedExtrinsicVisit(
|
||||
rootExtrinsic = context.rootExtrinsic,
|
||||
call = innerCall,
|
||||
success = false,
|
||||
events = emptyList(),
|
||||
origin = context.origin
|
||||
)
|
||||
}
|
||||
|
||||
subItemsToVisit.forEach { subItem ->
|
||||
context.nestedVisit(subItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.multisig
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.utils.compareTo
|
||||
import io.novafoundation.nova.common.utils.padEnd
|
||||
import io.novasama.substrate_sdk_android.hash.Hasher.blake2b256
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.useScaleWriter
|
||||
import io.novasama.substrate_sdk_android.scale.utils.directWrite
|
||||
|
||||
private val PREFIX = "modlpy/utilisuba".encodeToByteArray()
|
||||
|
||||
fun generateMultisigAddress(
|
||||
signatory: AccountIdKey,
|
||||
otherSignatories: List<AccountIdKey>,
|
||||
threshold: Int
|
||||
) = generateMultisigAddress(otherSignatories + signatory, threshold)
|
||||
|
||||
fun generateMultisigAddress(
|
||||
signatories: List<AccountIdKey>,
|
||||
threshold: Int
|
||||
): AccountIdKey {
|
||||
val accountIdSize = signatories.first().value.size
|
||||
|
||||
val sortedAccounts = signatories.sortedWith { a, b -> a.value.compareTo(b.value, unsigned = true) }
|
||||
|
||||
val entropy = useScaleWriter {
|
||||
directWrite(PREFIX)
|
||||
|
||||
writeCompact(sortedAccounts.size)
|
||||
sortedAccounts.forEach {
|
||||
directWrite(it.value)
|
||||
}
|
||||
|
||||
writeUint16(threshold)
|
||||
}.blake2b256()
|
||||
|
||||
val result = when {
|
||||
entropy.size == accountIdSize -> entropy
|
||||
entropy.size < accountIdSize -> entropy.padEnd(accountIdSize, 0)
|
||||
else -> entropy.copyOf(accountIdSize)
|
||||
}
|
||||
|
||||
return result.intoKey()
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.multisig
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCall
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindInt
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.common.utils.multisig
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.EventCountingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.NestedCallNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.NestedExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.VisitingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.peekItemFromEndOrThrow
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.takeFromEndOrThrow
|
||||
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.event
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
|
||||
|
||||
internal class MultisigNode : NestedCallNode {
|
||||
|
||||
override fun canVisit(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.MULTISIG && call.function.name == "as_multi"
|
||||
}
|
||||
|
||||
override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, context: EventCountingContext): Int {
|
||||
var endExclusive = context.endExclusive
|
||||
val completionEventTypes = context.runtime.multisigCompletionEvents()
|
||||
|
||||
val (completionEvent, completionIdx) = context.eventQueue.peekItemFromEndOrThrow(eventTypes = completionEventTypes, endExclusive = endExclusive)
|
||||
endExclusive = completionIdx
|
||||
|
||||
if (completionEvent.isMultisigExecutionSucceeded()) {
|
||||
val innerCall = this.extractInnerMultisigCall(call)
|
||||
endExclusive = context.endExclusiveToSkipInternalEvents(innerCall, endExclusive)
|
||||
}
|
||||
|
||||
return endExclusive
|
||||
}
|
||||
|
||||
override fun visit(call: GenericCall.Instance, context: VisitingContext) {
|
||||
if (!context.callSucceeded) {
|
||||
visitFailedMultisigCall(call, context)
|
||||
context.logger.info("asMulti - reverted by outer parent")
|
||||
return
|
||||
}
|
||||
|
||||
val completionEventTypes = context.runtime.multisigCompletionEvents()
|
||||
val completionEvent = context.eventQueue.takeFromEndOrThrow(*completionEventTypes)
|
||||
|
||||
// Not visiting pending mst's
|
||||
if (!completionEvent.isMultisigExecuted()) return
|
||||
|
||||
if (completionEvent.isMultisigExecutedOk()) {
|
||||
context.logger.info("asMulti: execution succeeded")
|
||||
|
||||
visitSucceededMultisigCall(call, context)
|
||||
} else {
|
||||
context.logger.info("asMulti: execution failed")
|
||||
|
||||
this.visitFailedMultisigCall(call, context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun visitFailedMultisigCall(call: GenericCall.Instance, context: VisitingContext) {
|
||||
this.visitMultisigCall(call, context, success = false, events = emptyList())
|
||||
}
|
||||
|
||||
private fun visitSucceededMultisigCall(call: GenericCall.Instance, context: VisitingContext) {
|
||||
this.visitMultisigCall(call, context, success = true, events = context.eventQueue.all())
|
||||
}
|
||||
|
||||
private fun visitMultisigCall(
|
||||
call: GenericCall.Instance,
|
||||
context: VisitingContext,
|
||||
success: Boolean,
|
||||
events: List<GenericEvent.Instance>
|
||||
) {
|
||||
val innerOrigin = extractMultisigOrigin(call, context.origin.intoKey())
|
||||
val innerCall = extractInnerMultisigCall(call)
|
||||
|
||||
val visit = NestedExtrinsicVisit(
|
||||
rootExtrinsic = context.rootExtrinsic,
|
||||
call = innerCall,
|
||||
success = success,
|
||||
events = events,
|
||||
origin = innerOrigin.value
|
||||
)
|
||||
|
||||
context.nestedVisit(visit)
|
||||
}
|
||||
|
||||
private fun GenericEvent.Instance.isMultisigExecutionSucceeded(): Boolean {
|
||||
if (!isMultisigExecuted()) {
|
||||
// not final execution
|
||||
return false
|
||||
}
|
||||
|
||||
return isMultisigExecutedOk()
|
||||
}
|
||||
|
||||
private fun GenericEvent.Instance.isMultisigExecuted(): Boolean {
|
||||
return instanceOf(Modules.MULTISIG, "MultisigExecuted")
|
||||
}
|
||||
|
||||
private fun GenericEvent.Instance.isMultisigExecutedOk(): Boolean {
|
||||
// dispatch_result in https://github.com/paritytech/polkadot-sdk/blob/fdf3d65e4c2d9094915c7fd7927e651339171edd/substrate/frame/multisig/src/lib.rs#L260
|
||||
return arguments[4].castToDictEnum().name == "Ok"
|
||||
}
|
||||
|
||||
private fun extractInnerMultisigCall(multisigCall: GenericCall.Instance): GenericCall.Instance {
|
||||
return bindGenericCall(multisigCall.arguments["call"])
|
||||
}
|
||||
|
||||
private fun RuntimeSnapshot.multisigCompletionEvents(): Array<Event> {
|
||||
val multisig = metadata.multisig()
|
||||
|
||||
return arrayOf(
|
||||
multisig.event("MultisigExecuted"),
|
||||
multisig.event("MultisigApproval"),
|
||||
multisig.event("NewMultisig"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractMultisigOrigin(call: GenericCall.Instance, parentOrigin: AccountIdKey): AccountIdKey {
|
||||
val threshold = bindInt(call.arguments["threshold"])
|
||||
val otherSignatories = bindList(call.arguments["other_signatories"], ::bindAccountIdKey)
|
||||
|
||||
return generateMultisigAddress(
|
||||
otherSignatories = otherSignatories,
|
||||
signatory = parentOrigin,
|
||||
threshold = threshold
|
||||
)
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.nodes.proxy
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCall
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.proxy
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.EventCountingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.NestedCallNode
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.NestedExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.VisitingContext
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.peekItemFromEndOrThrow
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.impl.takeFromEndOrThrow
|
||||
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.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.event
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
|
||||
|
||||
internal class ProxyNode : NestedCallNode {
|
||||
|
||||
private val proxyCalls = arrayOf("proxy", "proxyAnnounced")
|
||||
|
||||
override fun canVisit(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.PROXY && call.function.name in proxyCalls
|
||||
}
|
||||
|
||||
override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, context: EventCountingContext): Int {
|
||||
var endExclusive = context.endExclusive
|
||||
val completionEventType = context.runtime.proxyCompletionEvent()
|
||||
|
||||
val (completionEvent, completionIdx) = context.eventQueue.peekItemFromEndOrThrow(completionEventType, endExclusive = endExclusive)
|
||||
endExclusive = completionIdx
|
||||
|
||||
if (completionEvent.isProxySucceeded()) {
|
||||
val (innerCall) = this.callAndOriginFromProxy(call)
|
||||
endExclusive = context.endExclusiveToSkipInternalEvents(innerCall, endExclusive)
|
||||
}
|
||||
|
||||
return endExclusive
|
||||
}
|
||||
|
||||
override fun visit(call: GenericCall.Instance, context: VisitingContext) {
|
||||
if (!context.callSucceeded) {
|
||||
this.visitFailedProxyCall(call, context)
|
||||
context.logger.info("Proxy: reverted by outer parent")
|
||||
return
|
||||
}
|
||||
|
||||
val completionEventType = context.runtime.proxyCompletionEvent()
|
||||
val completionEvent = context.eventQueue.takeFromEndOrThrow(completionEventType)
|
||||
|
||||
if (completionEvent.isProxySucceeded()) {
|
||||
context.logger.info("Proxy: execution succeeded")
|
||||
|
||||
this.visitSucceededProxyCall(call, context)
|
||||
} else {
|
||||
context.logger.info("Proxy: execution failed")
|
||||
|
||||
this.visitFailedProxyCall(call, context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun visitFailedProxyCall(call: GenericCall.Instance, context: VisitingContext) {
|
||||
this.visitProxyCall(call, context, success = false, events = emptyList())
|
||||
}
|
||||
|
||||
private fun visitSucceededProxyCall(call: GenericCall.Instance, context: VisitingContext) {
|
||||
this.visitProxyCall(call, context, success = true, events = context.eventQueue.all())
|
||||
}
|
||||
|
||||
private fun visitProxyCall(
|
||||
call: GenericCall.Instance,
|
||||
context: VisitingContext,
|
||||
success: Boolean,
|
||||
events: List<GenericEvent.Instance>
|
||||
) {
|
||||
val (innerCall, innerOrigin) = this.callAndOriginFromProxy(call)
|
||||
|
||||
val visit = NestedExtrinsicVisit(
|
||||
rootExtrinsic = context.rootExtrinsic,
|
||||
call = innerCall,
|
||||
success = success,
|
||||
events = events,
|
||||
origin = innerOrigin
|
||||
)
|
||||
|
||||
context.nestedVisit(visit)
|
||||
}
|
||||
|
||||
private fun GenericEvent.Instance.isProxySucceeded(): Boolean {
|
||||
return arguments.first().castToDictEnum().name == "Ok"
|
||||
}
|
||||
|
||||
private fun callAndOriginFromProxy(proxyCall: GenericCall.Instance): Pair<GenericCall.Instance, AccountId> {
|
||||
return bindGenericCall(proxyCall.arguments["call"]) to bindAccountIdentifier(proxyCall.arguments["real"])
|
||||
}
|
||||
|
||||
private fun RuntimeSnapshot.proxyCompletionEvent(): Event {
|
||||
return metadata.proxy().event("ProxyExecuted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.runtime.mapper
|
||||
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainRuntimeInfoLocal
|
||||
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.chain.RuntimeVersion
|
||||
|
||||
fun ChainRuntimeInfoLocal.toRuntimeVersion(): RuntimeVersion? {
|
||||
return RuntimeVersion(
|
||||
specVersion = this.remoteVersion,
|
||||
transactionVersion = this.transactionVersion ?: return null
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.RuntimeContext
|
||||
import io.novafoundation.nova.common.utils.diffed
|
||||
import io.novafoundation.nova.common.utils.filterList
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.common.utils.mapNotNullToSet
|
||||
import io.novafoundation.nova.common.utils.provideContext
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
import io.novafoundation.nova.core.ethereum.Web3Api
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
import io.novafoundation.nova.core_db.model.chain.NodeSelectionPreferencesLocal
|
||||
import io.novafoundation.nova.runtime.ext.isDisabled
|
||||
import io.novafoundation.nova.runtime.ext.isEnabled
|
||||
import io.novafoundation.nova.runtime.ext.isFullSync
|
||||
import io.novafoundation.nova.runtime.ext.level
|
||||
import io.novafoundation.nova.runtime.ext.requiresBaseTypes
|
||||
import io.novafoundation.nova.runtime.ext.typesUsage
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset.EvmAssetsSyncService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.ChainSyncService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapChainLocalToChain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapConnectionStateToLocal
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.ConnectionState
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Node.ConnectionType
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.ChainConnection
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionPool
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.Web3ApiPool
|
||||
import io.novafoundation.nova.runtime.multiNetwork.exception.DisabledChainException
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProvider
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProviderPool
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeSubscriptionPool
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeSyncService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.BaseTypeSynchronizer
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class ChainWithAsset(
|
||||
val chain: Chain,
|
||||
val asset: Chain.Asset
|
||||
)
|
||||
|
||||
class ChainRegistry(
|
||||
private val runtimeProviderPool: RuntimeProviderPool,
|
||||
private val connectionPool: ConnectionPool,
|
||||
private val runtimeSubscriptionPool: RuntimeSubscriptionPool,
|
||||
private val chainDao: ChainDao,
|
||||
private val chainSyncService: ChainSyncService,
|
||||
private val evmAssetsSyncService: EvmAssetsSyncService,
|
||||
private val baseTypeSynchronizer: BaseTypeSynchronizer,
|
||||
private val runtimeSyncService: RuntimeSyncService,
|
||||
private val web3ApiPool: Web3ApiPool,
|
||||
private val gson: Gson
|
||||
) : CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
||||
|
||||
val currentChains = chainDao.joinChainInfoFlow()
|
||||
.mapList { mapChainLocalToChain(it, gson) }
|
||||
.diffed()
|
||||
.map { diff ->
|
||||
diff.removed.forEach { unregisterChain(it) }
|
||||
diff.newOrUpdated.forEach { chain -> registerChain(chain) }
|
||||
|
||||
diff.all
|
||||
}
|
||||
.filter { it.isNotEmpty() }
|
||||
.distinctUntilChanged()
|
||||
.inBackground()
|
||||
.shareIn(this, SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
val chainsById = currentChains.map { chains -> chains.associateBy { it.id } }
|
||||
.inBackground()
|
||||
.shareIn(this, SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
init {
|
||||
syncChainsAndAssets()
|
||||
|
||||
syncBaseTypesIfNeeded()
|
||||
}
|
||||
|
||||
fun getConnectionOrNull(chainId: String): ChainConnection? {
|
||||
return connectionPool.getConnectionOrNull(chainId.removeHexPrefix())
|
||||
}
|
||||
|
||||
@Deprecated("Use getActiveConnectionOrNull, since this method may throw an exception if Chain is disabled")
|
||||
suspend fun getActiveConnection(chainId: String): ChainConnection {
|
||||
requireConnectionStateAtLeast(chainId, ConnectionState.LIGHT_SYNC)
|
||||
|
||||
return connectionPool.getConnection(chainId.removeHexPrefix())
|
||||
}
|
||||
|
||||
suspend fun getActiveConnectionOrNull(chainId: String): ChainConnection? {
|
||||
return runCatching {
|
||||
requireConnectionStateAtLeast(chainId, ConnectionState.LIGHT_SYNC)
|
||||
|
||||
return connectionPool.getConnectionOrNull(chainId.removeHexPrefix())
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
suspend fun getEthereumApi(chainId: String, connectionType: ConnectionType): Web3Api? {
|
||||
return runCatching {
|
||||
requireConnectionStateAtLeast(chainId, ConnectionState.LIGHT_SYNC)
|
||||
|
||||
web3ApiPool.getWeb3Api(chainId, connectionType)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
suspend fun getRuntimeProvider(chainId: String): RuntimeProvider {
|
||||
requireConnectionStateAtLeast(chainId, ConnectionState.FULL_SYNC)
|
||||
|
||||
return runtimeProviderPool.getRuntimeProvider(chainId.removeHexPrefix())
|
||||
}
|
||||
|
||||
suspend fun getChain(chainId: String): Chain = chainsById.first().getValue(chainId.removeHexPrefix())
|
||||
|
||||
suspend fun enableFullSync(chainId: ChainId) {
|
||||
changeChainConnectionState(chainId, ConnectionState.FULL_SYNC)
|
||||
}
|
||||
|
||||
suspend fun changeChainConnectionState(chainId: ChainId, state: ConnectionState) {
|
||||
val connectionState = mapConnectionStateToLocal(state)
|
||||
chainDao.setConnectionState(chainId, connectionState)
|
||||
}
|
||||
|
||||
suspend fun setWssNodeSelectionStrategy(chainId: String, strategy: Chain.Nodes.NodeSelectionStrategy) {
|
||||
return when (strategy) {
|
||||
Chain.Nodes.NodeSelectionStrategy.AutoBalance -> enableAutoBalance(chainId)
|
||||
is Chain.Nodes.NodeSelectionStrategy.SelectedNode -> setSelectedNode(chainId, strategy.unformattedNodeUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enableAutoBalance(chainId: ChainId) {
|
||||
chainDao.setNodePreferences(NodeSelectionPreferencesLocal(chainId, autoBalanceEnabled = false, null))
|
||||
}
|
||||
|
||||
private suspend fun setSelectedNode(chainId: ChainId, unformattedNodeUrl: String) {
|
||||
val chain = getChain(chainId)
|
||||
|
||||
val chainSupportsNode = chain.nodes.nodes.any { it.unformattedUrl == unformattedNodeUrl }
|
||||
require(chainSupportsNode) { "Node with url $unformattedNodeUrl is not found for chain $chainId" }
|
||||
|
||||
chainDao.setNodePreferences(NodeSelectionPreferencesLocal(chainId, false, unformattedNodeUrl))
|
||||
}
|
||||
|
||||
private suspend fun requireConnectionStateAtLeast(chainId: ChainId, state: ConnectionState) {
|
||||
val chain = getChain(chainId)
|
||||
|
||||
if (chain.isDisabled) throw DisabledChainException()
|
||||
if (chain.connectionState.level >= state.level) return
|
||||
|
||||
Log.d("ConnectionState", "Requested state $state for ${chain.name}, current is ${chain.connectionState}. Triggering state change to $state")
|
||||
|
||||
chainDao.setConnectionState(chainId, mapConnectionStateToLocal(state))
|
||||
awaitConnectionStateIsAtLeast(chainId, state)
|
||||
}
|
||||
|
||||
private fun syncChainsAndAssets() {
|
||||
launch {
|
||||
runCatching {
|
||||
chainSyncService.syncUp()
|
||||
evmAssetsSyncService.syncUp()
|
||||
}.onFailure {
|
||||
Log.e(LOG_TAG, "Failed to sync chains or assets", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitConnectionStateIsAtLeast(chainId: ChainId, state: ConnectionState) {
|
||||
chainsById
|
||||
.mapNotNull { chainsById -> chainsById[chainId] }
|
||||
.first { it.connectionState.level >= state.level }
|
||||
}
|
||||
|
||||
private fun unregisterChain(chain: Chain) {
|
||||
unregisterSubstrateServices(chain)
|
||||
unregisterConnections(chain.id)
|
||||
}
|
||||
|
||||
private suspend fun registerChain(chain: Chain) {
|
||||
return when (chain.connectionState) {
|
||||
ConnectionState.FULL_SYNC -> registerFullSyncChain(chain)
|
||||
ConnectionState.LIGHT_SYNC -> registerLightSyncChain(chain)
|
||||
ConnectionState.DISABLED -> registerDisabledChain(chain)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerDisabledChain(chain: Chain) {
|
||||
unregisterSubstrateServices(chain)
|
||||
unregisterConnections(chain.id)
|
||||
}
|
||||
|
||||
private suspend fun registerLightSyncChain(chain: Chain) {
|
||||
registerConnection(chain)
|
||||
|
||||
unregisterSubstrateServices(chain)
|
||||
}
|
||||
|
||||
private suspend fun registerFullSyncChain(chain: Chain) {
|
||||
val connection = registerConnection(chain)
|
||||
|
||||
if (chain.hasSubstrateRuntime) {
|
||||
runtimeProviderPool.setupRuntimeProvider(chain)
|
||||
runtimeSyncService.registerChain(chain, connection)
|
||||
runtimeSubscriptionPool.setupRuntimeSubscription(chain, connection)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun registerConnection(chain: Chain): ChainConnection {
|
||||
val connection = connectionPool.setupConnection(chain)
|
||||
|
||||
if (chain.isEthereumBased) {
|
||||
web3ApiPool.setupWssApi(chain.id, connection.socketService)
|
||||
web3ApiPool.setupHttpsApi(chain)
|
||||
}
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
private fun syncBaseTypesIfNeeded() = launch {
|
||||
val chains = currentChains.first()
|
||||
val needToSyncBaseTypes = chains.any { it.typesUsage.requiresBaseTypes && it.connectionState.shouldSyncRuntime() }
|
||||
|
||||
if (needToSyncBaseTypes) {
|
||||
baseTypeSynchronizer.sync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterSubstrateServices(chain: Chain) {
|
||||
if (chain.hasSubstrateRuntime) {
|
||||
runtimeProviderPool.removeRuntimeProvider(chain.id)
|
||||
runtimeSubscriptionPool.removeSubscription(chain.id)
|
||||
runtimeSyncService.unregisterChain(chain.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterConnections(chainId: ChainId) {
|
||||
connectionPool.removeConnection(chainId)
|
||||
web3ApiPool.removeApis(chainId)
|
||||
}
|
||||
|
||||
private fun ConnectionState.shouldSyncRuntime(): Boolean {
|
||||
return isFullSync
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.getChainOrNull(chainId: String): Chain? {
|
||||
return chainsById.first()[chainId.removeHexPrefix()]
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.chainWithAssetOrNull(chainId: String, assetId: Int): ChainWithAsset? {
|
||||
val chain = getChainOrNull(chainId) ?: return null
|
||||
val chainAsset = chain.assetsById[assetId] ?: return null
|
||||
|
||||
return ChainWithAsset(chain, chainAsset)
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.enabledChainWithAssetOrNull(chainId: String, assetId: Int): ChainWithAsset? {
|
||||
val chain = getChainOrNull(chainId).takeIf { it?.isEnabled == true } ?: return null
|
||||
val chainAsset = chain.assetsById[assetId] ?: return null
|
||||
|
||||
return ChainWithAsset(chain, chainAsset)
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.assetOrNull(fullChainAssetId: FullChainAssetId): Chain.Asset? {
|
||||
val chain = getChainOrNull(fullChainAssetId.chainId) ?: return null
|
||||
|
||||
return chain.assetsById[fullChainAssetId.assetId]
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.chainWithAsset(chainId: String, assetId: Int): ChainWithAsset {
|
||||
val chain = chainsById.first().getValue(chainId)
|
||||
|
||||
return ChainWithAsset(chain, chain.assetsById.getValue(assetId))
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.chainWithAsset(fullChainAssetId: FullChainAssetId): ChainWithAsset {
|
||||
return chainWithAsset(fullChainAssetId.chainId, fullChainAssetId.assetId)
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.asset(chainId: String, assetId: Int): Chain.Asset {
|
||||
val chain = chainsById.first().getValue(chainId)
|
||||
|
||||
return chain.assetsById.getValue(assetId)
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.asset(fullChainAssetId: FullChainAssetId): Chain.Asset {
|
||||
return asset(fullChainAssetId.chainId, fullChainAssetId.assetId)
|
||||
}
|
||||
|
||||
fun ChainsById.assets(ids: Collection<FullChainAssetId>): List<Chain.Asset> {
|
||||
return ids.map { (chainId, assetId) ->
|
||||
getValue(chainId).assetsById.getValue(assetId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <R> ChainRegistry.withRuntime(chainId: ChainId, action: RuntimeContext.() -> R): R {
|
||||
return getRuntime(chainId).provideContext(action)
|
||||
}
|
||||
|
||||
suspend inline fun ChainRegistry.findChain(predicate: (Chain) -> Boolean): Chain? = currentChains.first().firstOrNull(predicate)
|
||||
suspend inline fun ChainRegistry.findChains(predicate: (Chain) -> Boolean): List<Chain> = currentChains.first().filter(predicate)
|
||||
|
||||
suspend inline fun ChainRegistry.findEnabledChains(predicate: (Chain) -> Boolean): List<Chain> = enabledChains().filter(predicate)
|
||||
|
||||
suspend inline fun ChainRegistry.findChainIds(predicate: (Chain) -> Boolean): Set<ChainId> = currentChains.first().mapNotNullToSet { chain ->
|
||||
chain.id.takeIf { predicate(chain) }
|
||||
}
|
||||
|
||||
suspend inline fun ChainRegistry.findChainsById(predicate: (Chain) -> Boolean): ChainsById {
|
||||
return chainsById().filterValues { chain -> predicate(chain) }.asChainsById()
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.getRuntime(chainId: String) = getRuntimeProvider(chainId).get()
|
||||
|
||||
suspend fun ChainRegistry.getRawMetadata(chainId: String) = getRuntimeProvider(chainId).getRaw()
|
||||
|
||||
suspend fun ChainRegistry.getSocket(chainId: String): SocketService = getActiveConnection(chainId).socketService
|
||||
|
||||
suspend fun ChainRegistry.getSocketOrNull(chainId: String): SocketService? = getActiveConnectionOrNull(chainId)?.socketService
|
||||
|
||||
suspend fun ChainRegistry.getEthereumApiOrThrow(chainId: String, connectionType: ConnectionType): Web3Api {
|
||||
return requireNotNull(getEthereumApi(chainId, connectionType)) {
|
||||
"Ethereum Api is not found for chain $chainId and connection type ${connectionType.name}"
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.getSubscriptionEthereumApiOrThrow(chainId: String): Web3Api {
|
||||
return getEthereumApiOrThrow(chainId, ConnectionType.WSS)
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.getSubscriptionEthereumApi(chainId: String): Web3Api? {
|
||||
return getEthereumApi(chainId, ConnectionType.WSS)
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.getCallEthereumApiOrThrow(chainId: String): Web3Api {
|
||||
return getEthereumApi(chainId, ConnectionType.HTTPS)
|
||||
?: getEthereumApiOrThrow(chainId, ConnectionType.WSS)
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.getCallEthereumApi(chainId: String): Web3Api? {
|
||||
return getEthereumApi(chainId, ConnectionType.HTTPS)
|
||||
?: getEthereumApi(chainId, ConnectionType.WSS)
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.chainsById(): ChainsById = ChainsById(chainsById.first())
|
||||
|
||||
suspend fun ChainRegistry.findEvmChain(evmChainId: Int): Chain? {
|
||||
return findChain { it.isEthereumBased && it.addressPrefix == evmChainId }
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.findEvmCallApi(evmChainId: Int): Web3Api? {
|
||||
return findEvmChain(evmChainId)?.let {
|
||||
getCallEthereumApi(it.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.findEvmChainFromHexId(evmChainIdHex: String): Chain? {
|
||||
val addressPrefix = evmChainIdHex.removeHexPrefix().toIntOrNull(radix = 16) ?: return null
|
||||
|
||||
return findEvmChain(addressPrefix)
|
||||
}
|
||||
|
||||
suspend fun ChainRegistry.findRelayChainOrThrow(chainId: ChainId): ChainId {
|
||||
val chain = getChain(chainId)
|
||||
return chain.parentId ?: chainId
|
||||
}
|
||||
|
||||
fun ChainRegistry.chainFlow(chainId: ChainId): Flow<Chain> {
|
||||
return chainsById.mapNotNull { it[chainId] }
|
||||
}
|
||||
|
||||
fun ChainRegistry.enabledChainsFlow() = currentChains
|
||||
.filterList { it.isEnabled }
|
||||
|
||||
suspend fun ChainRegistry.enabledChains() = enabledChainsFlow().first()
|
||||
|
||||
suspend fun ChainRegistry.disabledChains() = currentChains.filterList { it.isDisabled }
|
||||
.first()
|
||||
|
||||
fun ChainRegistry.enabledChainByIdFlow() = enabledChainsFlow().map { chains -> chains.associateBy { it.id } }
|
||||
|
||||
suspend fun ChainRegistry.enabledChainById() = ChainsById(enabledChainByIdFlow().first())
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork
|
||||
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
@JvmInline
|
||||
value class ChainsById(val value: Map<ChainId, Chain>) : Map<ChainId, Chain> by value {
|
||||
|
||||
override operator fun get(key: ChainId): Chain? {
|
||||
return value[key.removeHexPrefix()]
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun Map<ChainId, Chain>.asChainsById(): ChainsById {
|
||||
return ChainsById(this)
|
||||
}
|
||||
|
||||
fun ChainsById.assetOrNull(id: FullChainAssetId): Chain.Asset? {
|
||||
return get(id.chainId)?.assetsById?.get(id.assetId)
|
||||
}
|
||||
|
||||
fun ChainsById.chainWithAssetOrNull(id: FullChainAssetId): ChainWithAsset? {
|
||||
val chain = get(id.chainId) ?: return null
|
||||
val asset = chain.assetsById[id.assetId] ?: return null
|
||||
|
||||
return ChainWithAsset(chain, asset)
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.asset
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.CollectionDiffer
|
||||
import io.novafoundation.nova.common.utils.retryUntilDone
|
||||
import io.novafoundation.nova.core_db.dao.ChainAssetDao
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
import io.novafoundation.nova.core_db.ext.fullId
|
||||
import io.novafoundation.nova.core_db.model.chain.AssetSourceLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal.Companion.ENABLED_DEFAULT_BOOL
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset.remote.AssetFetcher
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapEVMAssetRemoteToLocalAssets
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class EvmAssetsSyncService(
|
||||
private val chainDao: ChainDao,
|
||||
private val chainAssetDao: ChainAssetDao,
|
||||
private val chainFetcher: AssetFetcher,
|
||||
private val gson: Gson,
|
||||
) {
|
||||
|
||||
suspend fun syncUp() = withContext(Dispatchers.Default) {
|
||||
syncEVMAssets()
|
||||
}
|
||||
|
||||
private suspend fun syncEVMAssets() {
|
||||
val availableChainIds = chainDao.getAllChainIds().toSet()
|
||||
|
||||
val oldAssets = chainAssetDao.getAssetsBySource(AssetSourceLocal.ERC20)
|
||||
val associatedOldAssets = oldAssets.associateBy { it.fullId() }
|
||||
|
||||
val newAssets = retryUntilDone { chainFetcher.getEVMAssets() }
|
||||
.flatMap { mapEVMAssetRemoteToLocalAssets(it, gson) }
|
||||
.mapNotNull { new ->
|
||||
// handle misconfiguration between chains.json and assets.json when assets contains asset for chain that is not present in chain
|
||||
if (new.chainId !in availableChainIds) return@mapNotNull null
|
||||
|
||||
val old = associatedOldAssets[new.fullId()]
|
||||
new.copy(enabled = old?.enabled ?: ENABLED_DEFAULT_BOOL)
|
||||
}
|
||||
|
||||
val diff = CollectionDiffer.findDiff(newAssets, oldAssets, forceUseNewItems = false)
|
||||
chainAssetDao.updateAssets(diff)
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.asset.remote
|
||||
|
||||
import io.novafoundation.nova.runtime.BuildConfig
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset.remote.model.EVMAssetRemote
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface AssetFetcher {
|
||||
|
||||
@GET(BuildConfig.EVM_ASSETS_URL)
|
||||
suspend fun getEVMAssets(): List<EVMAssetRemote>
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.asset.remote.model
|
||||
|
||||
data class EVMAssetRemote(
|
||||
val symbol: String,
|
||||
val precision: Int,
|
||||
val priceId: String?,
|
||||
val name: String,
|
||||
val icon: String?,
|
||||
val instances: List<EVMInstanceRemote>
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.asset.remote.model
|
||||
|
||||
class EVMInstanceRemote(
|
||||
val chainId: String,
|
||||
val contractAddress: String,
|
||||
val buyProviders: Map<String, Map<String, Any>>?,
|
||||
val sellProviders: Map<String, Map<String, Any>>?
|
||||
)
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.CollectionDiffer
|
||||
import io.novafoundation.nova.common.utils.retryUntilDone
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
import io.novafoundation.nova.core_db.dao.FullAssetIdLocal
|
||||
import io.novafoundation.nova.core_db.ext.fullId
|
||||
import io.novafoundation.nova.core_db.model.chain.AssetSourceLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal.Companion.ENABLED_DEFAULT_BOOL
|
||||
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.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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ChainSyncService(
|
||||
private val chainDao: ChainDao,
|
||||
private val chainFetcher: ChainFetcher,
|
||||
private val gson: Gson
|
||||
) {
|
||||
|
||||
suspend fun syncUp() = withContext(Dispatchers.Default) {
|
||||
val localChainsJoinedInfo = chainDao.getJoinChainInfo().filter { it.chain.source != ChainLocal.Source.CUSTOM }
|
||||
val oldChains = localChainsJoinedInfo.map { it.chain }
|
||||
val oldAssets = localChainsJoinedInfo.flatMap { it.assets }.filter { it.source == AssetSourceLocal.DEFAULT }
|
||||
val oldNodes = localChainsJoinedInfo.flatMap { it.nodes }.filter { it.source != ChainNodeLocal.Source.CUSTOM }
|
||||
val oldExplorers = localChainsJoinedInfo.flatMap { it.explorers }
|
||||
val oldExternalApis = localChainsJoinedInfo.flatMap { it.externalApis }
|
||||
val oldNodeSelectionPreferences = localChainsJoinedInfo.mapNotNull { it.nodeSelectionPreferences }
|
||||
|
||||
val oldChainsById = oldChains.associateBy { it.id }
|
||||
val associatedOldAssets = oldAssets.associateBy { it.fullId() }
|
||||
|
||||
val remoteChains = retryUntilDone { chainFetcher.getChains() }
|
||||
|
||||
val newChains = remoteChains.map { mapRemoteChainToLocal(it, oldChainsById[it.chainId], source = ChainLocal.Source.DEFAULT, gson) }
|
||||
val newAssets = remoteChains.flatMap { chain ->
|
||||
chain.assets.map {
|
||||
val fullAssetId = FullAssetIdLocal(chain.chainId, it.assetId)
|
||||
val oldAsset = associatedOldAssets[fullAssetId]
|
||||
mapRemoteAssetToLocal(chain, it, gson, oldAsset?.enabled ?: ENABLED_DEFAULT_BOOL)
|
||||
}
|
||||
}
|
||||
val newNodes = remoteChains.flatMap(::mapRemoteNodesToLocal)
|
||||
val newExplorers = remoteChains.flatMap(::mapRemoteExplorersToLocal)
|
||||
val newExternalApis = remoteChains.flatMap(::mapExternalApisToLocal)
|
||||
val newNodeSelectionPreferences = nodeSelectionPreferencesFor(newChains, oldNodeSelectionPreferences)
|
||||
|
||||
val chainsDiff = CollectionDiffer.findDiff(newChains, oldChains, forceUseNewItems = false)
|
||||
val assetDiff = CollectionDiffer.findDiff(newAssets, oldAssets, forceUseNewItems = false)
|
||||
val nodesDiff = CollectionDiffer.findDiff(newNodes, oldNodes, forceUseNewItems = false)
|
||||
val explorersDiff = CollectionDiffer.findDiff(newExplorers, oldExplorers, forceUseNewItems = false)
|
||||
val externalApisDiff = CollectionDiffer.findDiff(newExternalApis, oldExternalApis, forceUseNewItems = false)
|
||||
val nodeSelectionPreferencesDiff = CollectionDiffer.findDiff(newNodeSelectionPreferences, oldNodeSelectionPreferences, forceUseNewItems = false)
|
||||
|
||||
chainDao.applyDiff(
|
||||
chainDiff = chainsDiff,
|
||||
assetsDiff = assetDiff,
|
||||
nodesDiff = nodesDiff,
|
||||
explorersDiff = explorersDiff,
|
||||
externalApisDiff = externalApisDiff,
|
||||
nodeSelectionPreferencesDiff = nodeSelectionPreferencesDiff
|
||||
)
|
||||
}
|
||||
|
||||
private fun nodeSelectionPreferencesFor(
|
||||
newChains: List<ChainLocal>,
|
||||
oldNodeSelectionPreferences: List<NodeSelectionPreferencesLocal>
|
||||
): List<NodeSelectionPreferencesLocal> {
|
||||
val preferencesById = oldNodeSelectionPreferences.associateBy { it.chainId }
|
||||
return newChains.map {
|
||||
preferencesById[it.id]
|
||||
?: NodeSelectionPreferencesLocal(
|
||||
chainId = it.id,
|
||||
autoBalanceEnabled = NodeSelectionPreferencesLocal.DEFAULT_AUTO_BALANCE_BOOLEAN,
|
||||
selectedNodeUrl = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user