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);
+ }
+}