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