From cbe8d6c41126f3e955298f37e1a42eb7ea7a0c6f Mon Sep 17 00:00:00 2001 From: oskal174 Date: Mon, 25 Dec 2023 12:20:29 +0500 Subject: [PATCH 1/4] Substrate chains support --- .env.example | 198 +++++ CONTRIBUTORS.md | 2 + Modules/AcalaMainModule.php | 24 + Modules/AcalaTokensModule.php | 22 + Modules/AcalaXCMModule.php | 22 + Modules/AstarEVMERC1155Module.php | 22 + Modules/AstarEVMERC20Module.php | 22 + Modules/AstarEVMERC721Module.php | 22 + Modules/AstarEVMMainModule.php | 33 + Modules/AstarEVMTraceModule.php | 26 + Modules/AstarMainModule.php | 24 + Modules/CentrifugeEVMERC1155Module.php | 23 + Modules/CentrifugeEVMERC20Module.php | 23 + Modules/CentrifugeEVMERC721Module.php | 23 + Modules/CentrifugeEVMMainModule.php | 34 + Modules/CentrifugeMainModule.php | 24 + Modules/Common/SubstrateMainModule.php | 192 +++++ Modules/Common/SubstrateTokensModule.php | 150 ++++ Modules/Common/SubstrateTraits.php | 982 ++++++++++++++++++++++ Modules/Common/SubstrateXCMMainModule.php | 119 +++ Modules/Common/SubstrateXCMModule.php | 144 ++++ Modules/KusamaMainModule.php | 24 + Modules/KusamaXCMModule.php | 20 + Modules/PolkadotMainModule.php | 25 + Modules/PolkadotXCMModule.php | 20 + 25 files changed, 2220 insertions(+) create mode 100644 Modules/AcalaMainModule.php create mode 100644 Modules/AcalaTokensModule.php create mode 100644 Modules/AcalaXCMModule.php create mode 100644 Modules/AstarEVMERC1155Module.php create mode 100644 Modules/AstarEVMERC20Module.php create mode 100644 Modules/AstarEVMERC721Module.php create mode 100644 Modules/AstarEVMMainModule.php create mode 100644 Modules/AstarEVMTraceModule.php create mode 100644 Modules/AstarMainModule.php create mode 100644 Modules/CentrifugeEVMERC1155Module.php create mode 100644 Modules/CentrifugeEVMERC20Module.php create mode 100644 Modules/CentrifugeEVMERC721Module.php create mode 100644 Modules/CentrifugeEVMMainModule.php create mode 100644 Modules/CentrifugeMainModule.php create mode 100644 Modules/Common/SubstrateMainModule.php create mode 100644 Modules/Common/SubstrateTokensModule.php create mode 100644 Modules/Common/SubstrateTraits.php create mode 100644 Modules/Common/SubstrateXCMMainModule.php create mode 100644 Modules/Common/SubstrateXCMModule.php create mode 100644 Modules/KusamaMainModule.php create mode 100644 Modules/KusamaXCMModule.php create mode 100644 Modules/PolkadotMainModule.php create mode 100644 Modules/PolkadotXCMModule.php diff --git a/.env.example b/.env.example index 9e419f0c..7de69e90 100644 --- a/.env.example +++ b/.env.example @@ -744,6 +744,204 @@ MODULE_zcash-main_NODES[]=http://login:password@127.0.0.2:1234/ MODULE_zcash-main_REQUESTER_TIMEOUT=60 MODULE_zcash-main_REQUESTER_THREADS=12 +###################### +## Main Centrifuge Module +###################### + +MODULES[]=centrifuge-main +MODULE_centrifuge-main_CLASS=CentrifugeMainModule +MODULE_centrifuge-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-main_REQUESTER_TIMEOUT=60 +MODULE_centrifuge-main_REQUESTER_THREADS=12 + +###################### +## EVM Main Centrifuge Module +###################### + +MODULES[]=centrifuge-evm-main +MODULE_centrifuge-evm-main_CLASS=CentrifugeEVMMainModule +MODULE_centrifuge-evm-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-evm-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-evm-main_REQUESTER_TIMEOUT=60 +MODULE_centrifuge-evm-main_REQUESTER_THREADS=12 + +###################### +## EVM ERC20 Centrifuge Module +###################### + +MODULES[]=centrifuge-evm-erc-20 +MODULE_centrifuge-evm-erc-20_CLASS=CentrifugeEVMERC20Module +MODULE_centrifuge-evm-erc-20_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-evm-erc-20_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-evm-erc-20_REQUESTER_TIMEOUT=60 +MODULE_centrifuge-evm-erc-20_REQUESTER_THREADS=12 + +###################### +## EVM ERC721 Centrifuge Module +###################### + +MODULES[]=centrifuge-evm-erc-721 +MODULE_centrifuge-evm-erc-721_CLASS=CentrifugeEVMERC721Module +MODULE_centrifuge-evm-erc-721_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-evm-erc-721_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-evm-erc-721_REQUESTER_TIMEOUT=60 +MODULE_centrifuge-evm-erc-721_REQUESTER_THREADS=12 + +###################### +## EVM ERC1155 Centrifuge Module +###################### + +MODULES[]=centrifuge-evm-erc-1155 +MODULE_centrifuge-evm-erc-1155_CLASS=CentrifugeEVMERC1155Module +MODULE_centrifuge-evm-erc-1155_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-evm-erc-1155_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_centrifuge-evm-erc-1155_REQUESTER_TIMEOUT=60 +MODULE_centrifuge-evm-erc-1155_REQUESTER_THREADS=12 + +###################### +## Main Astar Module +###################### + +MODULES[]=astar-main +MODULE_astar-main_CLASS=AstarMainModule +MODULE_astar-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-main_REQUESTER_TIMEOUT=60 +MODULE_astar-main_REQUESTER_THREADS=12 + +###################### +## EVM Main Astar Module +###################### + +MODULES[]=astar-evm-main +MODULE_astar-evm-main_CLASS=AstarEVMMainModule +MODULE_astar-evm-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-main_REQUESTER_TIMEOUT=60 +MODULE_astar-evm-main_REQUESTER_THREADS=12 + +###################### +## EVM Trace Astar Module +###################### + +MODULES[]=astar-evm-trace +MODULE_astar-evm-trace_CLASS=AstarEVMTraceModule +MODULE_astar-evm-trace_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-trace_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-trace_REQUESTER_TIMEOUT=60 +MODULE_astar-evm-trace_REQUESTER_THREADS=12 + +###################### +## EVM ERC20 Astar Module +###################### + +MODULES[]=astar-evm-erc-20 +MODULE_astar-evm-erc-20_CLASS=AstarEVMERC20Module +MODULE_astar-evm-erc-20_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-erc-20_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-erc-20_REQUESTER_TIMEOUT=60 +MODULE_astar-evm-erc-20_REQUESTER_THREADS=12 + +###################### +## EVM ERC721 Astar Module +###################### + +MODULES[]=astar-evm-erc-721 +MODULE_astar-evm-erc-721_CLASS=AstarEVMERC721Module +MODULE_astar-evm-erc-721_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-erc-721_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-erc-721_REQUESTER_TIMEOUT=60 +MODULE_astar-evm-erc-721_REQUESTER_THREADS=12 + +###################### +## EVM ERC1155 Astar Module +###################### + +MODULES[]=astar-evm-erc-1155 +MODULE_astar-evm-erc-1155_CLASS=AstarEVMERC1155Module +MODULE_astar-evm-erc-1155_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-erc-1155_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_astar-evm-erc-1155_REQUESTER_TIMEOUT=60 +MODULE_astar-evm-erc-1155_REQUESTER_THREADS=12 + +###################### +## Main Acala Module +###################### + +MODULES[]=acala-main +MODULE_acala-main_CLASS=AcalaMainModule +MODULE_acala-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_acala-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_acala-main_REQUESTER_TIMEOUT=60 +MODULE_acala-main_REQUESTER_THREADS=12 + +###################### +## XCM Acala Module +###################### + +MODULES[]=acala-xcm +MODULE_acala-xcm_CLASS=AcalaXCMModule +MODULE_acala-xcm_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_acala-xcm_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_acala-xcm_REQUESTER_TIMEOUT=60 +MODULE_acala-xcm_REQUESTER_THREADS=12 + +###################### +## Tokens Acala Module +###################### + +MODULES[]=acala-tokens +MODULE_acala-tokens_CLASS=AcalaTokensModule +MODULE_acala-tokens_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_acala-tokens_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_acala-tokens_REQUESTER_TIMEOUT=60 +MODULE_acala-tokens_REQUESTER_THREADS=12 + +###################### +## Main Kusama Module +###################### + +MODULES[]=kusama-main +MODULE_kusama-main_CLASS=KusamaMainModule +MODULE_kusama-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_kusama-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_kusama-main_REQUESTER_TIMEOUT=60 +MODULE_kusama-main_REQUESTER_THREADS=12 + +###################### +## XCM Kusama Module +###################### + +MODULES[]=kusama-xcm +MODULE_kusama-xcm_CLASS=KusamaXCMModule +MODULE_kusama-xcm_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_kusama-xcm_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_kusama-xcm_REQUESTER_TIMEOUT=60 +MODULE_kusama-xcm_REQUESTER_THREADS=12 + +###################### +## Main Polkadot Module +###################### + +MODULES[]=polkadot-main +MODULE_polkadot-main_CLASS=PolkadotMainModule +MODULE_polkadot-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_polkadot-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_polkadot-main_REQUESTER_TIMEOUT=60 +MODULE_polkadot-main_REQUESTER_THREADS=12 + +###################### +## XCM Polkadot Module +###################### + +MODULES[]=polkadot-xcm +MODULE_polkadot-xcm_CLASS=PolkadotXCMModule +MODULE_polkadot-xcm_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_polkadot-xcm_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_polkadot-xcm_REQUESTER_TIMEOUT=60 +MODULE_polkadot-xcm_REQUESTER_THREADS=12 + ############################ # Titles, descriptions, etc. ############################ diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7c87c9e5..086d40d8 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -7,5 +7,7 @@ - Beacon Chain modules * [alexqrid](https://github.com/alexqrid) - TVM modules +* [Kirill Kuzminykh](https://github.com/Oskal174) + - Substrate modules * [Oleg Makaussov](https://github.com/Lorgansar) - Cardano Tokens modules diff --git a/Modules/AcalaMainModule.php b/Modules/AcalaMainModule.php new file mode 100644 index 00000000..c3082d6a --- /dev/null +++ b/Modules/AcalaMainModule.php @@ -0,0 +1,24 @@ +blockchain = 'acala'; + $this->module = 'acala-main'; + $this->is_main = true; + $this->first_block_date = '2021-12-18'; + $this->currency = 'acala'; + $this->currency_details = ['name' => 'Acala', 'symbol' => 'ACA', 'decimals' => 12, 'description' => null]; + + // Substrate-specific + $this->chain_type = SubstrateChainType::Para; + } +} diff --git a/Modules/AcalaTokensModule.php b/Modules/AcalaTokensModule.php new file mode 100644 index 00000000..29082726 --- /dev/null +++ b/Modules/AcalaTokensModule.php @@ -0,0 +1,22 @@ +blockchain = 'acala'; + $this->module = 'acala-tokens'; + $this->is_main = false; + $this->first_block_date = '2021-12-18'; + + // Tokens-specific + $this->native_token_id = 'native-ACA'; + } +} diff --git a/Modules/AcalaXCMModule.php b/Modules/AcalaXCMModule.php new file mode 100644 index 00000000..a003cfba --- /dev/null +++ b/Modules/AcalaXCMModule.php @@ -0,0 +1,22 @@ +blockchain = 'acala'; + $this->module = 'acala-xcm'; + $this->is_main = false; + $this->first_block_date = '2021-12-18'; + + // Substrait XCM specific + $this->native_asset_id = 'native-ACA'; + } +} diff --git a/Modules/AstarEVMERC1155Module.php b/Modules/AstarEVMERC1155Module.php new file mode 100644 index 00000000..f59a61d7 --- /dev/null +++ b/Modules/AstarEVMERC1155Module.php @@ -0,0 +1,22 @@ +blockchain = 'astar'; + $this->module = 'astar-evm-erc-1155'; + $this->is_main = false; + $this->first_block_date = '2021-12-18'; + + // Extrinsic id has different format + $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; + } +} diff --git a/Modules/AstarEVMERC20Module.php b/Modules/AstarEVMERC20Module.php new file mode 100644 index 00000000..32fd680f --- /dev/null +++ b/Modules/AstarEVMERC20Module.php @@ -0,0 +1,22 @@ +blockchain = 'astar'; + $this->module = 'astar-evm-erc-20'; + $this->is_main = false; + $this->first_block_date = '2021-12-18'; + + // Extrinsic id has different format + $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; + } +} diff --git a/Modules/AstarEVMERC721Module.php b/Modules/AstarEVMERC721Module.php new file mode 100644 index 00000000..447f3090 --- /dev/null +++ b/Modules/AstarEVMERC721Module.php @@ -0,0 +1,22 @@ +blockchain = 'astar'; + $this->module = 'astar-evm-erc-721'; + $this->is_main = false; + $this->first_block_date = '2021-12-18'; + + // Extrinsic id has different format + $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; + } +} diff --git a/Modules/AstarEVMMainModule.php b/Modules/AstarEVMMainModule.php new file mode 100644 index 00000000..81c128ac --- /dev/null +++ b/Modules/AstarEVMMainModule.php @@ -0,0 +1,33 @@ +blockchain = 'astar'; + $this->module = 'astar-evm-main'; + $this->is_main = false; + $this->first_block_date = '2021-12-18'; + $this->currency = 'astar'; + $this->currency_details = ['name' => 'Astar', 'symbol' => 'ASTR', 'decimals' => 18, 'description' => null]; + + // Extrinsic id has different format + $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; + + // EVM-specific + // TODO: checks params + $this->evm_implementation = EVMImplementation::geth; + $this->extra_features = []; + $this->reward_function = function($block_id) + { + return '0'; + }; + } +} diff --git a/Modules/AstarEVMTraceModule.php b/Modules/AstarEVMTraceModule.php new file mode 100644 index 00000000..e89525db --- /dev/null +++ b/Modules/AstarEVMTraceModule.php @@ -0,0 +1,26 @@ +blockchain = 'astar'; + $this->module = 'astar-evm-main'; + $this->is_main = false; + $this->first_block_date = '2021-12-18'; + $this->complements = 'astar-evm-main'; + + // Extrinsic id has different format + $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; + + // EVM-specific + $this->evm_implementation = EVMImplementation::geth; + } +} diff --git a/Modules/AstarMainModule.php b/Modules/AstarMainModule.php new file mode 100644 index 00000000..a69c892b --- /dev/null +++ b/Modules/AstarMainModule.php @@ -0,0 +1,24 @@ +blockchain = 'astar'; + $this->module = 'astar-main'; + $this->is_main = true; + $this->first_block_date = '2021-12-18'; + $this->currency = 'astar'; + $this->currency_details = ['name' => 'Astar', 'symbol' => 'ASTR', 'decimals' => 18, 'description' => null]; + + // Substrate-specific + $this->chain_type = SubstrateChainType::Para; + } +} diff --git a/Modules/CentrifugeEVMERC1155Module.php b/Modules/CentrifugeEVMERC1155Module.php new file mode 100644 index 00000000..0ed448a9 --- /dev/null +++ b/Modules/CentrifugeEVMERC1155Module.php @@ -0,0 +1,23 @@ +blockchain = 'centrifuge'; + $this->module = 'centrifuge-evm-erc-1155'; + $this->is_main = false; + $this->first_block_date = '2022-03-12'; + $this->first_block_id = 3308248; + + // Extrinsic id has different format + $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; + } +} diff --git a/Modules/CentrifugeEVMERC20Module.php b/Modules/CentrifugeEVMERC20Module.php new file mode 100644 index 00000000..87aab5a9 --- /dev/null +++ b/Modules/CentrifugeEVMERC20Module.php @@ -0,0 +1,23 @@ +blockchain = 'centrifuge'; + $this->module = 'centrifuge-evm-erc-20'; + $this->is_main = false; + $this->first_block_date = '2022-03-12'; + $this->first_block_id = 3308248; + + // Extrinsic id has different format + $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; + } +} diff --git a/Modules/CentrifugeEVMERC721Module.php b/Modules/CentrifugeEVMERC721Module.php new file mode 100644 index 00000000..5e743b18 --- /dev/null +++ b/Modules/CentrifugeEVMERC721Module.php @@ -0,0 +1,23 @@ +blockchain = 'centrifuge'; + $this->module = 'centrifuge-evm-erc-721'; + $this->is_main = false; + $this->first_block_date = '2022-03-12'; + $this->first_block_id = 3308248; + + // Extrinsic id has different format + $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; + } +} diff --git a/Modules/CentrifugeEVMMainModule.php b/Modules/CentrifugeEVMMainModule.php new file mode 100644 index 00000000..bdd96be5 --- /dev/null +++ b/Modules/CentrifugeEVMMainModule.php @@ -0,0 +1,34 @@ +blockchain = 'centrifuge'; + $this->module = 'centrifuge-evm-main'; + $this->is_main = false; + $this->first_block_date = '2022-03-12'; + $this->first_block_id = 3308248; + $this->currency = 'centrifuge'; + $this->currency_details = ['name' => 'Centrifuge', 'symbol' => 'CFG', 'decimals' => 18, 'description' => null]; + + // Extrinsic id has different format + $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; + + // EVM-specific + // TODO: checks params + $this->evm_implementation = EVMImplementation::geth; + $this->extra_features = []; + $this->reward_function = function($block_id) + { + return '0'; + }; + } +} diff --git a/Modules/CentrifugeMainModule.php b/Modules/CentrifugeMainModule.php new file mode 100644 index 00000000..274f4809 --- /dev/null +++ b/Modules/CentrifugeMainModule.php @@ -0,0 +1,24 @@ +blockchain = 'centrifuge'; + $this->module = 'centrifuge-main'; + $this->is_main = true; + $this->first_block_date = '2022-03-12'; + $this->currency = 'centrifuge'; + $this->currency_details = ['name' => 'Centrifuge', 'symbol' => 'CFG', 'decimals' => 18, 'description' => null]; + + // Substrate-specific + $this->chain_type = SubstrateChainType::Para; + } +} diff --git a/Modules/Common/SubstrateMainModule.php b/Modules/Common/SubstrateMainModule.php new file mode 100644 index 00000000..52645959 --- /dev/null +++ b/Modules/Common/SubstrateMainModule.php @@ -0,0 +1,192 @@ +value => 'Transaction fee', + SubstrateSpecialTransactions::Reward->value => 'Validator block reward', + SubstrateSpecialTransactions::StakingReward->value => 'Staking reward', + SubstrateSpecialTransactions::Slashed->value => 'Validator slashed', + SubstrateSpecialTransactions::DustLost->value => 'Account deleted and tokens lost', + SubstrateSpecialTransactions::CreatePool->value => 'Create nomination pool', + SubstrateSpecialTransactions::JoinPool->value => 'Join the nomination pool', + SubstrateSpecialTransactions::BondExtra->value => 'Bond extra amount to nomination pool', + SubstrateSpecialTransactions::ClaimBounty->value => 'Bounty amount claimed', + ]; + + public ?bool $should_return_events = true; + public ?bool $should_return_currencies = false; + public ?bool $allow_empty_return_events = true; + + public ?bool $mempool_implemented = false; + public ?bool $forking_implemented = false; + + // Substrate-specific + public ?SubstrateChainType $chain_type = null; + + final public function pre_initialize() + { + $this->version = 1; + } + + final public function post_post_initialize() + { + if (is_null($this->chain_type)) + throw new DeveloperError('Chain type is not set (developer error).'); + } + + final public function pre_process_block($block_id) + { + $block = requester_single($this->select_node(), endpoint: "blocks/{$block_id}", timeout: $this->timeout); + $validator = $block['authorId']; + + $events = []; + $sort_key = 0; + + // Parse onInitialize monetary events + $this->process_internal_main_events($block['onInitialize']['events'] ?? [], $sort_key, $events); + + // Parse extrinsics data + foreach ($block['extrinsics'] ?? [] as $extrinsic_number => $extrinsic) + { + $tx_id = $block_id . '-' . $extrinsic_number; + + if ($this->chain_type === SubstrateChainType::Relay) + $this->process_fee_and_reward($extrinsic, $validator, $tx_id, $sort_key, $events); + elseif ($this->chain_type === SubstrateChainType::Para) + $this->process_fee($extrinsic, $tx_id, $sort_key, $events); + + $with_transfer = false; + switch ($extrinsic['method']['pallet']) + { + // XCM transfers processed at another module + case 'xcmPallet': + case 'xTokens': + break; + + // For some chains may be deposits to System accounts + case 'timestamp': + $this->process_timestamp_pallet_deposits($extrinsic, $tx_id, $sort_key, $events); + break; + + case 'balances': + $this->process_balances_pallet($extrinsic, $tx_id, $sort_key, $events); + break; + + case 'staking': + $this->process_staking_pallet($extrinsic, $tx_id, $sort_key, $events); + break; + + case 'nominationPools': + $this->process_nomination_pools_pallet($extrinsic, $tx_id, $sort_key, $events); + break; + + case 'convictionVoting': + $this->process_conviction_voting_pallet($extrinsic, $tx_id, $sort_key, $events); + break; + + case 'childBounties': + case 'bounties': + $this->process_bounties_pallet($extrinsic, $tx_id, $sort_key, $events); + break; + + case 'multisig': + case 'utility': + $method = $extrinsic['method']['method']; + $calls = []; + + // utility + if (in_array($method, ['batch', 'batchAll', 'forceBatch'])) + $calls = $extrinsic['args']['calls']; + // utility + elseif ($method === 'asDerivative') + $calls[] = $extrinsic['args']['call']; + // multisig + elseif (in_array($method, ['asMulti', 'asMultiThreshold1'])) + $calls[] = $extrinsic['args']['call']; + + foreach ($calls ?? [] as $call) + { + $call_extrinsic = $extrinsic; + $call_extrinsic['method'] = $call['method']; + $call_extrinsic['args'] = $call['args']; + + switch ($call['method']['pallet']) + { + case 'balances': + $this->process_balances_pallet($call_extrinsic, $tx_id, $sort_key, $events); + break; + case 'staking': + $this->process_staking_pallet($call_extrinsic, $tx_id, $sort_key, $events); + break; + case 'nominationPools': + $this->process_nomination_pools_pallet($call_extrinsic, $tx_id, $sort_key, $events); + break; + case 'convictionVoting': + $this->process_conviction_voting_pallet($call_extrinsic, $tx_id, $sort_key, $events); + break; + case 'childBounties': + case 'bounties': + $this->process_bounties_pallet($call_extrinsic, $tx_id, $sort_key, $events); + break; + default: + $with_transfer = true; + } + } + break; + + // For other pallets check for transfer in additional events + default: + $with_transfer = true; + } + + $failed = !$extrinsic['success']; + $this->process_additional_main_events($extrinsic['events'] ?? [], $with_transfer, $tx_id, $failed, $sort_key, $events); + } + + // Parse onFinalize monetary events + $this->process_internal_main_events($block['onFinalize']['events'] ?? [], $sort_key, $events); + + foreach ($events as &$event) + { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + } + + $this->set_return_events($events); + } + + // Getting balances from the node + public function api_get_balance($address) + { + return requester_single($this->select_node(), endpoint: "accounts/{$address}/balance-info", timeout: $this->timeout, result_in: 'free'); + } +} diff --git a/Modules/Common/SubstrateTokensModule.php b/Modules/Common/SubstrateTokensModule.php new file mode 100644 index 00000000..8dbacbdf --- /dev/null +++ b/Modules/Common/SubstrateTokensModule.php @@ -0,0 +1,150 @@ +version = 1; + } + + final public function post_post_initialize() + { + if (is_null($this->native_token_id)) + throw new DeveloperError("Native token id is not set (developer error)."); + } + + final public function pre_process_block($block_id) + { + $block = requester_single($this->select_node(), endpoint: "blocks/{$block_id}", timeout: $this->timeout); + + $events = []; + $currencies = []; + $currencies_to_process = []; + $sort_key = 0; + + // Parse extrinsics data + foreach ($block['extrinsics'] ?? [] as $extrinsic_number => $extrinsic) + { + $tx_id = $block_id . '-' . $extrinsic_number; + + $with_transfer = false; + switch ($extrinsic['method']['pallet']) + { + case 'currencies': + $this->process_currencies_pallet($extrinsic, $tx_id, $sort_key, $events, $currencies_to_process); + break; + + case 'multisig': + case 'utility': + $method = $extrinsic['method']['method']; + $calls = []; + + // utility + if (in_array($method, ['batch', 'batchAll', 'forceBatch'])) + $calls = $extrinsic['args']['calls']; + // utility + elseif ($method === 'asDerivative') + $calls[] = $extrinsic['args']['call']; + // multisig + elseif (in_array($method, ['asMulti', 'asMultiThreshold1'])) + $calls[] = $extrinsic['args']['call']; + + foreach ($calls ?? [] as $call) + { + $call_extrinsic = $extrinsic; + $call_extrinsic['method'] = $call['method']; + $call_extrinsic['args'] = $call['args']; + + switch ($call['method']['pallet']) + { + case 'currencies': + $this->process_currencies_pallet($call_extrinsic, $tx_id, $sort_key, $events, $currencies_to_process); + break; + default: + $with_transfer = true; + } + } + break; + + // This pallets for XCM so we skip it + case 'xTokens': + case 'parachainSystem': + break; + + // For other pallets check for transfer in additional events + default: + $with_transfer = true; + } + + $failed = !$extrinsic['success']; + $this->process_additional_tokens_events($extrinsic['events'] ?? [], $with_transfer, $tx_id, $failed, $sort_key, $events, $currencies_to_process); + } + + foreach ($events as &$event) + { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + } + + $currencies_to_process = array_unique($currencies_to_process); + $currencies_to_process = check_existing_currencies($currencies_to_process, $this->currency_format); + + $assets_meta = requester_single($this->select_node(), endpoint: "/pallets/asset-registry", result_in: 'items', timeout: $this->timeout); + foreach ($currencies_to_process as $currency) + { + $meta = ['name' => '', 'symbol' => '', 'decimals' => '']; + if (in_array($currency, $assets_meta)) + $meta = $assets_meta[$currency]; + $currencies[] = [ + 'id' => $currency, + 'name' => $meta['name'], + 'symbol' => $meta['symbol'], + 'decimals' => $meta['decimals'], + ]; + } + + $this->set_return_events($events); + $this->set_return_currencies($currencies); + } +} diff --git a/Modules/Common/SubstrateTraits.php b/Modules/Common/SubstrateTraits.php new file mode 100644 index 00000000..e79e2647 --- /dev/null +++ b/Modules/Common/SubstrateTraits.php @@ -0,0 +1,982 @@ +select_node(), endpoint: 'blocks/head/header?finalized=false', result_in: 'number', timeout: $this->timeout); + } + + public function ensure_block($block_id, $break_on_first = false) + { + $multi_curl = []; + foreach ($this->nodes as $node) + { + $multi_curl[] = requester_multi_prepare($node, endpoint: "blocks/{$block_id}", timeout: $this->timeout); + } + + try + { + $curl_results = requester_multi($multi_curl, limit: count($this->nodes), timeout: $this->timeout); + } + catch (RequesterException $e) + { + throw new RequesterException("ensure_block(block_id: {$block_id}): no connection, previously: " . $e->getMessage()); + } + + if (count($curl_results) !== 0) + { + $result = requester_multi_process($curl_results[0]); + $this->block_hash = $result['hash']; + $this->block_time = date('Y-m-d H:i:s', $this->get_block_timestamp($result['extrinsics'])); + foreach ($curl_results as $curl_result) + { + if (requester_multi_process($curl_result, result_in: 'hash') !== $this->block_hash) + { + throw new ConsensusException("ensure_block(block_id: {$block_id}): no consensus"); + } + } + } + } + + // Block timestamp must be at first extrinsic + // Returns int unix timestamp (seconds) or throw exception + function get_block_timestamp(?array $extrinsics): int + { + if (is_null($extrinsics)) + throw new ModuleException("Invalid (empty) extrinsics array in get_block_timestamp."); + + foreach ($extrinsics as $extrinsic) + { + if ($extrinsic['method']['pallet'] === 'timestamp' && $extrinsic['method']['method'] === 'set') + { + return (int)((int)$extrinsic['args']['now'] / 1000); + } + } + + throw new ModuleException("Cannot get the block timestamp."); + } + + // Try to collect fee from extrinsic events + function process_fee(array $extrinsic, string $tx_id, int &$sort_key, array &$events) + { + if (!$extrinsic['paysFee']) + return; // No fee and rewards + + $failed = !$extrinsic['success']; + $signer = $extrinsic['signature']['signer']['id'] ?? $extrinsic['signature']['signer']; + // In some chains there is no partialFee in header + $partial_fee = $extrinsic['info']['partialFee'] ?? null; + + $fee_detected = false; + foreach ($extrinsic['events'] as $event) + { + // Burnt fee + if ($event['method']['pallet'] === 'fees' && $event['method']['method'] === 'FeeToBurn') + { + $from = $event['data'][0]; + $amount = $event['data'][1]; + + [$sub, $add] = $this->generate_event_pair($tx_id, $from, 'the-void', $amount, $failed, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::BurntFee->value; + $add['extra'] = SubstrateSpecialTransactions::BurntFee->value; + array_push($events, $sub, $add); + } + + if ($event['method']['pallet'] === 'transactionPayment' && $event['method']['method'] === 'TransactionFeePaid') + { + $fee_payer = $event['data'][0]; + $fee = $event['data'][1]; + + if ($fee_payer !== $signer) + throw new ModuleException("Fee payer is not a signer ({$tx_id})."); + if ($fee !== ($partial_fee ?? $fee)) // Checks only if its getting from header + throw new ModuleException("Fee from transactionPayment and partial_fee missmatch ({$tx_id})."); + + [$sub, $add] = $this->generate_event_pair($tx_id, $signer, 'treasury', $fee, $failed, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::Fee->value; + $add['extra'] = SubstrateSpecialTransactions::Fee->value; + array_push($events, $sub, $add); + $fee_detected = true; + } + } + + // For any old blocks TransactionFeePaid can not present so we check block header instead + if ($fee_detected === false) + { + $tip = $extrinsic['tip'] ?? '0'; + $fee = bcadd($partial_fee, $tip); + + [$sub, $add] = $this->generate_event_pair($tx_id, $signer, 'treasury', $fee, $failed, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::Fee->value; + $add['extra'] = SubstrateSpecialTransactions::Fee->value; + array_push($events, $sub, $add); + } + } + + // Try to collect fee and block reward from extrinsic events + function process_fee_and_reward(array $extrinsic, string $validator, string $tx_id, int &$sort_key, array &$events) + { + if (!$extrinsic['paysFee']) + return; // No fee and rewards + + $failed = !$extrinsic['success']; + $signer = $extrinsic['signature']['signer']['id'] ?? $extrinsic['signature']['signer']; + $partial_fee = $extrinsic['info']['partialFee']; + + $fee_to_treasury = '0'; + $fee_to_validator = '0'; + for ($i = 0; $i < count($extrinsic['events'] ?? []); $i++) + { + $pallet = $extrinsic['events'][$i]['method']['pallet']; + $method = $extrinsic['events'][$i]['method']['method']; + if ($pallet === 'treasury' && $method === 'Deposit') + { + $next_i = $i + 1; + if ($next_i >= count($extrinsic['events'])) + throw new ModuleException("Cannot find event for validator reward ({$tx_id})."); + + $pallet_next = $extrinsic['events'][$next_i]['method']['pallet']; + $method_next = $extrinsic['events'][$next_i]['method']['method']; + if ($pallet_next === 'balances' && $method_next === 'Deposit') + { + // 80% of fee amount goes to treasury + $fee_to_treasury = $extrinsic['events'][$i]['data'][0]; + // 20% of fee goes to validator + $fee_to_validator = $extrinsic['events'][$next_i]['data'][1]; + + if ($extrinsic['events'][$next_i]['data'][0] !== $validator) + throw new ModuleException("Invalid event for validator reward ({$tx_id})."); + + [$sub, $add] = $this->generate_event_pair($tx_id, $signer, 'treasury', $fee_to_treasury, $failed, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::Fee->value; + $add['extra'] = SubstrateSpecialTransactions::Fee->value; + array_push($events, $sub, $add); + + [$sub, $add] = $this->generate_event_pair($tx_id, $signer, $validator, $fee_to_validator, $failed, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::Reward->value; + $add['extra'] = SubstrateSpecialTransactions::Reward->value; + array_push($events, $sub, $add); + } + } + elseif ($pallet === 'transactionPayment' && $method === 'TransactionFeePaid') + { + $fee_payer = $extrinsic['events'][$i]['data'][0]; + $fee = $extrinsic['events'][$i]['data'][1]; + + if ($fee_payer !== $signer) + throw new ModuleException("Fee payer is not a signer ({$tx_id})."); + if ($fee !== $partial_fee) + throw new ModuleException("Fee from transactionPayment and partial_fee missmatch ({$tx_id})."); + if ($fee !== bcadd($fee_to_treasury, $fee_to_validator)) + throw new ModuleException("Fee from transactionPayment and parsed fee missmatch ({$tx_id})."); + } + } + } + + function process_timestamp_pallet_deposits(array $extrinsic, string $tx_id, int &$sort_key, array &$events) + { + $failed = !$extrinsic['success']; + + foreach ($extrinsic['events'] ?? [] as $e) + { + $pallet = $e['method']['pallet']; + $method = $e['method']['method']; + if ($pallet === 'balances' && $method === 'Deposit') + { + $to = $e['data'][0]; + $amount = $e['data'][1]; + [$sub, $add] = $this->generate_event_pair($tx_id, 'the-void', $to, $amount, $failed, $sort_key); + array_push($events, $sub, $add); + } + } + } + + function process_balances_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events) + { + $failed = !$extrinsic['success']; + $signer = $extrinsic['signature']['signer']['id'] ?? $extrinsic['signature']['signer']; + + $data = ['from' => null, 'to' => null, 'amount' => null]; + switch ($extrinsic['method']['method']) + { + case 'transfer': + case 'transferKeepAlive': + case 'transferAllowDeath': + $data['from'] = $signer; + $data['to'] = $extrinsic['args']['dest']['id'] ?? $extrinsic['args']['dest']; + $data['amount'] = $extrinsic['args']['value']; + break; + + case 'transferAll': + $data['from'] = $signer; + $data['to'] = $extrinsic['args']['dest']['id'] ?? $extrinsic['args']['dest']; + $data['amount'] = $this->find_transfer_amount_in_events($extrinsic['events']); + break; + + // Root can force transfer amount from account + case 'forceTransfer': + $data['from'] = $extrinsic['args']['source']['id'] ?? $extrinsic['args']['source']; + $data['to'] = $extrinsic['args']['dest']['id'] ?? $extrinsic['args']['dest']; + $data['amount'] = $extrinsic['args']['value']; + break; + } + + // Invalid transfer data + if (in_array(null, $data) || !is_string($data['from']) || !is_string($data['to'])) + return; + + [$sub, $add] = $this->generate_event_pair($tx_id, $data['from'], $data['to'], $data['amount'], $failed, $sort_key); + array_push($events, $sub, $add); + } + + function process_staking_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events) + { + $failed = !$extrinsic['success']; + $signer = $extrinsic['signature']['signer']['id'] ?? $extrinsic['signature']['signer']; + + switch ($extrinsic['method']['method']) + { + // Some small amount deposits to signer + case 'rebond': + foreach ($extrinsic['events'] as $e) + { + if ($e['method']['pallet'] !== 'balances') + continue; + if ($e['method']['method'] === 'Deposit') + { + $to = $e['data'][0]; + $amount = $e['data'][1]; + + if ($to === $signer) + { + [$sub, $add] = $this->generate_event_pair($tx_id, 'treasury', $to, $amount, $failed, $sort_key); + array_push($events, $sub, $add); + } + } + } + break; + + case 'payoutStakers': + $collect_rewards = false; + for ($i = 0; $i < count($extrinsic['events']); $i++) + { + $current_event = $extrinsic['events'][$i]; + $current_method = $current_event['method']['method']; + $current_pallet = $current_event['method']['pallet']; + + if ($current_method === 'PayoutStarted') + $collect_rewards = true; + + if ($current_pallet === 'balances' && $current_method === 'Deposit') + { + $next_event = $extrinsic['events'][$i+1]; + $next_method = $next_event['method']['method']; + $next_pallet = $next_event['method']['pallet']; + + if ($next_pallet === 'treasury' && $next_method === 'Deposit') + $collect_rewards = false; + + if ($collect_rewards === true) + { + $to = $current_event['data'][0]; + $amount = $current_event['data'][1]; + + [$sub, $add] = $this->generate_event_pair($tx_id, 'treasury', $to, $amount, $failed, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::StakingReward->value; + $add['extra'] = SubstrateSpecialTransactions::StakingReward->value; + array_push($events, $sub, $add); + } + } + } + break; + } + } + + function process_nomination_pools_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events) + { + $failed = !$extrinsic['success']; + $signer = $extrinsic['signature']['signer']['id'] ?? $extrinsic['signature']['signer']; + + $extra = [ + 'from' => ['check' => null, 'value' => null], + 'to' => ['check' => null, 'value' => null], + 'default' => null, + ]; + switch ($extrinsic['method']['method']) + { + case 'createWithPoolId': + case 'create': + $extra['from'] = ['check' => $signer, 'value' => SubstrateSpecialTransactions::CreatePool->value]; + break; + + case 'join': + $extra['default'] = SubstrateSpecialTransactions::JoinPool->value; + break; + + case 'claimCommission': + case 'claimPayout': + $extra['to'] = ['check' => $signer, 'value' => SubstrateSpecialTransactions::StakingReward->value]; + break; + + case 'bondExtra': + $extra['to'] = ['check' => $signer, 'value' => SubstrateSpecialTransactions::StakingReward->value]; + $extra['from'] = ['check' => $signer, 'value' => SubstrateSpecialTransactions::BondExtra->value]; + break; + + case 'unbond': + case 'withdrawUnbonded': + $member = $extrinsic['args']['member_account']['id'] ?? $extrinsic['args']['member_account']; + $extra['to'] = ['check' => $member, 'value' => SubstrateSpecialTransactions::StakingReward->value]; + break; + + case 'bondExtraOther': + $member = $extrinsic['args']['member']['id'] ?? $extrinsic['args']['member']; + $extra['to'] = ['check' => $member, 'value' => SubstrateSpecialTransactions::StakingReward->value]; + $extra['from'] = ['check' => $member, 'value' => SubstrateSpecialTransactions::BondExtra->value]; + break; + + case 'claimPayoutOther': + $other = $extrinsic['args']['other']; + $extra['to'] = ['check' => $other, 'value' => SubstrateSpecialTransactions::StakingReward->value]; + break; + + default: + return; // Skip not monetary events + } + + // Parse transfer + foreach ($extrinsic['events'] as $e) + { + if ($e['method']['pallet'] !== 'balances') + continue; + + if ($e['method']['method'] === 'Transfer') + { + $from = $e['data'][0]; + $to = $e['data'][1]; + $amount = $e['data'][2]; + + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + if (!is_null($extra['from']['check']) && $extra['from']['check'] === $from) + { + $sub['extra'] = $extra['from']['value']; + $add['extra'] = $extra['from']['value']; + } + elseif (!is_null($extra['to']['check']) && $extra['to']['check'] === $to) + { + $sub['extra'] = $extra['to']['value']; + $add['extra'] = $extra['to']['value']; + } + else + { + $sub['extra'] = $extra['default']; + $add['extra'] = $extra['default']; + } + array_push($events, $sub, $add); + } + if ($e['method']['method'] === 'Deposit') + { + $from = 'pool'; + $to = $e['data'][0]; + $amount = $e['data'][1]; + + if (!is_null($extra['to']['check']) && $extra['to']['check'] === $to) + { + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + $sub['extra'] = $extra['to']['value']; + $add['extra'] = $extra['to']['value']; + array_push($events, $sub, $add); + } + } + } + } + + function process_conviction_voting_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events) + { + $failed = !$extrinsic['success']; + $signer = $extrinsic['signature']['signer']['id'] ?? $extrinsic['signature']['signer']; + + switch ($extrinsic['method']['method']) + { + case 'delegate': + case 'undelegate': + foreach ($extrinsic['events'] ?? [] as $e) + { + if ($e['method']['pallet'] !== 'balances') + continue; + + if ($e['method']['method'] === 'Deposit') + { + $from = 'treasury'; + $to = $e['data'][0]; + $amount = $e['data'][1]; + + if ($signer === $to) + { + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + array_push($events, $sub, $add); + } + } + } + break; + } + } + + function process_bounties_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events) + { + $failed = !$extrinsic['success']; + switch ($extrinsic['method']['method']) + { + case 'claimChildBounty': + case 'claimBounty': + foreach ($extrinsic['events'] ?? [] as $e) + { + if ($e['method']['pallet'] !== 'balances') + continue; + + if ($e['method']['method'] === 'Transfer') + { + $from = $e['data'][0]; + $to = $e['data'][1]; + $amount = $e['data'][2]; + + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::ClaimBounty->value; + $add['extra'] = SubstrateSpecialTransactions::ClaimBounty->value; + array_push($events, $sub, $add); + } + } + break; + } + } + + function process_currencies_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events, array &$currencies) + { + $failed = !$extrinsic['success']; + $signer = $extrinsic['signature']['signer']['id'] ?? $extrinsic['signature']['signer']; + switch ($extrinsic['method']['method']) + { + case 'transfer': + // If extrinsic is failed we need to record failed attempt + if ($failed) + { + $from = $signer; + $to = $extrinsic['args']['dest']['id'] ?? $extrinsic['args']['dest']; + $amount = $extrinsic['args']['amount']; + $currency = $this->parse_currency_id($extrinsic['args']['currency_id']); + if ($currency === $this->native_token_id) + break; + + if (is_string($to) && !is_null($to) && !is_null($amount)) + { + $currencies[] = $currency; + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + $sub['currency'] = $currency; + $add['currency'] = $currency; + array_push($events, $sub, $add); + } + } + + // Check events for exact transferred assets + foreach ($extrinsic['events'] as $event) + { + $pallet = $event['method']['pallet']; + $method = $event['method']['method']; + if ($pallet === 'currencies' && $method === 'Transferred') + { + $currency = $this->parse_currency_id($event['data'][0]); + if ($currency === $this->native_token_id) + continue; + $from = $event['data'][1]; + $to = $event['data'][2]; + $amount = $event['data'][3]; + + $currencies[] = $currency; + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + $sub['currency'] = $currency; + $add['currency'] = $currency; + array_push($events, $sub, $add); + } + } + break; + } + } + + function process_xcm_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events) + { + $failed = !$extrinsic['success']; + switch ($extrinsic['method']['method']) + { + case 'reserveTransferAssets': + case 'limitedReserveTransferAssets': + foreach ($extrinsic['events'] as $event) + { + $pallet = $event['method']['pallet']; + $method = $event['method']['method']; + if ($pallet === 'balances' && $method === 'Transfer') + { + $from = $event['data'][0]; + $to = $event['data'][1]; + $amount = $event['data'][2]; + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + array_push($events, $sub, $add); + } + } + break; + + case 'limitedTeleportAssets': + case 'teleportAssets': + $assets = $extrinsic['args']['assets']['v3'] ?? $extrinsic['args']['assets']['v2'] ?? $extrinsic['args']['assets']['v1'] ?? $extrinsic['args']['assets']['v0']; + $amounts = []; + foreach ($assets as $asset) + $amounts[] = $asset['fun']['fungible'] ?? $asset['concreteFungible']['amount']; + + for ($i = 0; $i < count($extrinsic['events']); $i++) + { + $pallet = $extrinsic['events'][$i]['method']['pallet']; + $method = $extrinsic['events'][$i]['method']['method']; + if ($pallet === 'xcmPallet' && $method === 'Attempted') + { + // A situation is possible when the extrinsic is successful but the assets are not actually teleported + if ($i < 2) + throw new ModuleException("Invalid teleport assets pattern."); + + $deposit_event = $extrinsic['events'][$i - 1]; + $withdraw_event = $extrinsic['events'][$i - 2]; + if ($deposit_event['method']['pallet'] !== 'balances' && $deposit_event['method']['method'] !== 'Deposit') + throw new ModuleException("Invalid teleport assets pattern."); + if ($withdraw_event['method']['pallet'] === 'balances' && $withdraw_event['method']['pallet'] === 'Withdraw') + throw new ModuleException("Invalid teleport assets pattern."); + + $from = $withdraw_event['data'][0]; + $to = $deposit_event['data'][0]; + $amount = $deposit_event['data'][1]; + if (!in_array($amount, $amounts)) + throw new ModuleException("Invalid parsed amount for teleport assets."); + + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + array_push($events, $sub, $add); + } + } + break; + } + } + + function process_xtokens_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events, array &$currencies) + { + $failed = !$extrinsic['success']; + switch ($extrinsic['method']['method']) + { + case 'transfer': + case 'transferMultiassets': + case 'transferMultiasset': + foreach ($extrinsic['events'] as $event) + { + $pallet = $event['method']['pallet']; + $method = $event['method']['method']; + + // Detected HRMP transfer + if ($pallet === 'currencies' && $method === 'Transferred') + { + $currency_id = $this->parse_currency_id($event['data'][0]); + $currencies[] = $currency_id; + $from = $event['data'][1]; + $to = $event['data'][2]; + $amount = $event['data'][3]; + + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + $sub['currency'] = $currency_id; + $add['currency'] = $currency_id; + array_push($events, $sub, $add); + } + // Detected UMP transfer + if ($pallet === 'tokens' && $method === 'Withdrawn') + { + $currency_id = $this->parse_currency_id($event['data'][0]); + $currencies[] = $currency_id; + $from = $event['data'][1]; + $amount = $event['data'][2]; + + [$sub, $add] = $this->generate_event_pair($tx_id, $from, 'cross-consensus', $amount, $failed, $sort_key); + $sub['currency'] = $currency_id; + $add['currency'] = $currency_id; + array_push($events, $sub, $add); + } + } + + break; + } + } + + function process_parachain_system_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events, array &$currencies) + { + $failed = !$extrinsic['success']; + switch ($extrinsic['method']['method']) + { + case 'setValidationData': + for ($i = 0; $i < count($extrinsic['events']); $i++) + { + $e = $extrinsic['events'][$i]; + $pallet = $e['method']['pallet']; + $method = $e['method']['method']; + + $from = null; + $deposits = []; + // Detected HRMP and looking for Deposits + if ($pallet === 'xcmpQueue' && $method === 'Success') + { + for ($j = $i - 1; $j >= 0; $j--) + { + $e2 = $extrinsic['events'][$j]; + $pallet2 = $e2['method']['pallet']; + $method2 = $e2['method']['method']; + + // We can find the already processed UMP + if ($pallet2 === 'xcmpQueue' && $method2 === 'Success') + break; + if (count($deposits) >= 2 && !is_null($from)) + break; + + if ($pallet2 === 'tokens' && $method2 === 'Deposited') + { + $currency = $this->parse_currency_id($e2['data'][0]); + $currencies[] = $currency; + $deposits[] = ['to' => $e2['data'][1], 'amount' => $e2['data'][2], 'currency' => $currency]; + } + if ($pallet2 === 'tokens' && $method2 === 'Withdrawn') + $from = $e2['data'][1]; + // May transferred native tokens + if ($pallet2 === 'balances' && $method2 === 'Deposit') + { + $currency = $this->native_asset_id; + $currencies[] = $currency; + $deposits[] = ['to' => $e2['data'][0], 'amount' => $e2['data'][1], 'currency' => $currency]; + } + if ($pallet2 === 'balances' && $method2 === 'Withdraw') + $from = $e2['data'][0]; + } + } + // Detected DMP and looking for Deposits + elseif ($pallet === 'dmpQueue' && $method === 'ExecutedDownward') + { + for ($j = $i - 1; $j >= 0; $j--) + { + $e2 = $extrinsic['events'][$j]; + $pallet2 = $e2['method']['pallet']; + $method2 = $e2['method']['method']; + + // We can find the already processed UMP + if ($pallet2 === 'dmpQueue' && $method2 === 'ExecutedDownward') + break; + // End of the message + if ($pallet2 === 'parachainSystem' && $method2 === 'DownwardMessagesReceived') + break; + if (count($deposits) >= 2 && !is_null($from)) + break; + + if ($pallet2 === 'tokens' && $method2 === 'Deposited') + { + $currency = $this->parse_currency_id($e2['data'][0]); + $currencies[] = $currency; + $deposits[] = ['to' => $e2['data'][1], 'amount' => $e2['data'][2], 'currency' => $currency]; + } + if ($pallet2 === 'tokens' && $method2 === 'Withdrawn') + $from = $e2['data'][1]; + } + } + + foreach ($deposits as $deposit) + { + [$sub, $add] = $this->generate_event_pair($tx_id, $from ?? 'cross-consensus', $deposit['to'], $deposit['amount'], $failed, $sort_key); + $sub['currency'] = $deposit['currency']; + $add['currency'] = $deposit['currency']; + array_push($events, $sub, $add); + } + } + + break; + } + } + + function parse_currency_id(array $cid): string + { + if (array_key_exists('token', $cid)) + return 'native-' . $cid['token']; + elseif (array_key_exists('liquidCrowdloan', $cid)) + return 'native-' . $cid['liquidCrowdloan']; + elseif (array_key_exists('stableAssetPoolToken', $cid)) + return 'stable-' . $cid['stableAssetPoolToken']; + elseif (array_key_exists('stableAsset', $cid)) + return 'stable-' . $cid['stableAsset']; + elseif (array_key_exists('foreignAsset', $cid)) + return 'foreign-' . $cid['foreignAsset']; + elseif (array_key_exists('Erc20', $cid)) + return 'erc20-' . $cid['Erc20']; + elseif (array_key_exists('erc20', $cid)) + return 'erc20-' . $cid['erc20']; + // For unknown or composite tokens from dex we cannot known the id and get the meta + else + return 'unknown'; + } + + function process_xcm_in_parainherent_pallet(array $extrinsic, string $tx_id, int &$sort_key, array &$events) + { + if ($extrinsic['method']['method'] !== 'enter') + return; + + $failed = !$extrinsic['success']; + $xcm_executed = false; + $withdraw_address = null; + $deposits = []; + for ($i = 0; $i < count($extrinsic['events'] ?? []); $i++) + { + $e = $extrinsic['events'][$i]; + $pallet = $e['method']['pallet']; + $method = $e['method']['method']; + + if ($pallet === 'ump' && $method === 'ExecutedUpward' && $xcm_executed) + throw new ModuleException("Duplicate ExecutedUpward event ({$tx_id})"); + if ($pallet === 'ump' && $method === 'ExecutedUpward') + $xcm_executed = true; + if ($pallet === 'balances' && $method === 'Withdraw') + $withdraw_address = $e['data'][0]; + + if ($pallet === 'balances' && $method === 'Deposit') + { + if (is_null($withdraw_address)) + throw new ModuleException("Unknown system account address for XCM deposit ({$tx_id})."); + + $from = $withdraw_address; + $to = $e['data'][0]; + $amount = $e['data'][1]; + $deposits[] = ['from' => $from, 'to' => $to, 'amount' => $amount]; + } + } + + if ($xcm_executed) + { + foreach ($deposits as $deposit) + { + [$sub, $add] = $this->generate_event_pair($tx_id, $deposit['from'], $deposit['to'], $deposit['amount'], $failed, $sort_key); + array_push($events, $sub, $add); + } + } + } + + // Process some additional events like DustLost etc. + function process_additional_main_events(array $extrinsic_events, bool $with_transfer, string $tx_id, bool $failed, int &$sort_key, array &$events) + { + // Check for additional events. + foreach ($extrinsic_events as $e) + { + if ($e['method']['pallet'] !== 'balances') + continue; + + switch ($e['method']['method']) + { + case 'Transfer': + if (!$with_transfer) + break; + + $from = $e['data'][0]; + $to = $e['data'][1]; + $amount = $e['data'][2]; + + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + array_push($events, $sub, $add); + break; + + // Account was killed and amount lost + case 'DustLost': + $from = $e['data'][0]; + $amount = $e['data'][1]; + + [$sub, $add] = $this->generate_event_pair($tx_id, $from, 'the-void', $amount, $failed, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::DustLost->value; + $add['extra'] = SubstrateSpecialTransactions::DustLost->value; + array_push($events, $sub, $add); + break; + + case 'BalanceSet': + $to = $e['data'][0]; + $amount = $e['data'][1]; + + [$sub, $add] = $this->generate_event_pair($tx_id, 'the-void', $to, $amount, $failed, $sort_key); + array_push($events, $sub, $add); + break; + + case 'Slashed': + $from = $e['data'][0]; + $amount = $e['data'][1]; + + [$sub, $add] = $this->generate_event_pair(null, $from, 'treasury', $amount, false, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::Slashed->value; + $add['extra'] = SubstrateSpecialTransactions::Slashed->value; + array_push($events, $sub, $add); + } + } + } + + // Process some additional events like DustLost etc. + // With none native currencies. + function process_additional_tokens_events(array $extrinsic_events, bool $with_transfer, string $tx_id, bool $failed, int &$sort_key, array &$events, array &$currencies) + { + // Check for additional events. + foreach ($extrinsic_events as $e) + { + $pallet = $e['method']['pallet']; + $method = $e['method']['method']; + if ($pallet === 'currencies' && $method === 'Transferred' && $with_transfer) + { + $currency = $this->parse_currency_id($e['data'][0]); + if ($currency === $this->native_token_id) + continue; + $from = $e['data'][1]; + $to = $e['data'][2]; + $amount = $e['data'][3]; + + $currencies[] = $currency; + [$sub, $add] = $this->generate_event_pair($tx_id, $from, $to, $amount, $failed, $sort_key); + $sub['currency'] = $currency; + $add['currency'] = $currency; + array_push($events, $sub, $add); + } + } + } + + // Some monetary operations may be at onInitialize events array. + // Like Slashing. + function process_internal_main_events(array $internal_events, int &$sort_key, array &$events) + { + foreach ($internal_events as $e) + { + $pallet = $e['method']['pallet']; + $method = $e['method']['method']; + if ($pallet === 'balances' && $method === 'Slashed') + { + $from = $e['data'][0]; + $amount = $e['data'][1]; + + [$sub, $add] = $this->generate_event_pair(null, $from, 'treasury', $amount, false, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::Slashed->value; + $add['extra'] = SubstrateSpecialTransactions::Slashed->value; + array_push($events, $sub, $add); + } + if ($pallet === 'balances' && $method === 'Transfer') + { + $from = $e['data'][0]; + $to = $e['data'][1]; + $amount = $e['data'][2]; + [$sub, $add] = $this->generate_event_pair(null, $from, $to, $amount, false, $sort_key); + array_push($events, $sub, $add); + } + } + } + + // Parse XCM events from internal events (onInitialize/onFinalize) for relay chains + function process_internal_xcm_main_events(array $internal_events, int &$sort_key, array &$events) + { + for ($i = 0; $i < count($internal_events); $i++) + { + $e = $internal_events[$i]; + $pallet = $e['method']['pallet']; + $method = $e['method']['method']; + + // Detected UMP and looking for Deposits + // Looking for Deposit -> Deposit -> Withdraw -> MessageQueueProceed pattern - this is xcm transfer to Relay from Para. + $from = null; + $deposits = []; + if ($pallet === 'messageQueue' && $method === 'Processed') + { + for ($j = $i - 1; $j >= 0; $j--) + { + $e2 = $internal_events[$j]; + $pallet2 = $e2['method']['pallet']; + $method2 = $e2['method']['method']; + + // We can find the already processed UMP + if ($pallet2 === 'messageQueue' && $method2 === 'Processed') + break; + + if ($pallet2 === 'balances' && $method2 === 'Deposit') + $deposits[] = ['to' => $e2['data'][0], 'amount' => $e2['data'][1]]; + if ($pallet2 === 'balances' && $method2 === 'Withdraw') + $from = $e2['data'][0]; + } + } + + if (!is_null($from)) + { + foreach ($deposits as $deposit) + { + [$sub, $add] = $this->generate_event_pair(null, $from, $deposit['to'], $deposit['amount'], false, $sort_key); + array_push($events, $sub, $add); + } + } + } + } + + // Helper function for transferAll etc. + function find_transfer_amount_in_events(array $extrinsic_events): ?string + { + foreach ($extrinsic_events as $event) + { + if ($event['method']['pallet'] === 'balances' && $event['method']['method'] === 'Transfer') + { + return $event['data'][2]; + } + } + return null; + } + + // Utility functions + + function generate_event_pair(?string $tx, string $src, string $dst, string $amt, bool $fld, int &$sort_key): array + { + $sub = [ + 'transaction' => $tx, + 'sort_key' => $sort_key++, + 'address' => $src, + 'effect' => '-' . $amt, + 'failed' => $fld, + 'extra' => null + ]; + $add = [ + 'transaction' => $tx, + 'sort_key' => $sort_key++, + 'address' => $dst, + 'effect' => $amt, + 'failed' => $fld, + 'extra' => null + ]; + + return [$sub, $add]; + } +} diff --git a/Modules/Common/SubstrateXCMMainModule.php b/Modules/Common/SubstrateXCMMainModule.php new file mode 100644 index 00000000..ec0dd61f --- /dev/null +++ b/Modules/Common/SubstrateXCMMainModule.php @@ -0,0 +1,119 @@ +version = 1; + } + + final public function post_post_initialize() + { + } + + final public function pre_process_block($block_id) + { + $block = requester_single($this->select_node(), endpoint: "blocks/{$block_id}", timeout: $this->timeout); + + $events = []; + $sort_key = 0; + + // Parse onInitialize xcm events (UMP) + $this->process_internal_xcm_main_events($block['onInitialize']['events'] ?? [], $sort_key, $events); + + // Parse extrinsics data + foreach ($block['extrinsics'] ?? [] as $extrinsic_number => $extrinsic) + { + $tx_id = $block_id . '-' . $extrinsic_number; + + switch ($extrinsic['method']['pallet']) + { + // In old spec UMP not in internal events + case 'paraInherent': + $this->process_xcm_in_parainherent_pallet($extrinsic, $tx_id, $sort_key, $events); + break; + + // XCM transfers processed at another module (DMP) + case 'xcmPallet': + $this->process_xcm_pallet($extrinsic, $tx_id, $sort_key, $events); + break; + + case 'multisig': + case 'utility': + $method = $extrinsic['method']['method']; + $calls = []; + + // utility + if (in_array($method, ['batch', 'batchAll', 'forceBatch'])) + $calls = $extrinsic['args']['calls']; + // utility + elseif ($method === 'asDerivative') + $calls[] = $extrinsic['args']['call']; + // multisig + elseif (in_array($method, ['asMulti', 'asMultiThreshold1'])) + $calls[] = $extrinsic['args']['call']; + + foreach ($calls ?? [] as $call) + { + $call_extrinsic = $extrinsic; + $call_extrinsic['method'] = $call['method']; + $call_extrinsic['args'] = $call['args']; + + switch ($call['method']['pallet']) + { + case 'xcmPallet': + $this->process_xcm_pallet($call_extrinsic, $tx_id, $sort_key, $events); + break; + } + } + break; + } + } + + // Parse onFinalize xcm events (UMP) + $this->process_internal_xcm_main_events($block['onFinalize']['events'] ?? [], $sort_key, $events); + + foreach ($events as &$event) + { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + } + + $this->set_return_events($events); + } +} diff --git a/Modules/Common/SubstrateXCMModule.php b/Modules/Common/SubstrateXCMModule.php new file mode 100644 index 00000000..4a953353 --- /dev/null +++ b/Modules/Common/SubstrateXCMModule.php @@ -0,0 +1,144 @@ +version = 1; + } + + final public function post_post_initialize() + { + if (is_null($this->native_asset_id)) + throw new DeveloperError('native_asset_id is not set.'); + } + + final public function pre_process_block($block_id) + { + $block = requester_single($this->select_node(), endpoint: "blocks/{$block_id}", timeout: $this->timeout); + + $events = []; + $currencies = []; + $currencies_to_process = []; + $sort_key = 0; + + // Parse extrinsics data + foreach ($block['extrinsics'] ?? [] as $extrinsic_number => $extrinsic) + { + $tx_id = $block_id . '-' . $extrinsic_number; + + switch ($extrinsic['method']['pallet']) + { + // UMP, HRMP sends parse + case 'xTokens': + $this->process_xtokens_pallet($extrinsic, $tx_id, $sort_key, $events, $currencies_to_process); + break; + + // DMP, HRMP receives parse + case 'parachainSystem': + $this->process_parachain_system_pallet($extrinsic, $tx_id, $sort_key, $events, $currencies_to_process); + break; + + case 'multisig': + case 'utility': + $method = $extrinsic['method']['method']; + $calls = []; + + // utility + if (in_array($method, ['batch', 'batchAll', 'forceBatch'])) + $calls = $extrinsic['args']['calls']; + // utility + elseif ($method === 'asDerivative') + $calls[] = $extrinsic['args']['call']; + // multisig + elseif (in_array($method, ['asMulti', 'asMultiThreshold1'])) + $calls[] = $extrinsic['args']['call']; + + foreach ($calls ?? [] as $call) + { + $call_extrinsic = $extrinsic; + $call_extrinsic['method'] = $call['method']; + $call_extrinsic['args'] = $call['args']; + + switch ($call['method']['pallet']) + { + case 'xTokens': + $this->process_xtokens_pallet($call_extrinsic, $tx_id, $sort_key, $events, $currencies_to_process); + break; + } + } + break; + } + } + + foreach ($events as &$event) + { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + } + + $currencies_to_process = array_unique($currencies_to_process); + $currencies_to_process = check_existing_currencies($currencies_to_process, $this->currency_format); + + $assets_meta = requester_single($this->select_node(), endpoint: "/pallets/asset-registry", result_in: 'items', timeout: $this->timeout); + foreach ($currencies_to_process as $currency) + { + $meta = $assets_meta[$currency]; + $currencies[] = [ + 'id' => $currency, + 'name' => $meta['name'], + 'symbol' => $meta['symbol'], + 'decimals' => $meta['decimals'], + ]; + } + + $this->set_return_events($events); + $this->set_return_currencies($currencies); + } +} diff --git a/Modules/KusamaMainModule.php b/Modules/KusamaMainModule.php new file mode 100644 index 00000000..ddcb137b --- /dev/null +++ b/Modules/KusamaMainModule.php @@ -0,0 +1,24 @@ +blockchain = 'kusama'; + $this->module = 'kusama-main'; + $this->is_main = true; + $this->first_block_date = '2019-11-28'; + $this->currency = 'kusama'; + $this->currency_details = ['name' => 'Kusama', 'symbol' => 'KSM', 'decimals' => 12, 'description' => null]; + + // Substrait-specific + $this->chain_type = SubstrateChainType::Relay; + } +} diff --git a/Modules/KusamaXCMModule.php b/Modules/KusamaXCMModule.php new file mode 100644 index 00000000..be8dc352 --- /dev/null +++ b/Modules/KusamaXCMModule.php @@ -0,0 +1,20 @@ +blockchain = 'kusama'; + $this->module = 'kusama-xcm'; + $this->is_main = false; + $this->complements = 'kusama-main'; + $this->first_block_date = '2019-11-28'; + } +} diff --git a/Modules/PolkadotMainModule.php b/Modules/PolkadotMainModule.php new file mode 100644 index 00000000..8e9106ae --- /dev/null +++ b/Modules/PolkadotMainModule.php @@ -0,0 +1,25 @@ +blockchain = 'polkadot'; + $this->module = 'polkadot-main'; + $this->is_main = true; + $this->first_block_date = '2020-05-26'; + $this->currency = 'polkadot'; + // The denomination of DOT was changed from 12 decimals of precision at block #1,248,328 in an event known as Denomination Day + $this->currency_details = ['name' => 'Polkadot', 'symbol' => 'DOT', 'decimals' => 10, 'description' => null]; + + // Substrate-specific + $this->chain_type = SubstrateChainType::Relay; + } +} diff --git a/Modules/PolkadotXCMModule.php b/Modules/PolkadotXCMModule.php new file mode 100644 index 00000000..f03cec5c --- /dev/null +++ b/Modules/PolkadotXCMModule.php @@ -0,0 +1,20 @@ +blockchain = 'polkadot'; + $this->module = 'polkadot-xcm'; + $this->complements = 'polkadot-main'; + $this->is_main = false; + $this->first_block_date = '2020-05-26'; + } +} From 50dbbaf15b2fc61972b4445eddc940c7ea50a2a4 Mon Sep 17 00:00:00 2001 From: oskal174 Date: Mon, 25 Dec 2023 12:28:30 +0500 Subject: [PATCH 2/4] cleanup --- Modules/AstarEVMMainModule.php | 1 - Modules/CentrifugeEVMMainModule.php | 1 - 2 files changed, 2 deletions(-) diff --git a/Modules/AstarEVMMainModule.php b/Modules/AstarEVMMainModule.php index 81c128ac..3d3e9448 100644 --- a/Modules/AstarEVMMainModule.php +++ b/Modules/AstarEVMMainModule.php @@ -22,7 +22,6 @@ function initialize() $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; // EVM-specific - // TODO: checks params $this->evm_implementation = EVMImplementation::geth; $this->extra_features = []; $this->reward_function = function($block_id) diff --git a/Modules/CentrifugeEVMMainModule.php b/Modules/CentrifugeEVMMainModule.php index bdd96be5..c6b0be43 100644 --- a/Modules/CentrifugeEVMMainModule.php +++ b/Modules/CentrifugeEVMMainModule.php @@ -23,7 +23,6 @@ function initialize() $this->transaction_hash_format = TransactionHashFormat::AlphaNumeric; // EVM-specific - // TODO: checks params $this->evm_implementation = EVMImplementation::geth; $this->extra_features = []; $this->reward_function = function($block_id) From 08b8d6029e60e9483a700f194306b106226d7c6d Mon Sep 17 00:00:00 2001 From: alexqrid <> Date: Fri, 14 Jun 2024 05:06:47 +0300 Subject: [PATCH 3/4] fixed fee logic for old and new type of fees, added substrate address decoding --- Engine/Crypto/Base58.php | 4 +- Engine/Crypto/SS58.php | 77 ++++++++++++++++++++++++++ Modules/AstarMainModule.php | 9 +++ Modules/CentrifugeMainModule.php | 10 +++- Modules/Common/SubstrateMainModule.php | 10 +++- Modules/Common/SubstrateTraits.php | 62 ++++++++++++++++----- Modules/KusamaMainModule.php | 12 +++- Modules/PolkadotMainModule.php | 10 ++++ 8 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 Engine/Crypto/SS58.php diff --git a/Engine/Crypto/Base58.php b/Engine/Crypto/Base58.php index bbb1973a..4ee9c1bc 100644 --- a/Engine/Crypto/Base58.php +++ b/Engine/Crypto/Base58.php @@ -7,7 +7,7 @@ final class Base58 { private const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; - private static function base58_encode($string) + public static function base58_encode($string) { if (!$string) return ''; @@ -51,7 +51,7 @@ private static function base58_encode($string) return $output; } - private static function base58_decode($base58) + public static function base58_decode($base58) { if (!$base58) return ''; diff --git a/Engine/Crypto/SS58.php b/Engine/Crypto/SS58.php new file mode 100644 index 00000000..9e091f4b --- /dev/null +++ b/Engine/Crypto/SS58.php @@ -0,0 +1,77 @@ + 16383)) { + return null; + } + elseif (!in_array(strlen($key),self::ALLOWED_DECODED_LENGTHS)) { + return null; + } + $prefix = $ss58_format < 64 ? [$ss58_format] : [ + (($ss58_format & 252) >> 2) | 64, + ($ss58_format >> 8) | (($ss58_format & 3) << 6) + ]; + $dataWithPrefix = join(array_map("chr", $prefix)) . $key; + $checksum_length = in_array(strlen($key),[32,33]) ? 2 : 1; + $checksum = substr(sodium_crypto_generichash("SS58PRE" . $dataWithPrefix, '', 64), 0, $checksum_length); + + $dataWithChecksum = $dataWithPrefix . $checksum; + + return Base58::base58_encode($dataWithChecksum); + } + + /** + * @throws SodiumException + */ + public static function ss58_decode(string $ss58_address, $ignore_checksum=true): string + { + if (strlen($ss58_address) == 64) + { + // dirty hack for encode function + return hex2bin($ss58_address); + } + $decoded_data = Base58::base58_decode($ss58_address); + if (!in_array(strlen($decoded_data), self::ALLOWED_ENCODED_LENGTHS)) { + return ""; + } + // dirty hack for \x00 prefix + if (str_starts_with(bin2hex($decoded_data), '5c783030')) { + $decoded_data = unpack("C*",hex2bin(str_replace("5c783030","00",bin2hex($decoded_data)))); + $decoded_data = join(array_map("chr",$decoded_data)); + } + // Calculate address checksum + $ss58_length = (ord($decoded_data[0]) & 64) ? 2 : 1; + // prefix of the parachain decoded in $ss58_decoded +// $ss58_decoded = $ss58_length === 1 ? ord($decoded_data[0]) +// : ((ord($decoded_data[0]) & 63) << 2) | (ord($decoded_data[1]) >> 6) | ((ord($decoded_data[1]) & 63) << 8); + $isPublicKey = in_array(strlen($decoded_data), [34 + $ss58_length, 35 + $ss58_length]); + $length = strlen($decoded_data) - ($isPublicKey ? 2 : 1); + $data = "SS58PRE" . substr($decoded_data, 0, $length); + $hash = substr(sodium_crypto_generichash($data, '', 64), 0, 2); + + $isValid = (ord($decoded_data[0]) & 128) === 0 && !in_array(ord($decoded_data[0]), [46, 47]) && ($isPublicKey + ? str_ends_with($decoded_data, $hash) + : substr($decoded_data, -1) === $hash[0]); + if (!$isValid && !$ignore_checksum) { + return ""; + } + return bin2hex(substr($decoded_data, $ss58_length, $length - $ss58_length)); + } +} \ No newline at end of file diff --git a/Modules/AstarMainModule.php b/Modules/AstarMainModule.php index a69c892b..80e73d5a 100644 --- a/Modules/AstarMainModule.php +++ b/Modules/AstarMainModule.php @@ -17,8 +17,17 @@ function initialize() $this->first_block_date = '2021-12-18'; $this->currency = 'astar'; $this->currency_details = ['name' => 'Astar', 'symbol' => 'ASTR', 'decimals' => 18, 'description' => null]; + $this->handles_regex = '^([\w\d ]+)?((//?[^/]+)*)$'; + // this is a hack to return astar address if any substrate address provided + $this->api_get_handle = function ($handle) { + $address = $this->decode_address($handle); + if ($address == '') + return null; + return $address; + }; // Substrate-specific $this->chain_type = SubstrateChainType::Para; + $this->network_prefix = SUBSTRATE_NETWORK_PREFIX::Astar; } } diff --git a/Modules/CentrifugeMainModule.php b/Modules/CentrifugeMainModule.php index 274f4809..76ede8d8 100644 --- a/Modules/CentrifugeMainModule.php +++ b/Modules/CentrifugeMainModule.php @@ -17,8 +17,16 @@ function initialize() $this->first_block_date = '2022-03-12'; $this->currency = 'centrifuge'; $this->currency_details = ['name' => 'Centrifuge', 'symbol' => 'CFG', 'decimals' => 18, 'description' => null]; - + $this->handles_regex = '^([\w\d ]+)?((//?[^/]+)*)$'; + // this is a hack to return centrifuge address if any substrate address provided + $this->api_get_handle = function ($handle) { + $address = $this->decode_address($handle); + if ($address == '') + return null; + return $address; + }; // Substrate-specific $this->chain_type = SubstrateChainType::Para; + $this->network_prefix = SUBSTRATE_NETWORK_PREFIX::Centrifuge; } } diff --git a/Modules/Common/SubstrateMainModule.php b/Modules/Common/SubstrateMainModule.php index 52645959..3209259f 100644 --- a/Modules/Common/SubstrateMainModule.php +++ b/Modules/Common/SubstrateMainModule.php @@ -8,6 +8,7 @@ * This module process the main transfer events for Substrate SDK blockchains. * Works with Substrate Sidecar API: https://paritytech.github.io/substrate-api-sidecar/dist/. */ +require_once __DIR__ . "/../../Engine/Crypto/SS58.php"; abstract class SubstrateMainModule extends CoreModule { @@ -23,7 +24,9 @@ abstract class SubstrateMainModule extends CoreModule // treasury - special address for paid fee and etc. // the-void - special address for mint/burn events. // pool - special address for rewards from nomination pool. - public ?array $special_addresses = ['treasury', 'the-void', 'pool']; + public ?SUBSTRATE_NETWORK_PREFIX $network_prefix = null; + public ?string $treasury_address = null; + public ?array $special_addresses = ['the-void', 'pool']; public ?PrivacyModel $privacy_model = PrivacyModel::Transparent; public ?array $events_table_fields = ['block', 'transaction', 'sort_key', 'time', 'address', 'effect', 'failed', 'extra']; @@ -61,6 +64,11 @@ final public function post_post_initialize() { if (is_null($this->chain_type)) throw new DeveloperError('Chain type is not set (developer error).'); + if (is_null($this->network_prefix)) + throw new DeveloperError("Chain prefix should be set (developer error)."); + // bin2hex("modlpy/trsry") . str_repeat("0", 64-strlen(bin2hex("modlpy/trsry"))) + $this->treasury_address = SS58::ss58_encode("6d6f646c70792f74727372790000000000000000000000000000000000000000", $this->network_prefix->value); + } final public function pre_process_block($block_id) diff --git a/Modules/Common/SubstrateTraits.php b/Modules/Common/SubstrateTraits.php index e79e2647..5995d066 100644 --- a/Modules/Common/SubstrateTraits.php +++ b/Modules/Common/SubstrateTraits.php @@ -5,13 +5,21 @@ * Distributed under the MIT software license, see LICENSE.md */ /* Common Substrate functions and enums */ - enum SubstrateChainType { case Relay; case Para; } +enum SUBSTRATE_NETWORK_PREFIX: int { + case Polkadot = 0; + case Kusama = 2; + + case Astar = 5; + + case Centrifuge = 36; +} + enum SubstrateSpecialTransactions: string { case Fee = 'f'; @@ -119,7 +127,7 @@ function process_fee(array $extrinsic, string $tx_id, int &$sort_key, array &$ev if ($fee !== ($partial_fee ?? $fee)) // Checks only if its getting from header throw new ModuleException("Fee from transactionPayment and partial_fee missmatch ({$tx_id})."); - [$sub, $add] = $this->generate_event_pair($tx_id, $signer, 'treasury', $fee, $failed, $sort_key); + [$sub, $add] = $this->generate_event_pair($tx_id, $signer, $this->treasury_address, $fee, $failed, $sort_key); $sub['extra'] = SubstrateSpecialTransactions::Fee->value; $add['extra'] = SubstrateSpecialTransactions::Fee->value; array_push($events, $sub, $add); @@ -133,7 +141,7 @@ function process_fee(array $extrinsic, string $tx_id, int &$sort_key, array &$ev $tip = $extrinsic['tip'] ?? '0'; $fee = bcadd($partial_fee, $tip); - [$sub, $add] = $this->generate_event_pair($tx_id, $signer, 'treasury', $fee, $failed, $sort_key); + [$sub, $add] = $this->generate_event_pair($tx_id, $signer, $this->treasury_address, $fee, $failed, $sort_key); $sub['extra'] = SubstrateSpecialTransactions::Fee->value; $add['extra'] = SubstrateSpecialTransactions::Fee->value; array_push($events, $sub, $add); @@ -156,7 +164,13 @@ function process_fee_and_reward(array $extrinsic, string $validator, string $tx_ { $pallet = $extrinsic['events'][$i]['method']['pallet']; $method = $extrinsic['events'][$i]['method']['method']; - if ($pallet === 'treasury' && $method === 'Deposit') + // Here is 2 cases + // 1. treasury(Deposit) without treasury address and the next event is balances(Deposit) to validator + // 2. balances(Deposit) to treasury address "modlpy/trsry" (ss58 encoded) and the next event is balances(Deposit) to validator + if (($pallet === 'treasury' && $method === 'Deposit' && $fee_to_treasury === '0') || + ($pallet === 'balances' && $method === 'Deposit' && $fee_to_treasury === '0' && $extrinsic['events'][$i+1]['method']['pallet'] != 'treasury') + && $extrinsic['events'][$i]['data'][0] === $this->treasury_address + ) { $next_i = $i + 1; if ($next_i >= count($extrinsic['events'])) @@ -167,14 +181,14 @@ function process_fee_and_reward(array $extrinsic, string $validator, string $tx_ if ($pallet_next === 'balances' && $method_next === 'Deposit') { // 80% of fee amount goes to treasury - $fee_to_treasury = $extrinsic['events'][$i]['data'][0]; + $fee_to_treasury = $pallet == "balances" ? $extrinsic['events'][$i]['data'][1] : $extrinsic['events'][$i]['data'][0] ; // 20% of fee goes to validator $fee_to_validator = $extrinsic['events'][$next_i]['data'][1]; if ($extrinsic['events'][$next_i]['data'][0] !== $validator) throw new ModuleException("Invalid event for validator reward ({$tx_id})."); - [$sub, $add] = $this->generate_event_pair($tx_id, $signer, 'treasury', $fee_to_treasury, $failed, $sort_key); + [$sub, $add] = $this->generate_event_pair($tx_id, $signer, $this->treasury_address, $fee_to_treasury, $failed, $sort_key); $sub['extra'] = SubstrateSpecialTransactions::Fee->value; $add['extra'] = SubstrateSpecialTransactions::Fee->value; array_push($events, $sub, $add); @@ -189,7 +203,6 @@ function process_fee_and_reward(array $extrinsic, string $validator, string $tx_ { $fee_payer = $extrinsic['events'][$i]['data'][0]; $fee = $extrinsic['events'][$i]['data'][1]; - if ($fee_payer !== $signer) throw new ModuleException("Fee payer is not a signer ({$tx_id})."); if ($fee !== $partial_fee) @@ -198,6 +211,24 @@ function process_fee_and_reward(array $extrinsic, string $validator, string $tx_ throw new ModuleException("Fee from transactionPayment and parsed fee missmatch ({$tx_id})."); } } + + // Handle special case for early blocks when fee was paid only to validator + // example polkadot blocks 4206698, 4206696 + if ($fee_to_treasury === '0' && $fee_to_validator === '0') + { + for ($i =count($extrinsic['events'] ?? [1])-1; $i >0 ; $i--) + { + if ($extrinsic['events'][$i]['method']['pallet'] === 'balances' && $extrinsic['events'][$i]['method']['method'] === 'Deposit' + && $extrinsic['events'][$i]['data'][0] === $validator) + { + $fee_to_validator = $extrinsic['events'][$i]['data'][1]; + [$sub, $add] = $this->generate_event_pair($tx_id, $signer, $validator, $fee_to_validator, $failed, $sort_key); + $sub['extra'] = SubstrateSpecialTransactions::Reward->value; + $add['extra'] = SubstrateSpecialTransactions::Reward->value; + array_push($events, $sub, $add); + } + } + } } function process_timestamp_pallet_deposits(array $extrinsic, string $tx_id, int &$sort_key, array &$events) @@ -276,7 +307,7 @@ function process_staking_pallet(array $extrinsic, string $tx_id, int &$sort_key, if ($to === $signer) { - [$sub, $add] = $this->generate_event_pair($tx_id, 'treasury', $to, $amount, $failed, $sort_key); + [$sub, $add] = $this->generate_event_pair($tx_id, $this->treasury_address, $to, $amount, $failed, $sort_key); array_push($events, $sub, $add); } } @@ -300,7 +331,7 @@ function process_staking_pallet(array $extrinsic, string $tx_id, int &$sort_key, $next_method = $next_event['method']['method']; $next_pallet = $next_event['method']['pallet']; - if ($next_pallet === 'treasury' && $next_method === 'Deposit') + if ($next_pallet === $this->treasury_address && $next_method === 'Deposit') $collect_rewards = false; if ($collect_rewards === true) @@ -308,7 +339,7 @@ function process_staking_pallet(array $extrinsic, string $tx_id, int &$sort_key, $to = $current_event['data'][0]; $amount = $current_event['data'][1]; - [$sub, $add] = $this->generate_event_pair($tx_id, 'treasury', $to, $amount, $failed, $sort_key); + [$sub, $add] = $this->generate_event_pair($tx_id, $this->treasury_address, $to, $amount, $failed, $sort_key); $sub['extra'] = SubstrateSpecialTransactions::StakingReward->value; $add['extra'] = SubstrateSpecialTransactions::StakingReward->value; array_push($events, $sub, $add); @@ -434,7 +465,7 @@ function process_conviction_voting_pallet(array $extrinsic, string $tx_id, int & if ($e['method']['method'] === 'Deposit') { - $from = 'treasury'; + $from = $this->treasury_address; $to = $e['data'][0]; $amount = $e['data'][1]; @@ -836,7 +867,7 @@ function process_additional_main_events(array $extrinsic_events, bool $with_tran $from = $e['data'][0]; $amount = $e['data'][1]; - [$sub, $add] = $this->generate_event_pair(null, $from, 'treasury', $amount, false, $sort_key); + [$sub, $add] = $this->generate_event_pair(null, $from, $this->treasury_address, $amount, false, $sort_key); $sub['extra'] = SubstrateSpecialTransactions::Slashed->value; $add['extra'] = SubstrateSpecialTransactions::Slashed->value; array_push($events, $sub, $add); @@ -884,7 +915,7 @@ function process_internal_main_events(array $internal_events, int &$sort_key, ar $from = $e['data'][0]; $amount = $e['data'][1]; - [$sub, $add] = $this->generate_event_pair(null, $from, 'treasury', $amount, false, $sort_key); + [$sub, $add] = $this->generate_event_pair(null, $from, $this->treasury_address, $amount, false, $sort_key); $sub['extra'] = SubstrateSpecialTransactions::Slashed->value; $add['extra'] = SubstrateSpecialTransactions::Slashed->value; array_push($events, $sub, $add); @@ -979,4 +1010,9 @@ function generate_event_pair(?string $tx, string $src, string $dst, string $amt, return [$sub, $add]; } + + function decode_address($address): string + { + return SS58::ss58_encode($address,$this->network_prefix->value); + } } diff --git a/Modules/KusamaMainModule.php b/Modules/KusamaMainModule.php index ddcb137b..37c9f83f 100644 --- a/Modules/KusamaMainModule.php +++ b/Modules/KusamaMainModule.php @@ -17,8 +17,18 @@ function initialize() $this->first_block_date = '2019-11-28'; $this->currency = 'kusama'; $this->currency_details = ['name' => 'Kusama', 'symbol' => 'KSM', 'decimals' => 12, 'description' => null]; - + $this->handles_implemented = true; + $this->handles_regex = '^([\w\d ]+)?((//?[^/]+)*)$'; + // this is a hack to return kusama address if any substrate address provided + $this->api_get_handle = function ($handle) { + $address = $this->decode_address($handle); + if ($address == '') + return null; + return $address; + }; // Substrait-specific $this->chain_type = SubstrateChainType::Relay; + $this->network_prefix = SUBSTRATE_NETWORK_PREFIX::Kusama; + } } diff --git a/Modules/PolkadotMainModule.php b/Modules/PolkadotMainModule.php index 8e9106ae..8258a8db 100644 --- a/Modules/PolkadotMainModule.php +++ b/Modules/PolkadotMainModule.php @@ -18,8 +18,18 @@ function initialize() $this->currency = 'polkadot'; // The denomination of DOT was changed from 12 decimals of precision at block #1,248,328 in an event known as Denomination Day $this->currency_details = ['name' => 'Polkadot', 'symbol' => 'DOT', 'decimals' => 10, 'description' => null]; + $this->handles_regex = '^([\w\d ]+)?((//?[^/]+)*)$'; + // this is a hack to return polkadot address if any substrate address provided + $this->api_get_handle = function ($handle) { + $address = $this->decode_address($handle); + if ($address == '') + return null; + return $address; + }; // Substrate-specific $this->chain_type = SubstrateChainType::Relay; + $this->network_prefix = SUBSTRATE_NETWORK_PREFIX::Polkadot; + } } From 2b9f555dc073a086f7caa8b6d0219f1b1bc4f5a3 Mon Sep 17 00:00:00 2001 From: alexqrid <> Date: Fri, 14 Jun 2024 05:13:56 +0300 Subject: [PATCH 4/4] fixed typo and preserve naming convention --- Engine/Crypto/SS58.php | 19 +++++++++---------- Modules/Common/SubstrateTraits.php | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Engine/Crypto/SS58.php b/Engine/Crypto/SS58.php index 9e091f4b..54714046 100644 --- a/Engine/Crypto/SS58.php +++ b/Engine/Crypto/SS58.php @@ -28,13 +28,13 @@ public static function ss58_encode($accountId, $ss58_format = 42): ?string (($ss58_format & 252) >> 2) | 64, ($ss58_format >> 8) | (($ss58_format & 3) << 6) ]; - $dataWithPrefix = join(array_map("chr", $prefix)) . $key; + $data_with_prefix = join(array_map("chr", $prefix)) . $key; $checksum_length = in_array(strlen($key),[32,33]) ? 2 : 1; - $checksum = substr(sodium_crypto_generichash("SS58PRE" . $dataWithPrefix, '', 64), 0, $checksum_length); + $checksum = substr(sodium_crypto_generichash("SS58PRE" . $data_with_prefix, '', 64), 0, $checksum_length); - $dataWithChecksum = $dataWithPrefix . $checksum; + $data_with_checksum = $data_with_prefix . $checksum; - return Base58::base58_encode($dataWithChecksum); + return Base58::base58_encode($data_with_checksum); } /** @@ -59,17 +59,16 @@ public static function ss58_decode(string $ss58_address, $ignore_checksum=true): // Calculate address checksum $ss58_length = (ord($decoded_data[0]) & 64) ? 2 : 1; // prefix of the parachain decoded in $ss58_decoded -// $ss58_decoded = $ss58_length === 1 ? ord($decoded_data[0]) -// : ((ord($decoded_data[0]) & 63) << 2) | (ord($decoded_data[1]) >> 6) | ((ord($decoded_data[1]) & 63) << 8); - $isPublicKey = in_array(strlen($decoded_data), [34 + $ss58_length, 35 + $ss58_length]); - $length = strlen($decoded_data) - ($isPublicKey ? 2 : 1); + // $ss58_decoded = $ss58_length === 1 ? ord($decoded_data[0]) : ((ord($decoded_data[0]) & 63) << 2) | (ord($decoded_data[1]) >> 6) | ((ord($decoded_data[1]) & 63) << 8); + $is_public_key = in_array(strlen($decoded_data), [34 + $ss58_length, 35 + $ss58_length]); + $length = strlen($decoded_data) - ($is_public_key ? 2 : 1); $data = "SS58PRE" . substr($decoded_data, 0, $length); $hash = substr(sodium_crypto_generichash($data, '', 64), 0, 2); - $isValid = (ord($decoded_data[0]) & 128) === 0 && !in_array(ord($decoded_data[0]), [46, 47]) && ($isPublicKey + $is_valid = (ord($decoded_data[0]) & 128) === 0 && !in_array(ord($decoded_data[0]), [46, 47]) && ($is_public_key ? str_ends_with($decoded_data, $hash) : substr($decoded_data, -1) === $hash[0]); - if (!$isValid && !$ignore_checksum) { + if (!$is_valid && !$ignore_checksum) { return ""; } return bin2hex(substr($decoded_data, $ss58_length, $length - $ss58_length)); diff --git a/Modules/Common/SubstrateTraits.php b/Modules/Common/SubstrateTraits.php index 5995d066..95b1d2ea 100644 --- a/Modules/Common/SubstrateTraits.php +++ b/Modules/Common/SubstrateTraits.php @@ -331,7 +331,7 @@ function process_staking_pallet(array $extrinsic, string $tx_id, int &$sort_key, $next_method = $next_event['method']['method']; $next_pallet = $next_event['method']['pallet']; - if ($next_pallet === $this->treasury_address && $next_method === 'Deposit') + if ($next_pallet === 'treasury' && $next_method === 'Deposit') $collect_rewards = false; if ($collect_rewards === true)