diff --git a/README.md b/README.md index 873d46e..7712a94 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ This is a pure PHP SDK for working with Minter blockchain - [Unbond](#example-13) - [MultiSend](#example-14) - [EditCandidate](#example-15) + - [CreateMultisig](#example-16) + - [Sign transaction with multisignatures](#sign-transaction-with-multisignatures) - [Get fee of transaction](#get-fee-of-transaction) - [Get hash of transaction](#get-hash-of-transaction) - [Decode Transaction](#decode-transaction) @@ -59,7 +61,7 @@ composer require minter/minter-php-sdk ## Using MinterAPI -You can get all valid responses and full documentation at [Minter Node Api](https://minter-go-node.readthedocs.io/en/latest/api.html) +You can get all valid responses and full documentation at [Minter Node Api](https://docs.minter.network/) Create MinterAPI instance @@ -290,6 +292,27 @@ Returns a signed tx. use Minter\SDK\MinterTx; use Minter\SDK\MinterCoins\MinterSendCoinTx; +$tx = new MinterTx([ + 'nonce' => $nonce, + 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID + 'type' => MinterSendCoinTx::TYPE, + 'data' => [ + 'coin' => 'MNT', + 'to' => 'Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99', + 'value' => '10' + ] +]); + +$tx->sign('your private key') +``` + +At all type of transactions you can also specify: +gasPrice, gasCoin, payload, serviceData + +```php +use Minter\SDK\MinterTx; +use Minter\SDK\MinterCoins\MinterSendCoinTx; + $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID @@ -301,9 +324,8 @@ $tx = new MinterTx([ 'to' => 'Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99', 'value' => '10' ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + 'payload' => 'some message', + 'serviceData' => 'some service data' ]); $tx->sign('your private key') @@ -319,18 +341,13 @@ use Minter\SDK\MinterCoins\MinterSellCoinTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterSellCoinTx::TYPE, 'data' => [ 'coinToSell' => 'MNT', 'valueToSell' => '1', 'coinToBuy' => 'TEST', 'minimumValueToBuy' => 1 - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -346,17 +363,12 @@ use Minter\SDK\MinterCoins\MinterSellAllCoinTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterSellAllCoinTx::TYPE, 'data' => [ 'coinToSell' => 'TEST', 'coinToBuy' => 'MNT', 'minimumValueToBuy' => 1 - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -372,18 +384,13 @@ use Minter\SDK\MinterCoins\MinterBuyCoinTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterBuyCoinTx::TYPE, 'data' => [ 'coinToBuy' => 'MNT', 'valueToBuy' => '1', 'coinToSell' => 'TEST', 'maximumValueToSell' => 1 - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -399,19 +406,15 @@ use Minter\SDK\MinterCoins\MinterCreateCoinTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterCreateCoinTx::TYPE, 'data' => [ 'name' => 'TEST COIN', 'symbol' => 'TEST', 'initialAmount' => '100', 'initialReserve' => '10', - 'crr' => 10 - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + 'crr' => 10, + 'maxSupply' => '10000' + ] ]); $tx->sign('your private key') @@ -427,8 +430,6 @@ use Minter\SDK\MinterCoins\MinterDeclareCandidacyTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterDeclareCandidacyTx::TYPE, 'data' => [ 'address' => 'Mxa7bc33954f1ce855ed1a8c768fdd32ed927def47', @@ -436,10 +437,7 @@ $tx = new MinterTx([ 'commission' => 10, 'coin' => 'MNT', 'stake' => '5' - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -455,17 +453,12 @@ use Minter\SDK\MinterCoins\MinterDelegateTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterDelegateTx::TYPE, 'data' => [ 'pubkey' => 'Mp0eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43', 'coin' => 'MNT', 'stake' => '5' - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -481,15 +474,10 @@ use Minter\SDK\MinterCoins\MinterSetCandidateOnTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterSetCandidateOnTx::TYPE, 'data' => [ 'pubkey' => 'Mp0eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43' - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -505,15 +493,10 @@ use Minter\SDK\MinterCoins\MinterSetCandidateOffTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterSetCandidateOffTx::TYPE, 'data' => [ 'pubkey' => 'Mp0eb98ea04ae466d8d38f490db3c99b3996a90e24243952ce9822c6dc1e2c1a43' - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -529,16 +512,11 @@ use Minter\SDK\MinterCoins\MinterRedeemCheckTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterRedeemCheckTx::TYPE, 'data' => [ 'check' => 'your check', 'proof' => 'created by MinterCheck proof' - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -554,17 +532,12 @@ use Minter\SDK\MinterCoins\MinterUnbondTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterUnbondTx::TYPE, 'data' => [ 'pubkey' => 'Mp....', 'coin' => 'MNT', 'value' => '1' - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -580,8 +553,6 @@ use Minter\SDK\MinterCoins\MinterMultiSendTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterMultiSendTx::TYPE, 'data' => [ 'list' => [ @@ -595,10 +566,7 @@ $tx = new MinterTx([ 'value' => '15' ] ] - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] ]); $tx->sign('your private key') @@ -614,22 +582,73 @@ use Minter\SDK\MinterCoins\MinterEditCandidateTx; $tx = new MinterTx([ 'nonce' => $nonce, 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID - 'gasPrice' => 1, - 'gasCoin' => 'MNT', 'type' => MinterEditCandidateTx::TYPE, 'data' => [ 'pubkey' => 'candidate public key', 'reward_address' => 'Minter address for rewards', 'owner_address' => 'Minter address of owner' - ], - 'payload' => '', - 'serviceData' => '', - 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE // or SIGNATURE_MULTI_TYPE + ] +]); + +$tx->sign('your private key') +``` + +###### Example +* Sign the CreateMultisig transaction + +```php +use Minter\SDK\MinterTx; +use Minter\SDK\MinterCoins\MinterCreateMultisigTx; + +$tx = new MinterTx([ + 'nonce' => $nonce, + 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID + 'type' => MinterCreateMultisigTx::TYPE, + 'data' => [ + 'threshold' => 7, + 'weights' => [1, 3, 5], + 'addresses' => [ + 'Mxee81347211c72524338f9680072af90744333143', + 'Mxee81347211c72524338f9680072af90744333145', + 'Mxee81347211c72524338f9680072af90744333144' + ] + ] ]); $tx->sign('your private key') ``` +### Sign transaction with multisignatures + +Returns a signed tx. + +###### Example + +* To sign transaction with multisignatures, you need to call signMultisig method +and specify multisig Minter address and his private keys (in any order). + +```php +use Minter\SDK\MinterTx; +use Minter\SDK\MinterCoins\MinterSendCoinTx; + +$tx = new MinterTx([ + 'nonce' => $nonce, + 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID + 'type' => MinterSendCoinTx::TYPE, + 'data' => [ + 'coin' => 'MNT', + 'to' => 'Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99', + 'value' => '10' + ] +]); + +$signedTx = $tx->signMultisig('Mxdb4f4b6942cb927e8d7e3a1f602d0f1fb43b5bd2', [ + 'b354c3d1d456d5a1ddd65ca05fd710117701ec69d82dac1858986049a0385af9', + '38b7dfb77426247aed6081f769ed8f62aaec2ee2b38336110ac4f7484478dccb', + '94c0915734f92dd66acfdc48f82b1d0b208efd544fe763386160ec30c968b4af' +]) +``` + ### Get fee of transaction * Calculate fee of transaction. You can get fee AFTER signing or decoding transaction. @@ -694,7 +713,8 @@ $check = new MinterCheck([ 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID 'dueBlock' => 999999, 'coin' => 'MNT', - 'value' => '10' + 'value' => '10', + 'gasCoin' => 'MNT' ], 'your pass phrase'); echo $check->sign('your private key here'); @@ -761,6 +781,14 @@ use Minter\SDK\MinterWallet; $privateKey = MinterWallet::seedToPrivateKey($seed); ``` +* Get private key from mnemonic. + +```php +use Minter\SDK\MinterWallet; + +$privateKey = MinterWallet::mnemonicToPrivateKey($seed); +``` + * Get public key from private key. ```php diff --git a/src/Minter/Library/Helper.php b/src/Minter/Library/Helper.php index 3bbbb99..66944ad 100644 --- a/src/Minter/Library/Helper.php +++ b/src/Minter/Library/Helper.php @@ -180,4 +180,14 @@ public static function str2hex(string $str): string return array_shift($str); } + + /** + * @param string $str + * @return Buffer + */ + public static function str2buffer(string $str): Buffer + { + $splitted = str_split($str, 1); + return new Buffer($splitted); + } } diff --git a/src/Minter/SDK/MinterCheck.php b/src/Minter/SDK/MinterCheck.php index 02d993c..6f9eee4 100644 --- a/src/Minter/SDK/MinterCheck.php +++ b/src/Minter/SDK/MinterCheck.php @@ -41,6 +41,7 @@ class MinterCheck 'dueBlock', 'coin', 'value', + 'gasCoin', 'lock', 'v', 'r', @@ -101,7 +102,7 @@ public function getOwnerAddress(): string public function sign(string $privateKey): string { // create message hash and passphrase by first 4 fields - $msgHash = $this->serialize(array_slice($this->structure, 0, 5)); + $msgHash = $this->serialize(array_slice($this->structure, 0, 6)); $passphrase = hash('sha256', $this->passphrase); @@ -112,7 +113,7 @@ public function sign(string $privateKey): string $this->structure['lock'] = hex2bin($this->formatLockFromSignature($signature)); // create message hash with lock field - $msgHashWithLock = $this->serialize(array_slice($this->structure, 0, 6)); + $msgHashWithLock = $this->serialize(array_slice($this->structure, 0, 7)); // create signature $signature = ECDSA::sign($msgHashWithLock, $privateKey); @@ -160,13 +161,13 @@ protected function decode(string $check): array $check = Helper::rlpArrayToHexArray($check); // prepare decoded data - $data = []; foreach ($check as $key => $value) { $field = $this->structure[$key]; switch ($field) { case 'nonce': case 'coin': + case 'gasCoin': $data[$field] = Helper::hex2str($value); break; @@ -184,7 +185,7 @@ protected function decode(string $check): array } // set owner address - list($body, $signature) = array_chunk($data, 6, true); + list($body, $signature) = array_chunk($data, 7, true); $this->setOwnerAddress($body, $signature); return $data; @@ -251,6 +252,8 @@ protected function encode(array $check): array 'coin' => MinterConverter::convertCoinName($check['coin']), 'value' => MinterConverter::convertValue($check['value'], 'pip'), + + 'gasCoin' => MinterConverter::convertCoinName($check['gasCoin']) ]; } diff --git a/src/Minter/SDK/MinterCoins/MinterCreateCoinTx.php b/src/Minter/SDK/MinterCoins/MinterCreateCoinTx.php index 2e7a7e4..9a41232 100644 --- a/src/Minter/SDK/MinterCoins/MinterCreateCoinTx.php +++ b/src/Minter/SDK/MinterCoins/MinterCreateCoinTx.php @@ -32,7 +32,8 @@ class MinterCreateCoinTx extends MinterCoinTx implements MinterTxInterface 'symbol' => '', 'initialAmount' => '', 'initialReserve' => '', - 'crr' => '' + 'crr' => '', + 'maxSupply' => '' ]; /** @@ -56,7 +57,10 @@ public function encode(): array 'initialReserve' => MinterConverter::convertValue($this->data['initialReserve'], 'pip'), // Define crr field - 'crr' => $this->data['crr'] === 0 ? '' : $this->data['crr'] + 'crr' => $this->data['crr'] === 0 ? '' : $this->data['crr'], + + // Convert field from BIP to PIP + 'maxSupply' => MinterConverter::convertValue($this->data['maxSupply'], 'pip') ]; } @@ -82,7 +86,10 @@ public function decode(array $txData): array 'initialReserve' => MinterConverter::convertValue(Helper::hexDecode($txData[3]), 'bip'), // Convert crr field from hex string to number - 'crr' => hexdec($txData[4]) + 'crr' => hexdec($txData[4]), + + // Convert field from BIP to PIP + 'maxSupply' => MinterConverter::convertValue(Helper::hexDecode($txData[5]), 'bip') ]; } } \ No newline at end of file diff --git a/src/Minter/SDK/MinterCoins/MinterCreateMultisigTx.php b/src/Minter/SDK/MinterCoins/MinterCreateMultisigTx.php new file mode 100644 index 0000000..1c352f2 --- /dev/null +++ b/src/Minter/SDK/MinterCoins/MinterCreateMultisigTx.php @@ -0,0 +1,90 @@ + '', + 'weights' => [], + 'addresses' => [] + ]; + + /** + * Prepare data for signing + * + * @return array + */ + public function encode(): array + { + $addresses = []; + foreach ($this->data['addresses'] as $address) { + $address = Helper::removeWalletPrefix($address); + $addresses[] = hex2bin($address); + } + + $weights = []; + foreach ($this->data['weights'] as $weight) { + $weights[] = $weight === 0 ? '' : $weight; + } + + $threshold = $this->data['threshold'] === 0 ? '' : $this->data['threshold']; + + return [ + 'threshold' => $threshold, + 'weights' => $weights, + 'addresses' => $addresses, + ]; + } + + /** + * Prepare output tx data + * + * @param array $txData + * @return array + */ + public function decode(array $txData): array + { + list($txThreshold, $txWeights, $txAddresses) = $txData; + + $threshold = (int) Helper::hexDecode($txThreshold); + + $weights = []; + foreach ($txWeights as $weight) { + $weights[] = (int) Helper::hexDecode($weight); + } + + $addresses = []; + foreach ($txAddresses as $address) { + $addresses[] = Helper::addWalletPrefix($address); + } + + return [ + 'threshold' => $threshold, + 'weights' => $weights, + 'addresses' => $addresses + ]; + } +} \ No newline at end of file diff --git a/src/Minter/SDK/MinterTx.php b/src/Minter/SDK/MinterTx.php index e560a56..d175e18 100644 --- a/src/Minter/SDK/MinterTx.php +++ b/src/Minter/SDK/MinterTx.php @@ -9,12 +9,21 @@ use Minter\Library\ECDSA; use Minter\Library\Helper; use Minter\SDK\MinterCoins\{ - MinterCoinTx, MinterDelegateTx, MinterEditCandidateTx, - MinterMultiSendTx, MinterRedeemCheckTx, MinterSellAllCoinTx, - MinterSetCandidateOffTx, MinterSetCandidateOnTx, MinterCreateCoinTx, - MinterDeclareCandidacyTx, MinterSendCoinTx, MinterUnbondTx, - MinterSellCoinTx, MinterBuyCoinTx -}; + MinterCoinTx, + MinterCreateMultisigTx, + MinterDelegateTx, + MinterEditCandidateTx, + MinterMultiSendTx, + MinterRedeemCheckTx, + MinterSellAllCoinTx, + MinterSetCandidateOffTx, + MinterSetCandidateOnTx, + MinterCreateCoinTx, + MinterDeclareCandidacyTx, + MinterSendCoinTx, + MinterUnbondTx, + MinterSellCoinTx, + MinterBuyCoinTx}; /** * Class MinterTx @@ -79,6 +88,15 @@ class MinterTx /** Testnet chain id */ const TESTNET_CHAIN_ID = 2; + /** @var int */ + const DEFAULT_GAS_PRICE = 1; + + /** @var array */ + const DEFAULT_GAS_COINS = [ + self::MAINNET_CHAIN_ID => 'BIP', + self::TESTNET_CHAIN_ID => 'MNT' + ]; + /** * MinterTx constructor. * @param $tx @@ -139,13 +157,10 @@ public function getSenderAddress(array $tx): string */ public function sign(string $privateKey): string { - if(!is_array($this->tx)) { - throw new \Exception('Undefined transaction'); - } - // encode data array to RPL + $this->tx['signatureType'] = self::SIGNATURE_SINGLE_TYPE; $tx = $this->txDataRlpEncode($this->tx); - $tx['payload'] = new Buffer(str_split($tx['payload'], 1)); + $tx['payload'] = Helper::str2buffer($tx['payload']); // create keccak hash from transaction $keccak = Helper::createKeccakHash( @@ -164,6 +179,41 @@ public function sign(string $privateKey): string return MinterPrefix::TRANSACTION . $this->txSigned; } + /** + * Sign with multi-signature + * + * @param string $multisigAddress + * @param array $privateKeys + * @return string + * @throws Exception + */ + public function signMultisig(string $multisigAddress, array $privateKeys): string + { + // encode data array to RPL + $this->tx['signatureType'] = self::SIGNATURE_MULTI_TYPE; + $tx = $this->txDataRlpEncode($this->tx); + $tx['payload'] = Helper::str2buffer($tx['payload']); + + // create keccak hash from transaction + $keccak = Helper::createKeccakHash( + $this->rlp->encode($tx)->toString('hex') + ); + + $signatures = []; + foreach ($privateKeys as $privateKey) { + $signature = ECDSA::sign($keccak, $privateKey); + $signatures[] = Helper::hex2buffer($signature); + } + + $multisigAddress = hex2bin(Helper::removeWalletPrefix($multisigAddress)); + $tx['signatureData'] = $this->rlp->encode([$multisigAddress, $signatures]); + + // pack transaction to hex string + $this->txSigned = $this->rlp->encode($tx)->toString('hex'); + + return MinterPrefix::TRANSACTION . $this->txSigned; + } + /** * Recover public key * @@ -194,6 +244,7 @@ public function recoverPublicKey(array $tx): string * Get hash of transaction * * @return string + * @throws Exception */ public function getHash(): string { @@ -246,29 +297,38 @@ protected function decode(string $tx): array { // pack RLP to hex string $tx = $this->rlpToHex($tx); + $tx = array_combine($this->structure, $tx); // pack data of transaction to hex string - $tx[5] = $this->rlpToHex($tx[5]); - $tx[9] = $this->rlpToHex($tx[9]); + $tx['data'] = $this->rlpToHex($tx['data']); + $tx['signatureData'] = $this->rlpToHex($tx['signatureData']); // encode transaction data - return $this->encode($this->prepareResult($tx), true); + $decodedTx = $this->prepareResult($tx); + return $this->encode($decodedTx, true); } /** * Encode transaction data * * @param array $tx - * @param bool $isHexFormat + * @param bool $isHexFormat * @return array * @throws InvalidArgumentException + * @throws Exception */ protected function encode(array $tx, bool $isHexFormat = false): array { - // validate transaction structure - $this->validateTx($tx); + // fill with default values if not present + $tx['payload'] = $tx['payload'] ?? ''; + $tx['serviceData'] = $tx['serviceData'] ?? ''; + $tx['gasPrice'] = $tx['gasPrice'] ?? self::DEFAULT_GAS_PRICE; + $tx['gasCoin'] = $tx['gasCoin'] ?? self::DEFAULT_GAS_COINS[$tx['chainId']]; + // make right order in transaction params - $tx = array_replace(array_intersect_key(array_flip($this->structure), $tx), $tx); + $txFields = array_flip($this->structure); + $txFields = array_intersect_key($txFields, $tx); + $tx = array_replace($txFields, $tx); switch ($tx['type']) { case MinterSendCoinTx::TYPE: @@ -315,6 +375,10 @@ protected function encode(array $tx, bool $isHexFormat = false): array $this->txDataObject = new MinterSetCandidateOffTx($tx['data'], $isHexFormat); break; + case MinterCreateMultisigTx::TYPE: + $this->txDataObject = new MinterCreateMultisigTx($tx['data'], $isHexFormat); + break; + case MinterMultiSendTx::TYPE: $this->txDataObject = new MinterMultiSendTx($tx['data'], $isHexFormat); break; @@ -342,44 +406,39 @@ protected function encode(array $tx, bool $isHexFormat = false): array */ protected function prepareResult(array $tx): array { - $result = []; - foreach($this->structure as $key => $field) { - switch ($field) { - case 'data': - $result[$field] = $tx[$key]; - break; - - case 'payload': - $result[$field] = Helper::hex2str($tx[$key]); - break; - - case 'serviceData': - $result[$field] = Helper::hex2str($tx[$key]); - break; - - case 'gasCoin': - $result[$field] = MinterConverter::convertCoinName( - Helper::hex2str($tx[$key]) - ); - break; - - case 'signatureData': - $result[$field] = [ - 'v' => hexdec($tx[$key][0]), - 'r' => $tx[$key][1], - 's' => $tx[$key][2] - ]; - break; - - default: - $result[$field] = hexdec($tx[$key]); - break; - } + $tx = [ + 'nonce' => hexdec($tx['nonce']), + 'chainId' => hexdec($tx['chainId']), + 'gasPrice' => hexdec($tx['gasPrice']), + 'gasCoin' => MinterConverter::convertCoinName(Helper::hex2str($tx['gasCoin'])), + 'type' => hexdec($tx['type']), + 'data' => $tx['data'], + 'payload' => Helper::hex2str($tx['payload']), + 'serviceData' => Helper::hex2str($tx['serviceData']), + 'signatureType' => hexdec($tx['signatureType']), + 'signatureData' => $tx['signatureData'] + ]; + + if($tx['signatureType'] === self::SIGNATURE_SINGLE_TYPE) { + list($v, $r, $s) = $tx['signatureData']; + $tx['signatureData'] = ['v' => hexdec($v), 'r' => $r, 's' => $s]; + $tx['from'] = $this->getSenderAddress($tx); } - $result['from'] = $this->getSenderAddress($result); + if($tx['signatureType'] === self::SIGNATURE_MULTI_TYPE) { + list($multisigAddress, $signatures) = $tx['signatureData']; + $tx['signatureData'] = [$multisigAddress]; - return $result; + $signatures = array_map(function($signature) { + list($v, $r, $s) = $signature; + return ['v' => hexdec($v), 'r' => $r, 's' => $s]; + }, $signatures); + + $tx['signatureData'][] = $signatures; + $tx['from'] = Helper::addWalletPrefix($multisigAddress); + } + + return $tx; } /** diff --git a/src/Minter/SDK/MinterWallet.php b/src/Minter/SDK/MinterWallet.php index 9f9fcf1..f68b2c8 100644 --- a/src/Minter/SDK/MinterWallet.php +++ b/src/Minter/SDK/MinterWallet.php @@ -110,6 +110,18 @@ public static function seedToPrivateKey(string $seed): string return BIP44::fromMasterSeed($seed)->derive(self::BIP44_SEED_ADDRESS_PATH)->privateKey; } + /** + * Get private key from mnemonic. + * + * @param string $mnemonic + * @return string + */ + public static function mnemonicToPrivateKey(string $mnemonic): string + { + $seed = self::mnemonicToSeed($mnemonic); + return self::seedToPrivateKey($seed); + } + /** * Validate that address is valid Minter address * diff --git a/tests/MinterCheckTest.php b/tests/MinterCheckTest.php index a2de894..11a1bb2 100644 --- a/tests/MinterCheckTest.php +++ b/tests/MinterCheckTest.php @@ -28,7 +28,7 @@ final class MinterCheckTest extends TestCase /** * Predefined valid check string */ - CONST VALID_CHECK = 'Mcf8a38334383002830f423f8a4d4e5400000000000000888ac7230489e80000b841d184caa333fe636288fc68d99dea2c8af5f7db4569a0bb91e03214e7e238f89d2b21f4d2b730ef590fd8de72bd43eb5c6265664df5aa3610ef6c71538d9295ee001ba08bd966fc5a093024a243e62cdc8131969152d21ee9220bc0d95044f54e3dd485a033bc4e03da3ea8a2cd2bd149d16c022ee604298575380db8548b4fd6672a9195'; + CONST VALID_CHECK = 'Mcf8ae8334383002830f423f8a4d4e5400000000000000888ac7230489e800008a4d4e5400000000000000b841497c5f3e6fc182fd1a791522a9ef7576710bdfbc86fdbf165476ef220e89f9ff1380f93f2d9a2f92fdab0edc1e2605cc2c69b707cd404b2cb1522b7aba4defd5001ba083c9945169f0a7bbe596973b32dc887608780580b1d3bc7b188bedb3bd385594a047b2d5345946ed5498f5bee713f86276aac046a5fef820beaee77a9b6f9bc1df'; /** * Predefined valid proof @@ -45,7 +45,8 @@ public function testSignCheck() 'chainId' => MinterTx::TESTNET_CHAIN_ID, 'dueBlock' => 999999, 'coin' => 'MNT', - 'value' => 10 + 'value' => 10, + 'gasCoin' => 'MNT' ], self::PASSPHRASE); $signature = $check->sign(self::PRIVATE_KEY); @@ -79,10 +80,11 @@ public function testDecodeCheck() 'dueBlock' => 999999, 'coin' => 'MNT', 'value' => '10', - 'lock' => 'd184caa333fe636288fc68d99dea2c8af5f7db4569a0bb91e03214e7e238f89d2b21f4d2b730ef590fd8de72bd43eb5c6265664df5aa3610ef6c71538d9295ee00', + 'gasCoin' => 'MNT', + 'lock' => '497c5f3e6fc182fd1a791522a9ef7576710bdfbc86fdbf165476ef220e89f9ff1380f93f2d9a2f92fdab0edc1e2605cc2c69b707cd404b2cb1522b7aba4defd500', 'v' => 27, - 'r' => '8bd966fc5a093024a243e62cdc8131969152d21ee9220bc0d95044f54e3dd485', - 's' => '33bc4e03da3ea8a2cd2bd149d16c022ee604298575380db8548b4fd6672a9195' + 'r' => '83c9945169f0a7bbe596973b32dc887608780580b1d3bc7b188bedb3bd385594', + 's' => '47b2d5345946ed5498f5bee713f86276aac046a5fef820beaee77a9b6f9bc1df' ], $check->getBody()); $this->assertSame('Mxce931863b9c94a526d94acd8090c1c5955a6eb4b', $check->getOwnerAddress()); diff --git a/tests/MinterCreateCoinTxTest.php b/tests/MinterCreateCoinTxTest.php index c200908..be10dbc 100644 --- a/tests/MinterCreateCoinTxTest.php +++ b/tests/MinterCreateCoinTxTest.php @@ -28,14 +28,15 @@ final class MinterCreateCoinTxTest extends TestCase 'symbol' => 'SPRTEST', 'initialAmount' => '100', 'initialReserve' => '10', - 'crr' => 10 + 'crr' => 10, + 'maxSupply' => '1000' ]; /** * Predefined valid signature */ - const VALID_SIGNATURE = '0xf8850102018a4d4e540000000000000005abea8a535550455220544553548a5350525445535400000089056bc75e2d63100000888ac7230489e800000a808001b845f8431ca0a0b58787e19d8ef3cbd887936617af5cf069a25a568f838c3d04daf5ad2f6f8ea07660c13ab5017edb87f5b52be4574c8a33a893bac178adec9c262a1408e4f1fe'; + const VALID_SIGNATURE = '0xf88f0102018a4d4e540000000000000005b5f48a535550455220544553548a5350525445535400000089056bc75e2d63100000888ac7230489e800000a893635c9adc5dea00000808001b845f8431ca0ccfabd9283d27cf7978bca378e0cc7dc69a39ff3bdc56707fa2d552655f9290da0226057221cbaef35696c9315cd29e783d3c66d842d0a3948a922abb42ca0dabe'; /** * Test to decode data for MinterCreateCoinTx diff --git a/tests/MinterCreateMultisigTxTest.php b/tests/MinterCreateMultisigTxTest.php new file mode 100644 index 0000000..40ff25e --- /dev/null +++ b/tests/MinterCreateMultisigTxTest.php @@ -0,0 +1,74 @@ + 7, + 'weights' => [1, 3, 5], + 'addresses' => [ + 'Mxee81347211c72524338f9680072af90744333143', + 'Mxee81347211c72524338f9680072af90744333145', + 'Mxee81347211c72524338f9680072af90744333144' + ] + ]; + + /** + * Predefined valid signature + */ + + const VALID_SIGNATURE = '0xf8a30102018a4d4e54000000000000000cb848f84607c3010305f83f94ee81347211c72524338f9680072af9074433314394ee81347211c72524338f9680072af9074433314594ee81347211c72524338f9680072af90744333144808001b845f8431ca094eb41d39e6782f5539615cc66da7073d4283893f0b3ee2b2f36aee1eaeb7c57a037f90ffdb45eb9b6f4cf301b48e73a6a81df8182e605b656a52057537d264ab4'; + + /** + * Test to decode data for MinterCreateMultisigTx + */ + public function testDecode(): void + { + $tx = new MinterTx(self::VALID_SIGNATURE); + + $this->assertSame($tx->data, self::DATA); + $this->assertSame($tx->from, self::MINTER_ADDRESS); + } + + /** + * Test signing MinterCreateMultisigTx + */ + public function testSign(): void + { + $tx = new MinterTx([ + 'nonce' => 1, + 'chainId' => MinterTx::TESTNET_CHAIN_ID, + 'gasPrice' => 1, + 'gasCoin' => 'MNT', + 'type' => MinterCreateMultisigTx::TYPE, + 'data' => self::DATA, + 'payload' => '', + 'serviceData' => '', + 'signatureType' => MinterTx::SIGNATURE_SINGLE_TYPE + ]); + + $signature = $tx->sign(self::PRIVATE_KEY); + + $this->assertSame($signature, self::VALID_SIGNATURE); + } +} diff --git a/tests/MinterMultisigTxTest.php b/tests/MinterMultisigTxTest.php new file mode 100644 index 0000000..61a2796 --- /dev/null +++ b/tests/MinterMultisigTxTest.php @@ -0,0 +1,69 @@ + 1, + 'chainId' => 2, + 'gasPrice' => 1, + 'gasCoin' => 'MNT', + 'type' => 1, + 'data' => [ + 'coin' => 'MNT', + 'to' => 'Mxd82558ea00eb81d35f2654953598f5d51737d31d', + 'value' => 1 + ], + 'payload' => '', + 'serviceData' => '', + 'signatureType' => 2 + ]; + + /** + * Sender Minter address + */ + const SENDER_ADDRESS = 'Mxdb4f4b6942cb927e8d7e3a1f602d0f1fb43b5bd2'; + + /** + * Private key for transaction + */ + const PRIVATE_KEYS = [ + 'b354c3d1d456d5a1ddd65ca05fd710117701ec69d82dac1858986049a0385af9', + '38b7dfb77426247aed6081f769ed8f62aaec2ee2b38336110ac4f7484478dccb', + '94c0915734f92dd66acfdc48f82b1d0b208efd544fe763386160ec30c968b4af' + ]; + + /** + * Predefined valid transaction + */ + const VALID_TX = '0xf901270102018a4d4e540000000000000001aae98a4d4e540000000000000094d82558ea00eb81d35f2654953598f5d51737d31d880de0b6b3a7640000808002b8e8f8e694db4f4b6942cb927e8d7e3a1f602d0f1fb43b5bd2f8cff8431ca0a116e33d2fea86a213577fc9dae16a7e4cadb375499f378b33cddd1d4113b6c1a021ee1e9eb61bbd24233a0967e1c745ab23001cf8816bb217d01ed4595c6cb2cdf8431ca0f7f9c7a6734ab2db210356161f2d012aa9936ee506d88d8d0cba15ad6c84f8a7a04b71b87cbbe7905942de839211daa984325a15bdeca6eea75e5d0f28f9aaeef8f8431ba0d8c640d7605034eefc8870a6a3d1c22e2f589a9319288342632b1c4e6ce35128a055fe3f93f31044033fe7b07963d547ac50bccaac38a057ce61665374c72fb454'; + + /** + * Test signing. + */ + public function testSign() + { + $tx = new MinterTx(self::TX); + $signature = $tx->signMultisig(self::SENDER_ADDRESS, self::PRIVATE_KEYS); + $this->assertEquals(self::VALID_TX, $signature); + } + + /** + * Test get decode. + */ + public function testDecode() + { + $tx = new MinterTx(self::VALID_TX); + $this->assertEquals(self::TX['data'], $tx->data); + $this->assertEquals(self::SENDER_ADDRESS, $tx->from); + } +}