mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-10-28 22:49:58 +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:
commit
31c8c5995f
7621 files changed
+425838
No files matched your search
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1 @@
|
||||
0x6d6574610c841853797374656d011853797374656d401c4163636f756e7401010230543a3a4163636f756e744964944163636f756e74496e666f3c543a3a496e6465782c20543a3a4163636f756e74446174613e004101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e7400000c753332040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e2c426c6f636b576569676874010038436f6e73756d6564576569676874600000000000000000000000000000000000000000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e00000c753332040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b4861736801010538543a3a426c6f636b4e756d6265721c543a3a48617368008000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101050c7533321c5665633c75383e000400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d626572010038543a3a426c6f636b4e756d6265721000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801001c543a3a4861736880000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401002c4469676573744f663c543e040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301008c5665633c4576656e745265636f72643c543a3a4576656e742c20543a3a486173683e3e040004a0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e284576656e74436f756e740100284576656e74496e646578100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f706963730101021c543a3a48617368845665633c28543a3a426c6f636b4e756d6265722c204576656e74496e646578293e000400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e004d01205468652076616c756520686173207468652074797065206028543a3a426c6f636b4e756d6265722c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d65557067726164650000584c61737452756e74696d6555706772616465496e666f04000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e74010010626f6f6c0400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e74010010626f6f6c0400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e50686173650000145068617365040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e01282866696c6c5f626c6f636b04185f726174696f1c50657262696c6c040901204120646973706174636820746861742077696c6c2066696c6c2074686520626c6f636b2077656967687420757020746f2074686520676976656e20726174696f2e1872656d61726b041c5f72656d61726b1c5665633c75383e146c204d616b6520736f6d65206f6e2d636861696e2072656d61726b2e002c2023203c7765696768743e24202d20604f28312960302023203c2f7765696768743e387365745f686561705f7061676573041470616765730c75363420fc2053657420746865206e756d626572206f6620706167657320696e2074686520576562417Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
0x6d6574610c7c1853797374656d011853797374656d401c4163636f756e7401010230543a3a4163636f756e744964944163636f756e74496e666f3c543a3a496e6465782c20543a3a4163636f756e74446174613e004101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e7400000c753332040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e2c426c6f636b576569676874010038436f6e73756d6564576569676874600000000000000000000000000000000000000000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e00000c753332040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b4861736801010538543a3a426c6f636b4e756d6265721c543a3a48617368008000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101050c7533321c5665633c75383e000400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d626572010038543a3a426c6f636b4e756d6265721000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801001c543a3a4861736880000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401002c4469676573744f663c543e040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301008c5665633c4576656e745265636f72643c543a3a4576656e742c20543a3a486173683e3e040004a0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e284576656e74436f756e740100284576656e74496e646578100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f706963730101021c543a3a48617368845665633c28543a3a426c6f636b4e756d6265722c204576656e74496e646578293e000400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e004d01205468652076616c756520686173207468652074797065206028543a3a426c6f636b4e756d6265722c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d65557067726164650000584c61737452756e74696d6555706772616465496e666f04000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e74010010626f6f6c0400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e74010010626f6f6c0400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e50686173650000145068617365040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e01282866696c6c5f626c6f636b04185f726174696f1c50657262696c6c040901204120646973706174636820746861742077696c6c2066696c6c2074686520626c6f636b2077656967687420757020746f2074686520676976656e20726174696f2e1872656d61726b041c5f72656d61726b1c5665633c75383e146c204d616b6520736f6d65206f6e2d636861696e2072656d61726b2e002c2023203c7765696768743e24202d20604f28312960302023203c2f7765696768743e387365745f686561705f7061676573041470616765730c75363420fc2053657420746865206e756d626572206f6620706167657320696e2074686520576562417Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
0x6d6574610c941853797374656d011853797374656d401c4163636f756e7401010230543a3a4163636f756e744964944163636f756e74496e666f3c543a3a496e6465782c20543a3a4163636f756e74446174613e004101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e7400000c753332040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e2c426c6f636b576569676874010038436f6e73756d6564576569676874600000000000000000000000000000000000000000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e00000c753332040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b4861736801010538543a3a426c6f636b4e756d6265721c543a3a48617368008000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101050c7533321c5665633c75383e000400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d626572010038543a3a426c6f636b4e756d6265721000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801001c543a3a4861736880000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401002c4469676573744f663c543e040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301008c5665633c4576656e745265636f72643c543a3a4576656e742c20543a3a486173683e3e040004a0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e284576656e74436f756e740100284576656e74496e646578100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f706963730101021c543a3a48617368845665633c28543a3a426c6f636b4e756d6265722c204576656e74496e646578293e000400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e004d01205468652076616c756520686173207468652074797065206028543a3a426c6f636b4e756d6265722c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d65557067726164650000584c61737452756e74696d6555706772616465496e666f04000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e74010010626f6f6c0400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e74010010626f6f6c0400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e50686173650000145068617365040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e01282866696c6c5f626c6f636b04185f726174696f1c50657262696c6c040901204120646973706174636820746861742077696c6c2066696c6c2074686520626c6f636b2077656967687420757020746f2074686520676976656e20726174696f2e1872656d61726b041c5f72656d61726b1c5665633c75383e146c204d616b6520736f6d65206f6e2d636861696e2072656d61726b2e002c2023203c7765696768743e24202d20604f28312960302023203c2f7765696768743e387365745f686561705f7061676573041470616765730c75363420fc2053657420746865206e756d626572206f6620706167657320696e2074686520576562417Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
0x6d6574610ca01853797374656d011853797374656d401c4163636f756e7401010230543a3a4163636f756e744964944163636f756e74496e666f3c543a3a496e6465782c20543a3a4163636f756e74446174613e004101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e7400000c753332040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e2c426c6f636b576569676874010038436f6e73756d6564576569676874600000000000000000000000000000000000000000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e00000c753332040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b4861736801010538543a3a426c6f636b4e756d6265721c543a3a48617368008000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101050c7533321c5665633c75383e000400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d626572010038543a3a426c6f636b4e756d6265721000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801001c543a3a4861736880000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401002c4469676573744f663c543e040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301008c5665633c4576656e745265636f72643c543a3a4576656e742c20543a3a486173683e3e040004a0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e284576656e74436f756e740100284576656e74496e646578100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f706963730101021c543a3a48617368845665633c28543a3a426c6f636b4e756d6265722c204576656e74496e646578293e000400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e004d01205468652076616c756520686173207468652074797065206028543a3a426c6f636b4e756d6265722c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d65557067726164650000584c61737452756e74696d6555706772616465496e666f04000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e74010010626f6f6c0400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e74010010626f6f6c0400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e50686173650000145068617365040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e01282866696c6c5f626c6f636b04185f726174696f1c50657262696c6c040901204120646973706174636820746861742077696c6c2066696c6c2074686520626c6f636b2077656967687420757020746f2074686520676976656e20726174696f2e1872656d61726b041c5f72656d61726b1c5665633c75383e146c204d616b6520736f6d65206f6e2d636861696e2072656d61726b2e002c2023203c7765696768743e24202d20604f28312960302023203c2f7765696768743e387365745f686561705f7061676573041470616765730c75363420fc2053657420746865206e756d626572206f6620706167657320696e2074686520576562417Line truncated
|
||||
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
|
||||
}
|
||||
Loaded 100 of 7621 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user