mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-24 15:57:55 +00:00
Initial commit: Pezkuwi Wallet Android
Complete rebrand of Nova Wallet for Pezkuwichain ecosystem. ## Features - Full Pezkuwichain support (HEZ & PEZ tokens) - Polkadot ecosystem compatibility - Staking, Governance, DeFi, NFTs - XCM cross-chain transfers - Hardware wallet support (Ledger, Polkadot Vault) - WalletConnect v2 - Push notifications ## Languages - English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese Based on Nova Wallet by Novasama Technologies GmbH © Dijital Kurdistan Tech Institute 2026
This commit is contained in:
@@ -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,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,640 @@
|
||||
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 {
|
||||
|
||||
const val KUSAMA = "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe"
|
||||
const val POLKADOT = "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"
|
||||
const val WESTEND = "e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e"
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.runtime.ext
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
val Chain.mainChainsFirstAscendingOrder
|
||||
get() = when (genesisHash) {
|
||||
Chain.Geneses.POLKADOT_ASSET_HUB -> 0
|
||||
Chain.Geneses.KUSAMA_ASSET_HUB -> 1
|
||||
else -> 2
|
||||
}
|
||||
|
||||
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,20 @@
|
||||
package io.novafoundation.nova.runtime.ext
|
||||
|
||||
import io.novafoundation.nova.common.utils.TokenSymbol
|
||||
|
||||
val TokenSymbol.mainTokensFirstAscendingOrder
|
||||
get() = when (this.value) {
|
||||
"DOT" -> 0
|
||||
"KSM" -> 1
|
||||
"USDT" -> 2
|
||||
"USDC" -> 3
|
||||
else -> 4
|
||||
}
|
||||
|
||||
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 }
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.ChargeAssetTxPayment
|
||||
import io.novafoundation.nova.runtime.extrinsic.extensions.CheckAppId
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.TransactionExtension
|
||||
|
||||
object CustomTransactionExtensions {
|
||||
|
||||
fun applyDefaultValues(builder: ExtrinsicBuilder) {
|
||||
defaultValues().forEach(builder::setTransactionExtension)
|
||||
}
|
||||
|
||||
fun defaultValues(): List<TransactionExtension> {
|
||||
return listOf(
|
||||
ChargeAssetTxPayment(),
|
||||
CheckAppId()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.novafoundation.nova.runtime.extrinsic
|
||||
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.runtime.ext.requireGenesisHash
|
||||
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.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
|
||||
|
||||
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)
|
||||
|
||||
val mortality = mortalityConstructor.constructMortality(chain.id)
|
||||
val metadataProof = metadataShortenerService.generateMetadataProof(chain.id)
|
||||
|
||||
return generateSequence {
|
||||
ExtrinsicBuilder(
|
||||
runtime = runtime,
|
||||
extrinsicVersion = ExtrinsicVersion.V4,
|
||||
batchMode = options.batchMode,
|
||||
).apply {
|
||||
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().forEach(::setTransactionExtension)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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)
|
||||
}
|
||||
}
|
||||
+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
|
||||
)
|
||||
+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
|
||||
)
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
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 canBeEnabled = chain.additional.shouldDisableMetadataHashCheck().not()
|
||||
val atLeastMinimumVersion = runtimeMetadata.metadataVersion >= MINIMUM_METADATA_VERSION_TO_CALCULATE_HASH
|
||||
val hasSignedExtension = runtimeMetadata.extrinsic.hasSignedExtension(DefaultSignedExtensions.CHECK_METADATA_HASH)
|
||||
|
||||
return canBeEnabled && atLeastMinimumVersion && hasSignedExtension
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers
|
||||
|
||||
const val ASSET_NATIVE = "native"
|
||||
const val ASSET_STATEMINE = "statemine"
|
||||
const val ASSET_ORML = "orml"
|
||||
const val ASSET_ORML_HYDRATION_EVM = "orml-hydration-evm"
|
||||
const val ASSET_UNSUPPORTED = "unsupported"
|
||||
|
||||
const val ASSET_EVM_ERC20 = "evm"
|
||||
const val ASSET_EVM_NATIVE = "evmNative"
|
||||
|
||||
const val ASSET_EQUILIBRIUM = "equilibrium"
|
||||
const val ASSET_EQUILIBRIUM_ON_CHAIN_ID = "assetId"
|
||||
|
||||
const val STATEMINE_EXTRAS_ID = "assetId"
|
||||
const val STATEMINE_EXTRAS_PALLET_NAME = "palletName"
|
||||
const val STATEMINE_IS_SUFFICIENT = "isSufficient"
|
||||
|
||||
const val STATEMINE_IS_SUFFICIENT_DEFAULT = false
|
||||
|
||||
const val ORML_EXTRAS_CURRENCY_ID_SCALE = "currencyIdScale"
|
||||
const val ORML_EXTRAS_CURRENCY_TYPE = "currencyIdType"
|
||||
const val ORML_EXTRAS_EXISTENTIAL_DEPOSIT = "existentialDeposit"
|
||||
const val ORML_EXTRAS_TRANSFERS_ENABLED = "transfersEnabled"
|
||||
|
||||
const val EVM_EXTRAS_CONTRACT_ADDRESS = "contractAddress"
|
||||
|
||||
const val ORML_TRANSFERS_ENABLED_DEFAULT = true
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
fun mapSwapListToLocal(swap: List<Chain.Swap>) = swap.joinToString(separator = ",", transform = Chain.Swap::name)
|
||||
|
||||
fun mapGovernanceListToLocal(governance: List<Chain.Governance>) = governance.joinToString(separator = ",", transform = Chain.Governance::name)
|
||||
|
||||
fun mapCustomFeeToLocal(customFees: List<Chain.CustomFee>) = customFees.joinToString(separator = ",", transform = Chain.CustomFee::name)
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.asGsonParsedNumber
|
||||
import io.novafoundation.nova.core_db.model.chain.AssetSourceLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExplorerLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal.SourceType
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal.Companion.EMPTY_CHAIN_ICON
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal.ConnectionStateLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainNodeLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.NodeSelectionPreferencesLocal
|
||||
import io.novafoundation.nova.runtime.ext.autoBalanceEnabled
|
||||
import io.novafoundation.nova.runtime.ext.selectedUnformattedWssNodeUrlOrNull
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.EVM_TRANSFER_PARAMETER
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.GovernanceReferendaParameters
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.SUBSTRATE_TRANSFER_PARAMETER
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.TransferParameters
|
||||
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.ExternalApi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.StatemineAssetId
|
||||
|
||||
fun mapStakingTypeToLocal(stakingType: Chain.Asset.StakingType): String = stakingType.name
|
||||
|
||||
fun mapStakingTypesToLocal(stakingTypes: List<Chain.Asset.StakingType>): String {
|
||||
return stakingTypes.joinToString(separator = ",", transform = ::mapStakingTypeToLocal)
|
||||
}
|
||||
|
||||
private fun mapAssetSourceToLocal(source: Chain.Asset.Source): AssetSourceLocal {
|
||||
return when (source) {
|
||||
Chain.Asset.Source.DEFAULT -> AssetSourceLocal.DEFAULT
|
||||
Chain.Asset.Source.ERC20 -> AssetSourceLocal.ERC20
|
||||
Chain.Asset.Source.MANUAL -> AssetSourceLocal.MANUAL
|
||||
}
|
||||
}
|
||||
|
||||
fun mapChainAssetTypeToRaw(type: Chain.Asset.Type): Pair<String, Map<String, Any?>?> = when (type) {
|
||||
is Chain.Asset.Type.Native -> ASSET_NATIVE to null
|
||||
|
||||
is Chain.Asset.Type.Statemine -> ASSET_STATEMINE to mapOf(
|
||||
STATEMINE_EXTRAS_ID to mapStatemineAssetIdToRaw(type.id),
|
||||
STATEMINE_EXTRAS_PALLET_NAME to type.palletName,
|
||||
STATEMINE_IS_SUFFICIENT to type.isSufficient
|
||||
)
|
||||
|
||||
is Chain.Asset.Type.Orml -> ASSET_ORML to mapOf(
|
||||
ORML_EXTRAS_CURRENCY_ID_SCALE to type.currencyIdScale,
|
||||
ORML_EXTRAS_CURRENCY_TYPE to type.currencyIdType,
|
||||
ORML_EXTRAS_EXISTENTIAL_DEPOSIT to type.existentialDeposit.toString(),
|
||||
ORML_EXTRAS_TRANSFERS_ENABLED to type.transfersEnabled
|
||||
)
|
||||
|
||||
is Chain.Asset.Type.EvmErc20 -> ASSET_EVM_ERC20 to mapOf(
|
||||
EVM_EXTRAS_CONTRACT_ADDRESS to type.contractAddress
|
||||
)
|
||||
|
||||
is Chain.Asset.Type.EvmNative -> ASSET_EVM_NATIVE to null
|
||||
|
||||
is Chain.Asset.Type.Equilibrium -> ASSET_EQUILIBRIUM to mapOf(
|
||||
ASSET_EQUILIBRIUM_ON_CHAIN_ID to type.id.toString()
|
||||
)
|
||||
|
||||
Chain.Asset.Type.Unsupported -> ASSET_UNSUPPORTED to null
|
||||
}
|
||||
|
||||
private fun mapStatemineAssetIdToRaw(statemineAssetId: StatemineAssetId): String {
|
||||
return when (statemineAssetId) {
|
||||
is StatemineAssetId.Number -> statemineAssetId.value.toString()
|
||||
is StatemineAssetId.ScaleEncoded -> statemineAssetId.scaleHex
|
||||
}
|
||||
}
|
||||
|
||||
fun mapStatemineAssetIdFromRaw(rawValue: Any): StatemineAssetId {
|
||||
val asString = rawValue as? String ?: error("Invalid format")
|
||||
|
||||
return if (asString.startsWith("0x")) {
|
||||
StatemineAssetId.ScaleEncoded(asString)
|
||||
} else {
|
||||
StatemineAssetId.Number(asString.asGsonParsedNumber())
|
||||
}
|
||||
}
|
||||
|
||||
fun mapChainAssetToLocal(asset: Chain.Asset, gson: Gson): ChainAssetLocal {
|
||||
val (type, typeExtras) = mapChainAssetTypeToRaw(asset.type)
|
||||
|
||||
return ChainAssetLocal(
|
||||
id = asset.id,
|
||||
symbol = asset.symbol.value,
|
||||
precision = asset.precision.value,
|
||||
chainId = asset.chainId,
|
||||
name = asset.name,
|
||||
priceId = asset.priceId,
|
||||
staking = mapStakingTypesToLocal(asset.staking),
|
||||
type = type,
|
||||
source = mapAssetSourceToLocal(asset.source),
|
||||
buyProviders = gson.toJson(asset.buyProviders),
|
||||
sellProviders = gson.toJson(asset.sellProviders),
|
||||
typeExtras = gson.toJson(typeExtras),
|
||||
icon = asset.icon,
|
||||
enabled = asset.enabled
|
||||
)
|
||||
}
|
||||
|
||||
fun mapChainToLocal(chain: Chain, gson: Gson): ChainLocal {
|
||||
val types = chain.types?.let {
|
||||
ChainLocal.TypesConfig(
|
||||
url = it.url.orEmpty(),
|
||||
overridesCommon = it.overridesCommon
|
||||
)
|
||||
}
|
||||
|
||||
return ChainLocal(
|
||||
id = chain.id,
|
||||
parentId = chain.parentId,
|
||||
name = chain.name,
|
||||
icon = chain.icon ?: EMPTY_CHAIN_ICON,
|
||||
types = types,
|
||||
prefix = chain.addressPrefix,
|
||||
legacyPrefix = chain.legacyAddressPrefix,
|
||||
isEthereumBased = chain.isEthereumBased,
|
||||
isTestNet = chain.isTestNet,
|
||||
hasSubstrateRuntime = chain.hasSubstrateRuntime,
|
||||
pushSupport = chain.pushSupport,
|
||||
hasCrowdloans = chain.hasCrowdloans,
|
||||
multisigSupport = chain.multisigSupport,
|
||||
supportProxy = chain.supportProxy,
|
||||
swap = mapSwapListToLocal(chain.swap),
|
||||
customFee = mapCustomFeeToLocal(chain.customFee),
|
||||
governance = mapGovernanceListToLocal(chain.governance),
|
||||
additional = chain.additional?.let { gson.toJson(it) },
|
||||
connectionState = mapConnectionStateToLocal(chain.connectionState),
|
||||
nodeSelectionStrategy = mapNodeSelectionStrategyToLocal(chain),
|
||||
source = mapChainSourceToLocal(chain.source)
|
||||
)
|
||||
}
|
||||
|
||||
fun mapChainNodeToLocal(node: Chain.Node): ChainNodeLocal {
|
||||
return ChainNodeLocal(
|
||||
chainId = node.chainId,
|
||||
url = node.unformattedUrl,
|
||||
name = node.name,
|
||||
orderId = node.orderId,
|
||||
source = if (node.isCustom) ChainNodeLocal.Source.CUSTOM else ChainNodeLocal.Source.DEFAULT,
|
||||
)
|
||||
}
|
||||
|
||||
fun mapChainExplorerToLocal(explorer: Chain.Explorer): ChainExplorerLocal {
|
||||
return ChainExplorerLocal(
|
||||
chainId = explorer.chainId,
|
||||
name = explorer.name,
|
||||
extrinsic = explorer.extrinsic,
|
||||
account = explorer.account,
|
||||
event = explorer.event,
|
||||
)
|
||||
}
|
||||
|
||||
fun mapNodeSelectionPreferencesToLocal(chain: Chain): NodeSelectionPreferencesLocal {
|
||||
return NodeSelectionPreferencesLocal(
|
||||
chainId = chain.id,
|
||||
autoBalanceEnabled = chain.autoBalanceEnabled,
|
||||
selectedNodeUrl = chain.selectedUnformattedWssNodeUrlOrNull
|
||||
)
|
||||
}
|
||||
|
||||
fun mapNodeSelectionStrategyToLocal(domain: Chain): ChainLocal.AutoBalanceStrategyLocal {
|
||||
return when (domain.nodes.autoBalanceStrategy) {
|
||||
Chain.Nodes.AutoBalanceStrategy.ROUND_ROBIN -> ChainLocal.AutoBalanceStrategyLocal.ROUND_ROBIN
|
||||
Chain.Nodes.AutoBalanceStrategy.UNIFORM -> ChainLocal.AutoBalanceStrategyLocal.UNIFORM
|
||||
}
|
||||
}
|
||||
|
||||
fun mapChainSourceToLocal(domain: Chain.Source): ChainLocal.Source {
|
||||
return when (domain) {
|
||||
Chain.Source.CUSTOM -> ChainLocal.Source.CUSTOM
|
||||
Chain.Source.DEFAULT -> ChainLocal.Source.DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
fun mapConnectionStateToLocal(domain: ConnectionState): ConnectionStateLocal {
|
||||
return when (domain) {
|
||||
ConnectionState.FULL_SYNC -> ConnectionStateLocal.FULL_SYNC
|
||||
ConnectionState.LIGHT_SYNC -> ConnectionStateLocal.LIGHT_SYNC
|
||||
ConnectionState.DISABLED -> ConnectionStateLocal.DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
fun mapChainExternalApiToLocal(gson: Gson, chainId: String, api: ExternalApi): ChainExternalApiLocal {
|
||||
return when (api) {
|
||||
is ExternalApi.Transfers -> mapExternalApiTransfers(gson, chainId, api)
|
||||
is ExternalApi.Crowdloans -> mapExternalApiCrowdloans(chainId, api)
|
||||
is ExternalApi.GovernanceDelegations -> mapExternalApiGovernanceDelegations(chainId, api)
|
||||
is ExternalApi.GovernanceReferenda -> mapExternalApiGovernanceReferenda(gson, chainId, api)
|
||||
is ExternalApi.Staking -> mapExternalApiStaking(chainId, api)
|
||||
is ExternalApi.StakingRewards -> mapExternalApiStakingRewards(chainId, api)
|
||||
is ExternalApi.ReferendumSummary -> mapExternalApiReferendumSummary(chainId, api)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapExternalApiTransfers(gson: Gson, chainId: String, api: ExternalApi.Transfers): ChainExternalApiLocal {
|
||||
fun transferParametersByType(gson: Gson, type: String): String {
|
||||
return gson.toJson(TransferParameters(type))
|
||||
}
|
||||
|
||||
val parameters = when (api) {
|
||||
is ExternalApi.Transfers.Evm -> transferParametersByType(gson, EVM_TRANSFER_PARAMETER)
|
||||
is ExternalApi.Transfers.Substrate -> transferParametersByType(gson, SUBSTRATE_TRANSFER_PARAMETER)
|
||||
}
|
||||
|
||||
return ChainExternalApiLocal(
|
||||
chainId = chainId,
|
||||
sourceType = SourceType.UNKNOWN,
|
||||
apiType = ChainExternalApiLocal.ApiType.TRANSFERS,
|
||||
parameters = parameters,
|
||||
url = api.url
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapExternalApiCrowdloans(chainId: String, api: ExternalApi.Crowdloans): ChainExternalApiLocal {
|
||||
return ChainExternalApiLocal(
|
||||
chainId = chainId,
|
||||
sourceType = SourceType.GITHUB,
|
||||
apiType = ChainExternalApiLocal.ApiType.CROWDLOANS,
|
||||
parameters = null,
|
||||
url = api.url
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapExternalApiGovernanceDelegations(chainId: String, api: ExternalApi.GovernanceDelegations): ChainExternalApiLocal {
|
||||
return ChainExternalApiLocal(
|
||||
chainId = chainId,
|
||||
sourceType = SourceType.SUBQUERY,
|
||||
apiType = ChainExternalApiLocal.ApiType.GOVERNANCE_DELEGATIONS,
|
||||
parameters = null,
|
||||
url = api.url
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapExternalApiGovernanceReferenda(gson: Gson, chainId: String, api: ExternalApi.GovernanceReferenda): ChainExternalApiLocal {
|
||||
val (source, parameters) = when (api.source) {
|
||||
ExternalApi.GovernanceReferenda.Source.SubSquare -> SourceType.SUBSQUARE to null
|
||||
|
||||
is ExternalApi.GovernanceReferenda.Source.Polkassembly -> {
|
||||
SourceType.POLKASSEMBLY to gson.toJson(GovernanceReferendaParameters(api.source.network))
|
||||
}
|
||||
}
|
||||
|
||||
return ChainExternalApiLocal(
|
||||
chainId = chainId,
|
||||
sourceType = source,
|
||||
apiType = ChainExternalApiLocal.ApiType.GOVERNANCE_REFERENDA,
|
||||
parameters = parameters,
|
||||
url = api.url
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapExternalApiStaking(chainId: String, api: ExternalApi.Staking): ChainExternalApiLocal {
|
||||
return ChainExternalApiLocal(
|
||||
chainId = chainId,
|
||||
sourceType = SourceType.SUBQUERY,
|
||||
apiType = ChainExternalApiLocal.ApiType.STAKING,
|
||||
parameters = null,
|
||||
url = api.url
|
||||
)
|
||||
}
|
||||
|
||||
fun mapExternalApiStakingRewards(chainId: String, api: ExternalApi.StakingRewards): ChainExternalApiLocal {
|
||||
return ChainExternalApiLocal(
|
||||
chainId = chainId,
|
||||
sourceType = SourceType.SUBQUERY,
|
||||
apiType = ChainExternalApiLocal.ApiType.STAKING_REWARDS,
|
||||
parameters = null,
|
||||
url = api.url
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapExternalApiReferendumSummary(chainId: String, api: ExternalApi.ReferendumSummary): ChainExternalApiLocal {
|
||||
return ChainExternalApiLocal(
|
||||
chainId = chainId,
|
||||
sourceType = SourceType.UNKNOWN,
|
||||
apiType = ChainExternalApiLocal.ApiType.REFERENDUM_SUMMARY,
|
||||
parameters = null,
|
||||
url = api.url
|
||||
)
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.ethereumAddressToAccountId
|
||||
import io.novafoundation.nova.core_db.model.chain.AssetSourceLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset.remote.model.EVMAssetRemote
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
fun chainAssetIdOfErc20Token(contractAddress: String): Int {
|
||||
return contractAddress.ethereumAddressToAccountId()
|
||||
.contentHashCode()
|
||||
}
|
||||
|
||||
fun mapEVMAssetRemoteToLocalAssets(evmAssetRemote: EVMAssetRemote, gson: Gson): List<ChainAssetLocal> {
|
||||
return evmAssetRemote.instances.map {
|
||||
val assetId = chainAssetIdOfErc20Token(it.contractAddress)
|
||||
|
||||
val domainType = Chain.Asset.Type.EvmErc20(it.contractAddress)
|
||||
val (type, typeExtras) = mapChainAssetTypeToRaw(domainType)
|
||||
|
||||
ChainAssetLocal(
|
||||
id = assetId,
|
||||
chainId = it.chainId,
|
||||
symbol = evmAssetRemote.symbol,
|
||||
name = evmAssetRemote.name,
|
||||
precision = evmAssetRemote.precision,
|
||||
priceId = evmAssetRemote.priceId,
|
||||
icon = evmAssetRemote.icon,
|
||||
staking = mapStakingTypeToLocal(Chain.Asset.StakingType.UNSUPPORTED),
|
||||
source = AssetSourceLocal.ERC20,
|
||||
type = type,
|
||||
buyProviders = gson.toJson(it.buyProviders),
|
||||
sellProviders = gson.toJson(it.sellProviders),
|
||||
typeExtras = gson.toJson(typeExtras),
|
||||
enabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.asPrecision
|
||||
import io.novafoundation.nova.common.utils.asTokenSymbol
|
||||
import io.novafoundation.nova.common.utils.enumValueOfOrNull
|
||||
import io.novafoundation.nova.common.utils.fromJson
|
||||
import io.novafoundation.nova.common.utils.fromJsonOrNull
|
||||
import io.novafoundation.nova.common.utils.nullIfEmpty
|
||||
import io.novafoundation.nova.common.utils.parseArbitraryObject
|
||||
import io.novafoundation.nova.core_db.model.chain.AssetSourceLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExplorerLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal.ApiType
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal.SourceType
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal.AutoBalanceStrategyLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal.ConnectionStateLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainNodeLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.JoinedChainInfo
|
||||
import io.novafoundation.nova.core_db.model.chain.NodeSelectionPreferencesLocal
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.EVM_TRANSFER_PARAMETER
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.GovernanceReferendaParameters
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.SUBSTRATE_TRANSFER_PARAMETER
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils.TransferParameters
|
||||
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.ExternalApi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.AutoBalanceStrategy
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.NodeSelectionStrategy
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.TradeProviderArguments
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.TradeProviderId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.Type.Orml.SubType as OrmlSubType
|
||||
|
||||
private fun mapStakingTypeFromLocal(stakingTypesLocal: String): List<Chain.Asset.StakingType> {
|
||||
if (stakingTypesLocal.isEmpty()) return emptyList()
|
||||
|
||||
return stakingTypesLocal.split(",").mapNotNull { enumValueOfOrNull<Chain.Asset.StakingType>(it) }
|
||||
}
|
||||
|
||||
fun mapAssetSourceFromLocal(source: AssetSourceLocal): Chain.Asset.Source {
|
||||
return when (source) {
|
||||
AssetSourceLocal.DEFAULT -> Chain.Asset.Source.DEFAULT
|
||||
AssetSourceLocal.ERC20 -> Chain.Asset.Source.ERC20
|
||||
AssetSourceLocal.MANUAL -> Chain.Asset.Source.MANUAL
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun unsupportedOnError(creator: () -> Chain.Asset.Type): Chain.Asset.Type {
|
||||
return runCatching(creator)
|
||||
.onFailure { Log.e("ChainMapper", "Failed to construct chain type", it) }
|
||||
.getOrDefault(Chain.Asset.Type.Unsupported)
|
||||
}
|
||||
|
||||
private fun mapChainAssetTypeFromRaw(type: String?, typeExtras: Map<String, Any?>?): Chain.Asset.Type = unsupportedOnError {
|
||||
when (type) {
|
||||
null, ASSET_NATIVE -> Chain.Asset.Type.Native
|
||||
|
||||
ASSET_STATEMINE -> {
|
||||
val idRaw = typeExtras?.get(STATEMINE_EXTRAS_ID)!!
|
||||
val id = mapStatemineAssetIdFromRaw(idRaw)
|
||||
val palletName = typeExtras[STATEMINE_EXTRAS_PALLET_NAME] as String?
|
||||
val isSufficient = typeExtras[STATEMINE_IS_SUFFICIENT] as Boolean? ?: STATEMINE_IS_SUFFICIENT_DEFAULT
|
||||
|
||||
Chain.Asset.Type.Statemine(id, palletName, isSufficient)
|
||||
}
|
||||
|
||||
ASSET_ORML, ASSET_ORML_HYDRATION_EVM -> {
|
||||
Chain.Asset.Type.Orml(
|
||||
currencyIdScale = typeExtras!![ORML_EXTRAS_CURRENCY_ID_SCALE] as String,
|
||||
currencyIdType = typeExtras[ORML_EXTRAS_CURRENCY_TYPE] as String,
|
||||
existentialDeposit = (typeExtras[ORML_EXTRAS_EXISTENTIAL_DEPOSIT] as String).toBigInteger(),
|
||||
transfersEnabled = typeExtras[ORML_EXTRAS_TRANSFERS_ENABLED] as Boolean? ?: ORML_TRANSFERS_ENABLED_DEFAULT,
|
||||
subType = determineOrmlSubtype(type)
|
||||
)
|
||||
}
|
||||
|
||||
ASSET_EVM_ERC20 -> {
|
||||
Chain.Asset.Type.EvmErc20(
|
||||
contractAddress = typeExtras!![EVM_EXTRAS_CONTRACT_ADDRESS] as String
|
||||
)
|
||||
}
|
||||
|
||||
ASSET_EVM_NATIVE -> Chain.Asset.Type.EvmNative
|
||||
|
||||
ASSET_EQUILIBRIUM -> Chain.Asset.Type.Equilibrium((typeExtras!![ASSET_EQUILIBRIUM_ON_CHAIN_ID] as String).toBigInteger())
|
||||
|
||||
else -> Chain.Asset.Type.Unsupported
|
||||
}
|
||||
}
|
||||
|
||||
private fun determineOrmlSubtype(type: String): OrmlSubType {
|
||||
return when (type) {
|
||||
ASSET_ORML -> OrmlSubType.DEFAULT
|
||||
ASSET_ORML_HYDRATION_EVM -> OrmlSubType.HYDRATION_EVM
|
||||
else -> error("Unknown orml token subtype: $type")
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> ChainExternalApiLocal.ensureSourceType(
|
||||
type: SourceType,
|
||||
action: ChainExternalApiLocal.() -> T
|
||||
): T? {
|
||||
return if (sourceType == type) {
|
||||
action()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> ChainExternalApiLocal.parsedParameters(gson: Gson): T? {
|
||||
return parameters?.let { gson.fromJson<T>(it) }
|
||||
}
|
||||
|
||||
private fun mapTransferApiFromLocal(local: ChainExternalApiLocal, gson: Gson): ExternalApi.Transfers? {
|
||||
val parameters = local.parsedParameters<TransferParameters>(gson)
|
||||
|
||||
return when (parameters?.assetType) {
|
||||
null, SUBSTRATE_TRANSFER_PARAMETER -> ExternalApi.Transfers.Substrate(local.url)
|
||||
EVM_TRANSFER_PARAMETER -> ExternalApi.Transfers.Evm(local.url)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapGovernanceReferendaApiFromLocal(local: ChainExternalApiLocal, gson: Gson): ExternalApi.GovernanceReferenda? {
|
||||
val source = when (local.sourceType) {
|
||||
SourceType.SUBSQUARE -> ExternalApi.GovernanceReferenda.Source.SubSquare
|
||||
|
||||
SourceType.POLKASSEMBLY -> {
|
||||
val parameters = local.parsedParameters<GovernanceReferendaParameters>(gson)
|
||||
ExternalApi.GovernanceReferenda.Source.Polkassembly(parameters?.network)
|
||||
}
|
||||
|
||||
else -> return null
|
||||
}
|
||||
|
||||
return ExternalApi.GovernanceReferenda(local.url, source)
|
||||
}
|
||||
|
||||
private fun mapExternalApiLocalToExternalApi(externalApiLocal: ChainExternalApiLocal, gson: Gson): ExternalApi? = runCatching {
|
||||
when (externalApiLocal.apiType) {
|
||||
ApiType.STAKING -> externalApiLocal.ensureSourceType(SourceType.SUBQUERY) {
|
||||
ExternalApi.Staking(externalApiLocal.url)
|
||||
}
|
||||
|
||||
ApiType.STAKING_REWARDS -> externalApiLocal.ensureSourceType(SourceType.SUBQUERY) {
|
||||
ExternalApi.StakingRewards(externalApiLocal.url)
|
||||
}
|
||||
|
||||
ApiType.CROWDLOANS -> externalApiLocal.ensureSourceType(SourceType.GITHUB) {
|
||||
ExternalApi.Crowdloans(externalApiLocal.url)
|
||||
}
|
||||
|
||||
ApiType.TRANSFERS -> mapTransferApiFromLocal(externalApiLocal, gson)
|
||||
|
||||
ApiType.GOVERNANCE_REFERENDA -> mapGovernanceReferendaApiFromLocal(externalApiLocal, gson)
|
||||
|
||||
ApiType.GOVERNANCE_DELEGATIONS -> externalApiLocal.ensureSourceType(SourceType.SUBQUERY) {
|
||||
ExternalApi.GovernanceDelegations(externalApiLocal.url)
|
||||
}
|
||||
|
||||
ApiType.REFERENDUM_SUMMARY -> {
|
||||
ExternalApi.ReferendumSummary(externalApiLocal.url)
|
||||
}
|
||||
|
||||
ApiType.UNKNOWN -> null
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
private fun mapAutoBalanceStrategyFromLocal(local: AutoBalanceStrategyLocal): AutoBalanceStrategy {
|
||||
return when (local) {
|
||||
AutoBalanceStrategyLocal.ROUND_ROBIN -> AutoBalanceStrategy.ROUND_ROBIN
|
||||
AutoBalanceStrategyLocal.UNIFORM -> AutoBalanceStrategy.UNIFORM
|
||||
AutoBalanceStrategyLocal.UNKNOWN -> AutoBalanceStrategy.ROUND_ROBIN
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapNodeSelectionFromLocal(nodeSelectionPreferencesLocal: NodeSelectionPreferencesLocal?): NodeSelectionStrategy {
|
||||
if (nodeSelectionPreferencesLocal == null) return NodeSelectionStrategy.AutoBalance
|
||||
|
||||
val selectedUnformattedWssUrl = nodeSelectionPreferencesLocal.selectedUnformattedWssNodeUrl
|
||||
|
||||
return if (selectedUnformattedWssUrl != null && !nodeSelectionPreferencesLocal.autoBalanceEnabled) {
|
||||
NodeSelectionStrategy.SelectedNode(selectedUnformattedWssUrl)
|
||||
} else {
|
||||
NodeSelectionStrategy.AutoBalance
|
||||
}
|
||||
}
|
||||
|
||||
fun mapChainLocalToChain(chainLocal: JoinedChainInfo, gson: Gson): Chain {
|
||||
return mapChainLocalToChain(
|
||||
chainLocal.chain,
|
||||
chainLocal.nodes,
|
||||
chainLocal.nodeSelectionPreferences,
|
||||
chainLocal.assets,
|
||||
chainLocal.explorers,
|
||||
chainLocal.externalApis,
|
||||
gson
|
||||
)
|
||||
}
|
||||
|
||||
fun mapChainLocalToChain(
|
||||
chainLocal: ChainLocal,
|
||||
nodesLocal: List<ChainNodeLocal>,
|
||||
nodeSelectionPreferences: NodeSelectionPreferencesLocal?,
|
||||
assetsLocal: List<ChainAssetLocal>,
|
||||
explorersLocal: List<ChainExplorerLocal>,
|
||||
externalApisLocal: List<ChainExternalApiLocal>,
|
||||
gson: Gson
|
||||
): Chain {
|
||||
val nodes = nodesLocal.sortedBy { it.orderId }.map {
|
||||
Chain.Node(
|
||||
unformattedUrl = it.url,
|
||||
name = it.name,
|
||||
chainId = it.chainId,
|
||||
orderId = it.orderId,
|
||||
isCustom = it.source == ChainNodeLocal.Source.CUSTOM,
|
||||
)
|
||||
}
|
||||
|
||||
val nodesConfig = Chain.Nodes(
|
||||
autoBalanceStrategy = mapAutoBalanceStrategyFromLocal(chainLocal.autoBalanceStrategy),
|
||||
wssNodeSelectionStrategy = mapNodeSelectionFromLocal(nodeSelectionPreferences),
|
||||
nodes = nodes
|
||||
)
|
||||
|
||||
val assets = assetsLocal.map { mapChainAssetLocalToAsset(it, gson) }
|
||||
|
||||
val explorers = explorersLocal.map {
|
||||
Chain.Explorer(
|
||||
name = it.name,
|
||||
account = it.account,
|
||||
extrinsic = it.extrinsic,
|
||||
event = it.event,
|
||||
chainId = it.chainId
|
||||
)
|
||||
}
|
||||
|
||||
val types = chainLocal.types?.let {
|
||||
Chain.Types(
|
||||
url = it.url.nullIfEmpty(),
|
||||
overridesCommon = it.overridesCommon
|
||||
)
|
||||
}
|
||||
|
||||
val externalApis = externalApisLocal.mapNotNull {
|
||||
mapExternalApiLocalToExternalApi(it, gson)
|
||||
}
|
||||
|
||||
val additional = chainLocal.additional?.let { raw ->
|
||||
gson.fromJson<Chain.Additional>(raw)
|
||||
}
|
||||
|
||||
return with(chainLocal) {
|
||||
Chain(
|
||||
id = id,
|
||||
parentId = parentId,
|
||||
name = name,
|
||||
assets = assets,
|
||||
types = types,
|
||||
nodes = nodesConfig,
|
||||
explorers = explorers,
|
||||
icon = icon.takeIf { it.isNotBlank() },
|
||||
externalApis = externalApis,
|
||||
addressPrefix = prefix,
|
||||
legacyAddressPrefix = legacyPrefix,
|
||||
isEthereumBased = isEthereumBased,
|
||||
isTestNet = isTestNet,
|
||||
hasCrowdloans = hasCrowdloans,
|
||||
pushSupport = pushSupport,
|
||||
supportProxy = supportProxy,
|
||||
multisigSupport = multisigSupport,
|
||||
hasSubstrateRuntime = hasSubstrateRuntime,
|
||||
governance = mapGovernanceListFromLocal(governance),
|
||||
swap = mapSwapListFromLocal(swap),
|
||||
customFee = mapCustomFeeFromLocal(customFee),
|
||||
connectionState = mapConnectionStateFromLocal(connectionState),
|
||||
additional = additional,
|
||||
source = mapSourceFromLocal(source)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapChainAssetLocalToAsset(local: ChainAssetLocal, gson: Gson): Chain.Asset {
|
||||
val typeExtrasParsed = local.typeExtras?.let(gson::parseArbitraryObject)
|
||||
val buyProviders = local.buyProviders?.let<String, Map<TradeProviderId, TradeProviderArguments>?>(gson::fromJsonOrNull).orEmpty()
|
||||
val sellProviders = local.sellProviders?.let<String, Map<TradeProviderId, TradeProviderArguments>?>(gson::fromJsonOrNull).orEmpty()
|
||||
|
||||
return Chain.Asset(
|
||||
icon = local.icon,
|
||||
id = local.id,
|
||||
symbol = local.symbol.asTokenSymbol(),
|
||||
precision = local.precision.asPrecision(),
|
||||
name = local.name,
|
||||
chainId = local.chainId,
|
||||
priceId = local.priceId,
|
||||
buyProviders = buyProviders,
|
||||
sellProviders = sellProviders,
|
||||
staking = mapStakingTypeFromLocal(local.staking),
|
||||
type = mapChainAssetTypeFromRaw(local.type, typeExtrasParsed),
|
||||
source = mapAssetSourceFromLocal(local.source),
|
||||
enabled = local.enabled
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapSourceFromLocal(local: ChainLocal.Source): Chain.Source {
|
||||
return when (local) {
|
||||
ChainLocal.Source.DEFAULT -> Chain.Source.DEFAULT
|
||||
ChainLocal.Source.CUSTOM -> Chain.Source.CUSTOM
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapConnectionStateFromLocal(local: ConnectionStateLocal): ConnectionState {
|
||||
return when (local) {
|
||||
ConnectionStateLocal.FULL_SYNC -> ConnectionState.FULL_SYNC
|
||||
ConnectionStateLocal.LIGHT_SYNC -> ConnectionState.LIGHT_SYNC
|
||||
ConnectionStateLocal.DISABLED -> ConnectionState.DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapGovernanceListFromLocal(governanceLocal: String) = governanceLocal.split(",").mapNotNull {
|
||||
runCatching { Chain.Governance.valueOf(it) }.getOrNull()
|
||||
}
|
||||
|
||||
private fun mapSwapListFromLocal(swapLocal: String): List<Chain.Swap> {
|
||||
if (swapLocal.isEmpty()) return emptyList()
|
||||
|
||||
return swapLocal.split(",").mapNotNull {
|
||||
enumValueOfOrNull<Chain.Swap>(swapLocal)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapCustomFeeFromLocal(customFee: String): List<Chain.CustomFee> {
|
||||
if (customFee.isEmpty()) return emptyList()
|
||||
|
||||
return customFee.split(",").mapNotNull {
|
||||
enumValueOfOrNull<Chain.CustomFee>(it)
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.NodeSelectionPreferencesLocal
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainRemote
|
||||
|
||||
class RemoteToDomainChainMapperFacade(
|
||||
private val gson: Gson
|
||||
) {
|
||||
|
||||
fun mapRemoteChainToDomain(chainRemote: ChainRemote, source: Chain.Source): Chain {
|
||||
val localSource = when (source) {
|
||||
Chain.Source.DEFAULT -> ChainLocal.Source.DEFAULT
|
||||
Chain.Source.CUSTOM -> ChainLocal.Source.CUSTOM
|
||||
}
|
||||
val chainLocal = mapRemoteChainToLocal(chainRemote, null, localSource, gson)
|
||||
val assetsLocal = chainRemote.assets.map { mapRemoteAssetToLocal(chainRemote, it, gson, isEnabled = true) }
|
||||
val nodesLocal = mapRemoteNodesToLocal(chainRemote)
|
||||
val explorersLocal = mapRemoteExplorersToLocal(chainRemote)
|
||||
val externalApisLocal = mapExternalApisToLocal(chainRemote)
|
||||
|
||||
return mapChainLocalToChain(
|
||||
chainLocal = chainLocal,
|
||||
nodesLocal = nodesLocal,
|
||||
nodeSelectionPreferences = NodeSelectionPreferencesLocal(chainLocal.id, autoBalanceEnabled = true, selectedNodeUrl = null),
|
||||
assetsLocal = assetsLocal,
|
||||
explorersLocal = explorersLocal,
|
||||
externalApisLocal = externalApisLocal,
|
||||
gson = gson
|
||||
)
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.LightChain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.LightChainRemote
|
||||
|
||||
fun mapRemoteToDomainLightChain(chain: LightChainRemote): LightChain {
|
||||
return LightChain(
|
||||
id = chain.chainId,
|
||||
name = chain.name,
|
||||
icon = chain.icon,
|
||||
)
|
||||
}
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.asGsonParsedIntOrNull
|
||||
import io.novafoundation.nova.common.utils.asGsonParsedLongOrNull
|
||||
import io.novafoundation.nova.core_db.model.chain.AssetSourceLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainAssetLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExplorerLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal.ApiType
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainExternalApiLocal.SourceType
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal.AutoBalanceStrategyLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal.Companion.EMPTY_CHAIN_ICON
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainLocal.ConnectionStateLocal
|
||||
import io.novafoundation.nova.core_db.model.chain.ChainNodeLocal
|
||||
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.remote.model.ChainAssetRemote
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainRemote
|
||||
|
||||
private const val ETHEREUM_OPTION = "ethereumBased"
|
||||
private const val CROWDLOAN_OPTION = "crowdloans"
|
||||
private const val TESTNET_OPTION = "testnet"
|
||||
private const val PROXY_OPTION = "proxy"
|
||||
|
||||
private const val MULTISIG_SUPPORT = "multisig"
|
||||
private const val SWAP_HUB = "swap-hub"
|
||||
private const val HYDRA_DX_SWAPS = "hydradx-swaps"
|
||||
private const val NO_SUBSTRATE_RUNTIME = "noSubstrateRuntime"
|
||||
private const val FULL_SYNC_BY_DEFAULT = "fullSyncByDefault"
|
||||
private const val PUSH_SUPPORT = "pushSupport"
|
||||
private const val CUSTOM_FEE_ASSET_HUB = "assethub-fees"
|
||||
private const val CUSTOM_FEE_HYDRA_DX = "hydration-fees"
|
||||
|
||||
private const val CHAIN_ADDITIONAL_TIP = "defaultTip"
|
||||
private const val CHAIN_THEME_COLOR = "themeColor"
|
||||
private const val CHAIN_STAKING_WIKI = "stakingWiki"
|
||||
private const val DEFAULT_BLOCK_TIME = "defaultBlockTime"
|
||||
private const val RELAYCHAIN_AS_NATIVE = "relaychainAsNative"
|
||||
private const val MAX_ELECTING_VOTES = "stakingMaxElectingVoters"
|
||||
private const val FEE_VIA_RUNTIME_CALL = "feeViaRuntimeCall"
|
||||
private const val SUPPORT_GENERIC_LEDGER_APP = "supportsGenericLedgerApp"
|
||||
private const val IDENTITY_CHAIN = "identityChain"
|
||||
private const val DISABLED_CHECK_METADATA_HASH = "disabledCheckMetadataHash"
|
||||
private const val SESSION_LENGTH = "sessionLength"
|
||||
private const val SESSIONS_PER_ERA = "sessionsPerEra"
|
||||
private const val TIMELINE_CHAIN = "timelineChain"
|
||||
|
||||
fun mapRemoteChainToLocal(
|
||||
chainRemote: ChainRemote,
|
||||
oldChain: ChainLocal?,
|
||||
source: ChainLocal.Source,
|
||||
gson: Gson
|
||||
): ChainLocal {
|
||||
val types = chainRemote.types?.let {
|
||||
ChainLocal.TypesConfig(
|
||||
url = it.url.orEmpty(),
|
||||
overridesCommon = it.overridesCommon
|
||||
)
|
||||
}
|
||||
|
||||
val additional = chainRemote.additional?.let {
|
||||
Chain.Additional(
|
||||
defaultTip = (it[CHAIN_ADDITIONAL_TIP] as? String)?.toBigInteger(),
|
||||
themeColor = (it[CHAIN_THEME_COLOR] as? String),
|
||||
stakingWiki = (it[CHAIN_STAKING_WIKI] as? String),
|
||||
defaultBlockTimeMillis = it[DEFAULT_BLOCK_TIME].asGsonParsedLongOrNull(),
|
||||
relaychainAsNative = it[RELAYCHAIN_AS_NATIVE] as? Boolean,
|
||||
stakingMaxElectingVoters = it[MAX_ELECTING_VOTES].asGsonParsedIntOrNull(),
|
||||
feeViaRuntimeCall = it[FEE_VIA_RUNTIME_CALL] as? Boolean,
|
||||
supportLedgerGenericApp = it[SUPPORT_GENERIC_LEDGER_APP] as? Boolean,
|
||||
identityChain = it[IDENTITY_CHAIN] as? ChainId,
|
||||
disabledCheckMetadataHash = it[DISABLED_CHECK_METADATA_HASH] as? Boolean,
|
||||
sessionLength = it[SESSION_LENGTH].asGsonParsedIntOrNull(),
|
||||
sessionsPerEra = it[SESSIONS_PER_ERA].asGsonParsedIntOrNull(),
|
||||
timelineChain = it[TIMELINE_CHAIN] as? ChainId
|
||||
)
|
||||
}
|
||||
|
||||
val chainLocal = with(chainRemote) {
|
||||
val optionsOrEmpty = options.orEmpty()
|
||||
|
||||
ChainLocal(
|
||||
id = chainId,
|
||||
parentId = parentId,
|
||||
name = name,
|
||||
types = types,
|
||||
icon = icon ?: EMPTY_CHAIN_ICON,
|
||||
prefix = addressPrefix,
|
||||
legacyPrefix = legacyAddressPrefix,
|
||||
isEthereumBased = ETHEREUM_OPTION in optionsOrEmpty,
|
||||
isTestNet = TESTNET_OPTION in optionsOrEmpty,
|
||||
hasCrowdloans = CROWDLOAN_OPTION in optionsOrEmpty,
|
||||
supportProxy = PROXY_OPTION in optionsOrEmpty,
|
||||
multisigSupport = MULTISIG_SUPPORT in optionsOrEmpty,
|
||||
hasSubstrateRuntime = NO_SUBSTRATE_RUNTIME !in optionsOrEmpty,
|
||||
pushSupport = PUSH_SUPPORT in optionsOrEmpty,
|
||||
governance = mapGovernanceRemoteOptionsToLocal(optionsOrEmpty),
|
||||
swap = mapSwapRemoteOptionsToLocal(optionsOrEmpty),
|
||||
customFee = mapCustomFeeRemoteOptionsToLocal(optionsOrEmpty),
|
||||
connectionState = determineConnectionState(chainRemote, oldChain),
|
||||
additional = gson.toJson(additional),
|
||||
nodeSelectionStrategy = mapNodeSelectionStrategyToLocal(nodeSelectionStrategy),
|
||||
source = source
|
||||
)
|
||||
}
|
||||
|
||||
return chainLocal
|
||||
}
|
||||
|
||||
private fun mapNodeSelectionStrategyToLocal(remote: String?): AutoBalanceStrategyLocal {
|
||||
return when (remote) {
|
||||
null, "roundRobin" -> AutoBalanceStrategyLocal.ROUND_ROBIN
|
||||
"uniform" -> AutoBalanceStrategyLocal.UNIFORM
|
||||
else -> AutoBalanceStrategyLocal.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun determineConnectionState(remoteChain: ChainRemote, oldLocalChain: ChainLocal?): ConnectionStateLocal {
|
||||
if (oldLocalChain != null && oldLocalChain.connectionState.isNotDefault()) {
|
||||
return oldLocalChain.connectionState
|
||||
}
|
||||
|
||||
val options = remoteChain.options.orEmpty()
|
||||
val fullSyncByDefault = FULL_SYNC_BY_DEFAULT in options
|
||||
val hasNoSubstrateRuntime = NO_SUBSTRATE_RUNTIME in options
|
||||
|
||||
return if (fullSyncByDefault || hasNoSubstrateRuntime) ConnectionStateLocal.FULL_SYNC else ConnectionStateLocal.LIGHT_SYNC
|
||||
}
|
||||
|
||||
private fun ConnectionStateLocal.isNotDefault(): Boolean {
|
||||
return this != ConnectionStateLocal.LIGHT_SYNC
|
||||
}
|
||||
|
||||
private fun mapGovernanceRemoteOptionsToLocal(remoteOptions: Set<String>): String {
|
||||
val domainGovernanceTypes = remoteOptions.governanceTypesFromOptions()
|
||||
|
||||
return mapGovernanceListToLocal(domainGovernanceTypes)
|
||||
}
|
||||
|
||||
private fun mapSwapRemoteOptionsToLocal(remoteOptions: Set<String>): String {
|
||||
val domainGovernanceTypes = remoteOptions.swapTypesFromOptions()
|
||||
|
||||
return mapSwapListToLocal(domainGovernanceTypes)
|
||||
}
|
||||
|
||||
private fun mapCustomFeeRemoteOptionsToLocal(remoteOptions: Set<String>): String {
|
||||
val domainGovernanceTypes = remoteOptions.customFeeTypeFromOptions()
|
||||
|
||||
return mapCustomFeeToLocal(domainGovernanceTypes)
|
||||
}
|
||||
|
||||
fun mapRemoteAssetToLocal(
|
||||
chainRemote: ChainRemote,
|
||||
assetRemote: ChainAssetRemote,
|
||||
gson: Gson,
|
||||
isEnabled: Boolean
|
||||
): ChainAssetLocal {
|
||||
return ChainAssetLocal(
|
||||
id = assetRemote.assetId,
|
||||
symbol = assetRemote.symbol,
|
||||
precision = assetRemote.precision,
|
||||
chainId = chainRemote.chainId,
|
||||
name = assetRemote.name ?: chainRemote.name,
|
||||
priceId = assetRemote.priceId,
|
||||
staking = mapRemoteStakingTypesToLocal(assetRemote.staking),
|
||||
type = assetRemote.type,
|
||||
source = AssetSourceLocal.DEFAULT,
|
||||
buyProviders = gson.toJson(assetRemote.buyProviders),
|
||||
sellProviders = gson.toJson(assetRemote.sellProviders),
|
||||
typeExtras = gson.toJson(assetRemote.typeExtras),
|
||||
icon = assetRemote.icon,
|
||||
enabled = isEnabled
|
||||
)
|
||||
}
|
||||
|
||||
fun mapRemoteNodesToLocal(chainRemote: ChainRemote): List<ChainNodeLocal> {
|
||||
return chainRemote.nodes.mapIndexed { index, chainNodeRemote ->
|
||||
ChainNodeLocal(
|
||||
url = chainNodeRemote.url,
|
||||
name = chainNodeRemote.name,
|
||||
chainId = chainRemote.chainId,
|
||||
orderId = index,
|
||||
source = ChainNodeLocal.Source.DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapRemoteExplorersToLocal(chainRemote: ChainRemote): List<ChainExplorerLocal> {
|
||||
val explorers = chainRemote.explorers?.map {
|
||||
ChainExplorerLocal(
|
||||
chainId = chainRemote.chainId,
|
||||
name = it.name,
|
||||
extrinsic = it.extrinsic,
|
||||
account = it.account,
|
||||
event = it.event
|
||||
)
|
||||
}
|
||||
|
||||
return explorers.orEmpty()
|
||||
}
|
||||
|
||||
fun mapExternalApisToLocal(chainRemote: ChainRemote): List<ChainExternalApiLocal> {
|
||||
return chainRemote.externalApi?.flatMap { (apiType, apis) ->
|
||||
apis.map { api ->
|
||||
ChainExternalApiLocal(
|
||||
chainId = chainRemote.chainId,
|
||||
sourceType = mapSourceTypeRemoteToLocal(api.sourceType),
|
||||
apiType = mapApiTypeRemoteToLocal(apiType),
|
||||
parameters = api.parameters,
|
||||
url = api.url
|
||||
)
|
||||
}
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
private fun mapApiTypeRemoteToLocal(apiType: String): ApiType = when (apiType) {
|
||||
"history" -> ApiType.TRANSFERS
|
||||
"staking" -> ApiType.STAKING
|
||||
"staking-rewards" -> ApiType.STAKING_REWARDS
|
||||
"crowdloans" -> ApiType.CROWDLOANS
|
||||
"governance" -> ApiType.GOVERNANCE_REFERENDA
|
||||
"governance-delegations" -> ApiType.GOVERNANCE_DELEGATIONS
|
||||
"referendum-summary" -> ApiType.REFERENDUM_SUMMARY
|
||||
else -> ApiType.UNKNOWN
|
||||
}
|
||||
|
||||
private fun mapSourceTypeRemoteToLocal(sourceType: String): SourceType = when (sourceType) {
|
||||
"subquery" -> SourceType.SUBQUERY
|
||||
"github" -> SourceType.GITHUB
|
||||
"polkassembly" -> SourceType.POLKASSEMBLY
|
||||
"etherscan" -> SourceType.ETHERSCAN
|
||||
"subsquare" -> SourceType.SUBSQUARE
|
||||
else -> SourceType.UNKNOWN
|
||||
}
|
||||
|
||||
private fun mapRemoteStakingTypesToLocal(stakingTypesRemote: List<String>?): String {
|
||||
return stakingTypesRemote.orEmpty().joinToString(separator = ",") { stakingTypeRemote ->
|
||||
val stakingType = mapStakingStringToStakingType(stakingTypeRemote)
|
||||
mapStakingTypeToLocal(stakingType)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapStakingStringToStakingType(stakingString: String?): Chain.Asset.StakingType {
|
||||
return when (stakingString) {
|
||||
null -> Chain.Asset.StakingType.UNSUPPORTED
|
||||
"relaychain" -> Chain.Asset.StakingType.RELAYCHAIN
|
||||
"parachain" -> Chain.Asset.StakingType.PARACHAIN
|
||||
"nomination-pools" -> Chain.Asset.StakingType.NOMINATION_POOLS
|
||||
"aura-relaychain" -> Chain.Asset.StakingType.RELAYCHAIN_AURA
|
||||
"turing" -> Chain.Asset.StakingType.TURING
|
||||
"aleph-zero" -> Chain.Asset.StakingType.ALEPH_ZERO
|
||||
"mythos" -> Chain.Asset.StakingType.MYTHOS
|
||||
else -> Chain.Asset.StakingType.UNSUPPORTED
|
||||
}
|
||||
}
|
||||
|
||||
fun mapStakingTypeToStakingString(stakingType: Chain.Asset.StakingType): String? {
|
||||
return when (stakingType) {
|
||||
Chain.Asset.StakingType.UNSUPPORTED -> null
|
||||
Chain.Asset.StakingType.RELAYCHAIN -> "relaychain"
|
||||
Chain.Asset.StakingType.PARACHAIN -> "parachain"
|
||||
Chain.Asset.StakingType.RELAYCHAIN_AURA -> "aura-relaychain"
|
||||
Chain.Asset.StakingType.TURING -> "turing"
|
||||
Chain.Asset.StakingType.ALEPH_ZERO -> "aleph-zero"
|
||||
Chain.Asset.StakingType.NOMINATION_POOLS -> "nomination-pools"
|
||||
Chain.Asset.StakingType.MYTHOS -> "mythos"
|
||||
}
|
||||
}
|
||||
|
||||
private fun Set<String>.governanceTypesFromOptions(): List<Chain.Governance> {
|
||||
return mapNotNull { option ->
|
||||
when (option) {
|
||||
"governance" -> Chain.Governance.V2 // for backward compatibility of dev builds. Can be removed once everyone will update dev app
|
||||
"governance-v2" -> Chain.Governance.V2
|
||||
"governance-v1" -> Chain.Governance.V1
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Set<String>.swapTypesFromOptions(): List<Chain.Swap> {
|
||||
return mapNotNull { option ->
|
||||
when (option) {
|
||||
SWAP_HUB -> Chain.Swap.ASSET_CONVERSION
|
||||
HYDRA_DX_SWAPS -> Chain.Swap.HYDRA_DX
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Set<String>.customFeeTypeFromOptions(): List<Chain.CustomFee> {
|
||||
return mapNotNull { option ->
|
||||
when (option) {
|
||||
CUSTOM_FEE_ASSET_HUB -> Chain.CustomFee.ASSET_HUB
|
||||
CUSTOM_FEE_HYDRA_DX -> Chain.CustomFee.HYDRA_DX
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils
|
||||
|
||||
class GovernanceReferendaParameters(val network: String?)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.mappers.utils
|
||||
|
||||
const val SUBSTRATE_TRANSFER_PARAMETER = "substrate"
|
||||
const val EVM_TRANSFER_PARAMETER = "evm"
|
||||
|
||||
class TransferParameters(val assetType: String?)
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.Identifiable
|
||||
import io.novafoundation.nova.common.utils.Precision
|
||||
import io.novafoundation.nova.common.utils.TokenSymbol
|
||||
import java.io.Serializable
|
||||
import java.math.BigInteger
|
||||
|
||||
typealias ChainId = String
|
||||
typealias ChainAssetId = Int
|
||||
typealias StringTemplate = String
|
||||
|
||||
typealias ExplorerTemplateExtractor = (Chain.Explorer) -> StringTemplate?
|
||||
|
||||
typealias TradeProviderId = String
|
||||
typealias TradeProviderArguments = Map<String, Any?>
|
||||
|
||||
data class FullChainAssetId(val chainId: ChainId, val assetId: ChainAssetId) {
|
||||
|
||||
companion object
|
||||
}
|
||||
|
||||
data class Chain(
|
||||
val id: ChainId,
|
||||
val name: String,
|
||||
val assets: List<Asset>,
|
||||
val nodes: Nodes,
|
||||
val explorers: List<Explorer>,
|
||||
val externalApis: List<ExternalApi>,
|
||||
val icon: String?,
|
||||
val addressPrefix: Int,
|
||||
val legacyAddressPrefix: Int?,
|
||||
val types: Types?,
|
||||
val isEthereumBased: Boolean,
|
||||
val isTestNet: Boolean,
|
||||
val source: Source,
|
||||
val hasSubstrateRuntime: Boolean,
|
||||
val pushSupport: Boolean,
|
||||
val hasCrowdloans: Boolean,
|
||||
val supportProxy: Boolean,
|
||||
val governance: List<Governance>,
|
||||
val swap: List<Swap>,
|
||||
val customFee: List<CustomFee>,
|
||||
val multisigSupport: Boolean,
|
||||
val connectionState: ConnectionState,
|
||||
val parentId: String?,
|
||||
val additional: Additional?
|
||||
) : Identifiable, Serializable {
|
||||
|
||||
companion object // extensions
|
||||
|
||||
val assetsById = assets.associateBy(Asset::id)
|
||||
|
||||
data class Additional(
|
||||
val defaultTip: BigInteger?,
|
||||
val themeColor: String?,
|
||||
val stakingWiki: String?,
|
||||
val defaultBlockTimeMillis: Long?,
|
||||
val relaychainAsNative: Boolean?,
|
||||
val stakingMaxElectingVoters: Int?,
|
||||
val feeViaRuntimeCall: Boolean?,
|
||||
val supportLedgerGenericApp: Boolean?,
|
||||
val identityChain: ChainId?,
|
||||
val disabledCheckMetadataHash: Boolean?,
|
||||
val sessionLength: Int?,
|
||||
val sessionsPerEra: Int?,
|
||||
val timelineChain: ChainId?
|
||||
)
|
||||
|
||||
data class Types(
|
||||
val url: String?,
|
||||
val overridesCommon: Boolean,
|
||||
)
|
||||
|
||||
data class Asset(
|
||||
val icon: String?,
|
||||
val id: ChainAssetId,
|
||||
val priceId: String?,
|
||||
val chainId: ChainId,
|
||||
val symbol: TokenSymbol,
|
||||
val precision: Precision,
|
||||
val buyProviders: Map<TradeProviderId, TradeProviderArguments>,
|
||||
val sellProviders: Map<TradeProviderId, TradeProviderArguments>,
|
||||
val staking: List<StakingType>,
|
||||
val type: Type,
|
||||
val source: Source,
|
||||
val name: String,
|
||||
val enabled: Boolean,
|
||||
) : Identifiable, Serializable {
|
||||
|
||||
enum class Source {
|
||||
DEFAULT, ERC20, MANUAL
|
||||
}
|
||||
|
||||
sealed class Type {
|
||||
object Native : Type()
|
||||
|
||||
data class Statemine(
|
||||
val id: StatemineAssetId,
|
||||
val palletName: String?,
|
||||
val isSufficient: Boolean,
|
||||
) : Type()
|
||||
|
||||
data class Orml(
|
||||
val currencyIdScale: String,
|
||||
val currencyIdType: String,
|
||||
val existentialDeposit: BigInteger,
|
||||
val transfersEnabled: Boolean,
|
||||
val subType: SubType
|
||||
) : Type() {
|
||||
|
||||
enum class SubType {
|
||||
DEFAULT, HYDRATION_EVM
|
||||
}
|
||||
}
|
||||
|
||||
data class EvmErc20(
|
||||
val contractAddress: String
|
||||
) : Type()
|
||||
|
||||
object EvmNative : Type()
|
||||
|
||||
data class Equilibrium(
|
||||
val id: BigInteger
|
||||
) : Type()
|
||||
|
||||
object Unsupported : Type()
|
||||
}
|
||||
|
||||
enum class StakingType {
|
||||
UNSUPPORTED,
|
||||
RELAYCHAIN, RELAYCHAIN_AURA, ALEPH_ZERO, // relaychain like
|
||||
PARACHAIN, TURING, // parachain-staking like
|
||||
NOMINATION_POOLS,
|
||||
MYTHOS
|
||||
}
|
||||
|
||||
override val identifier = "$chainId:$id"
|
||||
}
|
||||
|
||||
data class Nodes(
|
||||
val autoBalanceStrategy: AutoBalanceStrategy,
|
||||
val wssNodeSelectionStrategy: NodeSelectionStrategy,
|
||||
val nodes: List<Node>,
|
||||
) {
|
||||
|
||||
enum class AutoBalanceStrategy {
|
||||
ROUND_ROBIN, UNIFORM
|
||||
}
|
||||
|
||||
sealed class NodeSelectionStrategy {
|
||||
|
||||
object AutoBalance : NodeSelectionStrategy()
|
||||
|
||||
class SelectedNode(val unformattedNodeUrl: String) : NodeSelectionStrategy()
|
||||
}
|
||||
}
|
||||
|
||||
data class Node(
|
||||
val chainId: ChainId,
|
||||
val unformattedUrl: String,
|
||||
val name: String,
|
||||
val orderId: Int,
|
||||
val isCustom: Boolean
|
||||
) : Identifiable {
|
||||
|
||||
enum class ConnectionType {
|
||||
HTTPS, WSS, UNKNOWN
|
||||
}
|
||||
|
||||
val connectionType = when {
|
||||
unformattedUrl.startsWith("wss://") || unformattedUrl.startsWith("ws://") -> ConnectionType.WSS
|
||||
unformattedUrl.startsWith("https://") -> ConnectionType.HTTPS
|
||||
else -> ConnectionType.UNKNOWN
|
||||
}
|
||||
|
||||
override val identifier: String = "$chainId:$unformattedUrl"
|
||||
}
|
||||
|
||||
data class Explorer(
|
||||
val chainId: ChainId,
|
||||
val name: String,
|
||||
val account: StringTemplate?,
|
||||
val extrinsic: StringTemplate?,
|
||||
val event: StringTemplate?
|
||||
) : Identifiable {
|
||||
|
||||
override val identifier = "$chainId:$name"
|
||||
}
|
||||
|
||||
sealed class ExternalApi {
|
||||
|
||||
abstract val url: String
|
||||
|
||||
sealed class Transfers : ExternalApi() {
|
||||
|
||||
data class Substrate(override val url: String) : Transfers()
|
||||
|
||||
data class Evm(override val url: String) : Transfers()
|
||||
}
|
||||
|
||||
data class Crowdloans(override val url: String) : ExternalApi()
|
||||
|
||||
data class Staking(override val url: String) : ExternalApi()
|
||||
|
||||
data class StakingRewards(override val url: String) : ExternalApi()
|
||||
|
||||
data class GovernanceReferenda(override val url: String, val source: Source) : ExternalApi() {
|
||||
|
||||
sealed class Source {
|
||||
|
||||
data class Polkassembly(val network: String?) : Source()
|
||||
|
||||
object SubSquare : Source()
|
||||
}
|
||||
}
|
||||
|
||||
data class GovernanceDelegations(override val url: String) : ExternalApi()
|
||||
|
||||
data class ReferendumSummary(override val url: String) : ExternalApi()
|
||||
}
|
||||
|
||||
enum class Governance {
|
||||
V1, V2
|
||||
}
|
||||
|
||||
enum class Swap {
|
||||
ASSET_CONVERSION, HYDRA_DX
|
||||
}
|
||||
|
||||
enum class CustomFee {
|
||||
ASSET_HUB, HYDRA_DX
|
||||
}
|
||||
|
||||
enum class ConnectionState {
|
||||
/**
|
||||
* Runtime sync is performed for the chain and the chain can be considered ready for any operation
|
||||
*/
|
||||
FULL_SYNC,
|
||||
|
||||
/**
|
||||
* Websocket connection is established for the chain, but runtime is not synced.
|
||||
* Thus, only runtime-independent operations can be performed
|
||||
*/
|
||||
LIGHT_SYNC,
|
||||
|
||||
/**
|
||||
* Chain is completely disabled - it does not initialize websockets not allocates any other resources
|
||||
*/
|
||||
DISABLED
|
||||
}
|
||||
|
||||
enum class Source {
|
||||
DEFAULT, CUSTOM
|
||||
}
|
||||
|
||||
override val identifier: String = id
|
||||
}
|
||||
|
||||
enum class TypesUsage {
|
||||
BASE, OWN, BOTH, NONE
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user