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:
Kurdistan Tech Ministry committed 2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 files changed
+441811

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
+1
View File
@@ -0,0 +1 @@
0x6d6574610c841853797374656d011853797374656d401c4163636f756e7401010230543a3a4163636f756e744964944163636f756e74496e666f3c543a3a496e6465782c20543a3a4163636f756e74446174613e004101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e7400000c753332040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e2c426c6f636b576569676874010038436f6e73756d6564576569676874600000000000000000000000000000000000000000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e00000c753332040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b4861736801010538543a3a426c6f636b4e756d6265721c543a3a48617368008000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101050c7533321c5665633c75383e000400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d626572010038543a3a426c6f636b4e756d6265721000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801001c543a3a4861736880000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401002c4469676573744f663c543e040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301008c5665633c4576656e745265636f72643c543a3a4576656e742c20543a3a486173683e3e040004a0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e284576656e74436f756e740100284576656e74496e646578100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f706963730101021c543a3a48617368845665633c28543a3a426c6f636b4e756d6265722c204576656e74496e646578293e000400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e004d01205468652076616c756520686173207468652074797065206028543a3a426c6f636b4e756d6265722c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d65557067726164650000584c61737452756e74696d6555706772616465496e666f04000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e74010010626f6f6c0400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e74010010626f6f6c0400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e50686173650000145068617365040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e01282866696c6c5f626c6f636b04185f726174696f1c50657262696c6c040901204120646973706174636820746861742077696c6c2066696c6c2074686520626c6f636b2077656967687420757020746f2074686520676976656e20726174696f2e1872656d61726b041c5f72656d61726b1c5665633c75383e146c204d616b6520736f6d65206f6e2d636861696e2072656d61726b2e002c2023203c7765696768743e24202d20604f28312960302023203c2f7765696768743e387365745f686561705f7061676573041470616765730c75363420fc2053657420746865206e756d626572206f6620706167657320696e2074686520576562417Line truncated
@@ -0,0 +1 @@
0x6d6574610c7c1853797374656d011853797374656d401c4163636f756e7401010230543a3a4163636f756e744964944163636f756e74496e666f3c543a3a496e6465782c20543a3a4163636f756e74446174613e004101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e7400000c753332040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e2c426c6f636b576569676874010038436f6e73756d6564576569676874600000000000000000000000000000000000000000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e00000c753332040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b4861736801010538543a3a426c6f636b4e756d6265721c543a3a48617368008000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101050c7533321c5665633c75383e000400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d626572010038543a3a426c6f636b4e756d6265721000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801001c543a3a4861736880000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401002c4469676573744f663c543e040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301008c5665633c4576656e745265636f72643c543a3a4576656e742c20543a3a486173683e3e040004a0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e284576656e74436f756e740100284576656e74496e646578100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f706963730101021c543a3a48617368845665633c28543a3a426c6f636b4e756d6265722c204576656e74496e646578293e000400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e004d01205468652076616c756520686173207468652074797065206028543a3a426c6f636b4e756d6265722c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d65557067726164650000584c61737452756e74696d6555706772616465496e666f04000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e74010010626f6f6c0400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e74010010626f6f6c0400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e50686173650000145068617365040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e01282866696c6c5f626c6f636b04185f726174696f1c50657262696c6c040901204120646973706174636820746861742077696c6c2066696c6c2074686520626c6f636b2077656967687420757020746f2074686520676976656e20726174696f2e1872656d61726b041c5f72656d61726b1c5665633c75383e146c204d616b6520736f6d65206f6e2d636861696e2072656d61726b2e002c2023203c7765696768743e24202d20604f28312960302023203c2f7765696768743e387365745f686561705f7061676573041470616765730c75363420fc2053657420746865206e756d626572206f6620706167657320696e2074686520576562417Line truncated
+1
View File
@@ -0,0 +1 @@
0x6d6574610c941853797374656d011853797374656d401c4163636f756e7401010230543a3a4163636f756e744964944163636f756e74496e666f3c543a3a496e6465782c20543a3a4163636f756e74446174613e004101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e7400000c753332040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e2c426c6f636b576569676874010038436f6e73756d6564576569676874600000000000000000000000000000000000000000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e00000c753332040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b4861736801010538543a3a426c6f636b4e756d6265721c543a3a48617368008000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101050c7533321c5665633c75383e000400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d626572010038543a3a426c6f636b4e756d6265721000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801001c543a3a4861736880000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401002c4469676573744f663c543e040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301008c5665633c4576656e745265636f72643c543a3a4576656e742c20543a3a486173683e3e040004a0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e284576656e74436f756e740100284576656e74496e646578100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f706963730101021c543a3a48617368845665633c28543a3a426c6f636b4e756d6265722c204576656e74496e646578293e000400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e004d01205468652076616c756520686173207468652074797065206028543a3a426c6f636b4e756d6265722c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d65557067726164650000584c61737452756e74696d6555706772616465496e666f04000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e74010010626f6f6c0400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e74010010626f6f6c0400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e50686173650000145068617365040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e01282866696c6c5f626c6f636b04185f726174696f1c50657262696c6c040901204120646973706174636820746861742077696c6c2066696c6c2074686520626c6f636b2077656967687420757020746f2074686520676976656e20726174696f2e1872656d61726b041c5f72656d61726b1c5665633c75383e146c204d616b6520736f6d65206f6e2d636861696e2072656d61726b2e002c2023203c7765696768743e24202d20604f28312960302023203c2f7765696768743e387365745f686561705f7061676573041470616765730c75363420fc2053657420746865206e756d626572206f6620706167657320696e2074686520576562417Line truncated
+1
View File
@@ -0,0 +1 @@
0x6d6574610ca01853797374656d011853797374656d401c4163636f756e7401010230543a3a4163636f756e744964944163636f756e74496e666f3c543a3a496e6465782c20543a3a4163636f756e74446174613e004101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e7400000c753332040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e2c426c6f636b576569676874010038436f6e73756d6564576569676874600000000000000000000000000000000000000000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e00000c753332040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b4861736801010538543a3a426c6f636b4e756d6265721c543a3a48617368008000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101050c7533321c5665633c75383e000400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d626572010038543a3a426c6f636b4e756d6265721000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801001c543a3a4861736880000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401002c4469676573744f663c543e040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301008c5665633c4576656e745265636f72643c543a3a4576656e742c20543a3a486173683e3e040004a0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e284576656e74436f756e740100284576656e74496e646578100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f706963730101021c543a3a48617368845665633c28543a3a426c6f636b4e756d6265722c204576656e74496e646578293e000400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e004d01205468652076616c756520686173207468652074797065206028543a3a426c6f636b4e756d6265722c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d65557067726164650000584c61737452756e74696d6555706772616465496e666f04000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e74010010626f6f6c0400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e74010010626f6f6c0400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e50686173650000145068617365040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e01282866696c6c5f626c6f636b04185f726174696f1c50657262696c6c040901204120646973706174636820746861742077696c6c2066696c6c2074686520626c6f636b2077656967687420757020746f2074686520676976656e20726174696f2e1872656d61726b041c5f72656d61726b1c5665633c75383e146c204d616b6520736f6d65206f6e2d636861696e2072656d61726b2e002c2023203c7765696768743e24202d20604f28312960302023203c2f7765696768743e387365745f686561705f7061676573041470616765730c75363420fc2053657420746865206e756d626572206f6620706167657320696e2074686520576562417Line truncated
File diff suppressed because it is too large. Load diff
+410
View File
@@ -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"
}
}
+153
View File
@@ -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"
}
}
]
}
+53
View File
@@ -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"
}
}
]
}
+184
View File
@@ -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"
}
}
]
}
@@ -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
)
}
}
@@ -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)
}
@@ -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()
@@ -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()
}
}
}
}
@@ -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())
}
}
@@ -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))
}
}
}
@@ -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))
@@ -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)
}
}
@@ -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()
@@ -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()
}
}
@@ -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)
}
}
@@ -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
}
@@ -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)
}
@@ -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
}
@@ -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)
)
}
}
@@ -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
}
}
@@ -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())
}
}
@@ -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))
}
}
}
@@ -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()
}
@@ -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()
}
@@ -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 }
@@ -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())
}
@@ -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" }
}
}
@@ -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")
@@ -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() }
)
}
@@ -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)
}
}
@@ -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
)
@@ -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))
}
}
}
@@ -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
)
@@ -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
)
@@ -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
)
@@ -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
)
@@ -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
}
}
}
@@ -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
)
@@ -0,0 +1,4 @@
package io.novafoundation.nova.runtime.extrinsic.metadata
@JvmInline
value class ExtrinsicProof(val value: ByteArray)
@@ -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
)
@@ -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
}
}
@@ -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()
}
}
@@ -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))
}
}
@@ -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)
}
@@ -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)
}
}
@@ -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)
}
}
}
@@ -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
}
@@ -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
)
@@ -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)
}
}
}
@@ -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
}
@@ -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"
}
}
@@ -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"
}
}
@@ -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"
}
}
@@ -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
)
}
}
@@ -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
}
@@ -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
)
@@ -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)
}
}
}
@@ -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
}
}
@@ -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
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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
}
@@ -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)
}
}
}
@@ -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()
}
@@ -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
)
}
}
@@ -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)
}
@@ -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)
}
}
@@ -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>
}
@@ -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>
)
@@ -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>>?
)
@@ -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
)
}
}
}
@@ -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
@@ -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