diff --git a/readme.md b/readme.md index 6216bd9..c201704 100644 --- a/readme.md +++ b/readme.md @@ -9,9 +9,7 @@ Using this package allows you to call things like admin functions in Pact from y > If your users have to sign a command, something like Kadena.js would still be required on the frontend. This is not a complete replacement. -> ⚠️ This package is under active development and has only been released under versions 0.x.x to allow for testing of the Package. During development, breaking changes might be made in minor version upgrades. -> -> Some features like adding capabilities are still missing. +> ⚠️ This package is under active development and has no stable production release yet ## Installation @@ -26,22 +24,11 @@ composer require kadena-php/client Key Pairs are used to sign your Pact commands. You can generate a new KeyPair using ```php -$keyPair = \Kadena\Crypto\KeyPair::generate(); +$keyPair = KeyFactory::generate(); ``` -### Commands -Commands are requests sent to the Pact API. -They contain a payload, metadata, signers, network id, and a nonce. -When creating a command, only the metadata and payload are required. -The `networkId` is `null` by default, and when no nonce is set, it will default to the current time. - -Signers must be present in a signed command, but using the `getSignedCommand` will take care of that for you. - -First, let's create metadata to construct our command: - -```php -$metadata = Meta::create(); -``` -The `create()` method takes an optional array of options, options with their default values are: +### Metadata +Every command sent to the Kadena API requires a metadata object to be present. This object can be created manually, or be constructed using the `MetadataFactory`. +The factory will set predefined defaults if certain options are no provided. The defaults and options are as follows: ```php creationTime: Carbon::now(), ttl: 7200, @@ -50,49 +37,77 @@ chainId: '0', gasPrice: 1e-8, sender: '' ``` -Then create a payload: +If we want to create an object with the default options but on a different chain, we can do it like this: ```php -$executePayload = new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 1 2)' - ) -); +$factory = new MetadataFactory(); +$metadata = $factory->withOptions([ + 'chainId' => '1', +])->make(); +``` +If no custom options are required, you can just call `$factory->make()` to create your `Metadata` object. +### Signers +Commands have to be signed before sending them to the Kadena API. To support this, a `Signer` (one or many) has to be created. A signer consists of a public key and optionally a list of capabilities. -$continuePayload = new Payload( - payloadType: PayloadType::CONTINUE, - executePayload: new ContinuePayload( - pactId: 'pact-id', - rollback: false, - step: 0 - ) +Let's create a signer with a public key of `example-key` and the `coin.transfer` capability. As a signer can have multiple or no capabilities, all `Capability` objects should be wrapped in a `CapabilityCollection` object: +```php +// Just as an example, keys should be created using the KeyFactory +$publicKey = new PublicKey(new SignaturePublicKey(new HiddenString('example-key'))); + +$transferCapability = new Capability( + name: 'coin.transfer', + arguments: [ + 'address-from', + 'address-to', + 5 + ] ); + +$signer = new Signer( + publicKey: $publicKey, + capabilities: new CapabilityCollection($transferCapability) +) ``` -As you can see there are two payload types, an execute and a continue payload. With these metadata and payload classes, we can construct our commands: +Multiple signers can be wrapped in the `SignerCollection` object. + +### Payloads +Payloads are the code to be executed by pact. There are two types of payloads: and execute and a continue payload. ```php -$executeCommand = new Command( - meta: $metadata, - payload: $executePyaload +$executePayload new ExecutePayload( + code: '(+ 1 2)' ); -$continueCommand = new Command( - meta: $metadata, - payload: $continuePyaload +$continuePayload = new ContinuePayload( + pactId: 'pact-id', + rollback: false, + step: 0 ); ``` -### Signing Commands -After creating a command, you can sign it using any number of key pairs. To do this, first, create a `KeyPairCollection` from the key pairs you have. This can be a single key pair or many. +### Commands +Commands wrap all data sent to the Kadena API, a `Command` object can be created manually, but it is recommended to use the `CommandFactory` for this. +The factory will set certain defaults, and can be used like this: ```php -$kp1 = KeyPair::generate(); -$kp2 = KeyPair::generate(); +$factory = new CommandFactory(); -$keyPairCollection = new KeyPairCollection($kp1, $kp2); +$factory->withExecutePayload($executePayload) + ->withMetadata($metadata) + ->withSigners(new SignerCollection($signer)) + ->withNetworkId('mainnet0') + ->withNonce('nonce-string') + ->make(); +``` +The `withExecutePayload` or the `withContinuePayload` options are always required to create a `Command` object, but all others are optional. + +### Signing Commands +After creating a command, you can sign it using any number of key pairs. To do this, first, create a `KeyPairCollection` from the key pairs you have. +These key pairs should correspond to the signers you added to your account. +```php +$kpc = new KeyPairCollection($keypair); ``` Now using these key pairs, we can sign the previously created command ```php -$signedCommand = $command->getSignedCommand($keyPairCollection); +$signedCommand = CommandSigner::sign($command, $kpc); ``` This returns a new instance of a `SignedCommand` @@ -100,12 +115,12 @@ This returns a new instance of a `SignedCommand` Instead of signing the command in the backend, a command might be signed elsewhere (user wallet). A signed command can be reconstructed from a valid Pact command string using: ```php -$signedCommand = SignedCommand::fromString($commandString) +$signedCommand = SignedCommandMapper::fromString($commandString) ``` A signed command can also be cast to a string or an array using ```php -$signedCommand->toString(); -$signedCommand->toArray(); +$commandString = SignedCommandMapper::toString($signedCommand); +$commandArray = SignedCommandMapper::toArray($signedCommand); ``` ### Using the Client diff --git a/src/Client.php b/src/Client.php index 2447143..6c447c7 100644 --- a/src/Client.php +++ b/src/Client.php @@ -3,11 +3,13 @@ namespace Kadena; use JsonException; -use Kadena\Contracts\Pact as PactContract; -use Kadena\Pact\RequestKey; -use Kadena\Pact\RequestKeyCollection; -use Kadena\Pact\SignedCommand; -use Kadena\Pact\SignedCommandCollection; +use Kadena\Contracts\Client as ClientContract; +use Kadena\DataMappers\SignedCommandCollectionMapper; +use Kadena\DataMappers\SignedCommandMapper; +use Kadena\ValueObjects\Command\SignedCommand; +use Kadena\ValueObjects\Command\SignedCommandCollection; +use Kadena\ValueObjects\RequestKey\RequestKey; +use Kadena\ValueObjects\RequestKey\RequestKeyCollection; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; @@ -17,7 +19,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; -class Client implements PactContract +class Client implements ClientContract { private HttpClientInterface $client; @@ -36,11 +38,12 @@ public function __construct(private readonly string $apiUrl, HttpClientInterface * @throws RedirectionExceptionInterface * @throws DecodingExceptionInterface * @throws ClientExceptionInterface + * @throws JsonException */ public function send(SignedCommandCollection $commands): RequestKeyCollection { $response = $this->client->request('POST', $this->apiUrl . '/api/v1/send', [ - 'json' => $commands->toPayload(), + 'json' => SignedCommandCollectionMapper::toArray($commands), ]); $requestKeys = array_map(static function (string $requestKey) { @@ -57,7 +60,7 @@ public function send(SignedCommandCollection $commands): RequestKeyCollection public function local(SignedCommand $command): ResponseInterface { return $this->client->request('POST', $this->apiUrl . '/api/v1/local', [ - 'json' => $command->toArray(), + 'json' => SignedCommandMapper::toArray($command), ]); } diff --git a/src/Contracts/Pact.php b/src/Contracts/Client.php similarity index 69% rename from src/Contracts/Pact.php rename to src/Contracts/Client.php index 8738a5f..7dc7f98 100644 --- a/src/Contracts/Pact.php +++ b/src/Contracts/Client.php @@ -2,13 +2,13 @@ namespace Kadena\Contracts; -use Kadena\Pact\RequestKey; -use Kadena\Pact\RequestKeyCollection; -use Kadena\Pact\SignedCommand; -use Kadena\Pact\SignedCommandCollection; +use Kadena\ValueObjects\Command\SignedCommand; +use Kadena\ValueObjects\Command\SignedCommandCollection; +use Kadena\ValueObjects\RequestKey\RequestKey; +use Kadena\ValueObjects\RequestKey\RequestKeyCollection; use Symfony\Contracts\HttpClient\ResponseInterface; -interface Pact +interface Client { public function send(SignedCommandCollection $commands): RequestKeyCollection; diff --git a/src/Contracts/Collection.php b/src/Contracts/Collection.php new file mode 100644 index 0000000..df84e76 --- /dev/null +++ b/src/Contracts/Collection.php @@ -0,0 +1,10 @@ +toArray() as $keyPair) { + $signatures[] = MessageSigner::sign($commandString, $keyPair); + } + + return new SignedCommand( + hash: $commandHash, + signatures: new SignatureCollection(...$signatures), + command: $command + ); + } +} diff --git a/src/Crypto/Contracts/KeyPair.php b/src/Crypto/Contracts/KeyPair.php deleted file mode 100644 index 26eb7ad..0000000 --- a/src/Crypto/Contracts/KeyPair.php +++ /dev/null @@ -1,8 +0,0 @@ -getPublicKey()), + secretKey: new SecretKey($keyPair->getSecretKey()) + ); + } +} diff --git a/src/Crypto/KeyPair.php b/src/Crypto/KeyPair.php deleted file mode 100644 index 6c1745b..0000000 --- a/src/Crypto/KeyPair.php +++ /dev/null @@ -1,35 +0,0 @@ -getPublicKey(), - secretKey: $keyPair->getSecretKey() - ); - } -} diff --git a/src/Crypto/KeyPairCollection.php b/src/Crypto/KeyPairCollection.php deleted file mode 100644 index 44bb5a7..0000000 --- a/src/Crypto/KeyPairCollection.php +++ /dev/null @@ -1,24 +0,0 @@ -array = $keyPair; - } - - /** - * @return KeyPair[] - */ - public function toArray(): array - { - return $this->array; - } -} diff --git a/src/Crypto/Signer.php b/src/Crypto/MessageSigner.php similarity index 60% rename from src/Crypto/Signer.php rename to src/Crypto/MessageSigner.php index ba02391..22e7874 100644 --- a/src/Crypto/Signer.php +++ b/src/Crypto/MessageSigner.php @@ -2,10 +2,10 @@ namespace Kadena\Crypto; -use Kadena\Crypto\Contracts\Signer as SignerContract; -use Kadena\Pact\Command; +use Kadena\Contracts\Crypto\MessageSigner as MessageSignerContract; +use Kadena\ValueObjects\Signer\KeyPair; +use Kadena\ValueObjects\Signer\Signature; use ParagonIE\ConstantTime\Base64UrlSafe; -use ParagonIE\ConstantTime\Hex; use ParagonIE\Halite\Alerts\InvalidSignature; use ParagonIE\Halite\Alerts\InvalidType; use ParagonIE\Halite\Asymmetric\Crypto; @@ -13,11 +13,9 @@ use ParagonIE\Halite\Halite; use SodiumException; -final class Signer implements SignerContract +final class MessageSigner implements MessageSignerContract { /** - * Sign a message given a key pair and return a SignedMessage object - * * @throws InvalidType * @throws SodiumException */ @@ -29,25 +27,21 @@ public static function sign(string $message, KeyPair $keyPair): Signature } /** - * Sign a hash given a key pair and return a SignedMessage object - * * @throws InvalidType * @throws SodiumException */ public static function signHash(string $hash, KeyPair $keyPair): Signature { - $signature = Crypto::sign($hash, $keyPair->secretKey, Halite::ENCODE_HEX); + $signature = Crypto::sign($hash, $keyPair->secretKey->key, Halite::ENCODE_HEX); return new Signature( hash: Base64UrlSafe::encodeUnpadded($hash), signature: $signature, - publicKey: Hex::encode($keyPair->publicKey->getRawKeyMaterial()) + publicKey: $keyPair->publicKey->toString() ); } /** - * Verify a signature given a public key and message - * * @throws InvalidType * @throws InvalidSignature * @throws SodiumException @@ -56,15 +50,4 @@ public static function verifySignature(string $message, string $signature, Signa { return Crypto::verify($message, $publicKey, $signature, Halite::ENCODE_HEX); } - - /** - * @throws InvalidType - * @throws SodiumException - */ - public static function signCommand(Command $command, KeyPair $keyPair): Signature - { - $message = $command->toString(); - - return self::sign($message, $keyPair); - } } diff --git a/src/Crypto/SignatureCollection.php b/src/Crypto/SignatureCollection.php deleted file mode 100644 index 46a5446..0000000 --- a/src/Crypto/SignatureCollection.php +++ /dev/null @@ -1,41 +0,0 @@ -array = $signature; - } - - public function first(): Signature - { - return $this->array[0]; - } - - /** - * @return Signature[] - */ - public function toArray(): array - { - return $this->array; - } - - public static function fromArray(array $signatures, string $hash): self - { - return new self(...array_map(static function (array|object $signature) use ($hash) { - $signature = (array) $signature; - - return new Signature( - hash: $hash, - signature: $signature['sig'] - ); - }, $signatures)); - } -} diff --git a/src/DataMappers/CommandMapper.php b/src/DataMappers/CommandMapper.php new file mode 100644 index 0000000..7979a08 --- /dev/null +++ b/src/DataMappers/CommandMapper.php @@ -0,0 +1,199 @@ +payload->payloadType === PayloadType::EXECUTE) { + $payload = [ + 'data' => $command->payload->executePayload->data, + 'code' => $command->payload->executePayload->code, + ]; + } else { + $payload = [ + 'data' => $command->payload->continuePayload->data, + 'step' => $command->payload->continuePayload->step, + 'rollback' => $command->payload->continuePayload->rollback, + 'pactId' => $command->payload->continuePayload->pactId, + 'proof' => $command->payload->continuePayload->proof, + ]; + } + + $array = [ + 'networkId' => $command->networkId, + 'payload' => [ + $command->payload->payloadType->value => $payload + ], + 'meta' => [ + 'creationTime' => $command->meta->creationTime->getTimestamp(), + 'ttl' => $command->meta->ttl, + 'gasLimit' => $command->meta->gasLimit, + 'chainId' => $command->meta->chainId, + 'gasPrice' => $command->meta->gasPrice, + 'sender' => $command->meta->sender, + ], + 'nonce' => $command->nonce + ]; + + if ($command->signers->count() === 0) { + return $array; + } + + $signers = []; + + /** @var Signer $signer */ + foreach ($command->signers->toArray() as $signer) { + $signerArray = [ + 'pubKey' => $signer->publicKey->toString(), + ]; + + if ($signer->capabilities->count() !== 0) { + $capabilityList = []; + + /** @var Capability $capability */ + foreach ($signer->capabilities->toArray() as $capability) { + $capabilityList[] = [ + 'name' => $capability->name, + 'args' => $capability->arguments, + ]; + } + + $signerArray['clist'] = $capabilityList; + } + + $signers[] = $signerArray; + } + + $array['signers'] = $signers; + + return $array; + } + + /** + * @throws JsonException + */ + public static function toString(Command $command): string + { + return json_encode(self::toArray($command), JSON_THROW_ON_ERROR); + } + + /** + * @throws JsonException + * @throws InvalidKey + */ + public static function fromString(string $commandJson): Command + { + $command = json_decode($commandJson, false, 512, JSON_THROW_ON_ERROR); + + if (! isset($command->signers, $command->payload, $command->meta)) { + throw new InvalidArgumentException('Invalid Command JSON string given'); + } + + $meta = $command->meta; + + if (! isset( + $meta->creationTime, + $meta->ttl, + $meta->gasLimit, + $meta->chainId, + $meta->gasPrice, + $meta->sender, + )) { + throw new InvalidArgumentException('Invalid meta object given'); + } + + $payloadType = PayloadType::from(key((array) $command->payload)); + + if ($payloadType === PayloadType::EXECUTE) { + $exec = $command->payload->exec; + + if (! isset($exec->data, $exec->code)) { + throw new InvalidArgumentException('Invalid execute command object given'); + } + + $payload = new Payload( + payloadType: $payloadType, + executePayload: new ExecutePayload( + code: $exec->code, + data: (array) ($exec->data ?? []), + ) + ); + } elseif ($payloadType === PayloadType::CONTINUE) { + $cont = $command->payload->cont; + + if (! isset($cont->data, $cont->code, $cont->pactId, $cont->rollback, $cont->step)) { + throw new InvalidArgumentException('Invalid continue command object given'); + } + + $payload = new Payload( + payloadType: $payloadType, + continuePayload: new ContinuePayload( + pactId: $cont->pactId, + rollback: $cont->rollback, + step: $cont->step, + proof: $cont->proof ?? null, + data: (array) ($cont->data ?? []), + ) + ); + } else { + throw new InvalidArgumentException('Unknown payload type'); + } + + $signers = []; + + foreach ($command->signers as $signer) { + $capabilities = []; + + if (isset($signer->clist)) { + foreach ($signer->clist as $capabilityList) { + $capabilities[] = new Capability( + name: $capabilityList->name, + arguments: $capabilityList->args + ); + } + } + + $signers[] = new Signer( + publicKey: new PublicKey(new SignaturePublicKey(new HiddenString(Hex::decode($signer->pubKey)))), + capabilities: new CapabilityCollection(...$capabilities) + ); + } + + return new Command( + meta: new Metadata( + creationTime: Carbon::createFromTimestamp($meta->creationTime), + ttl: $meta->ttl, + gasLimit: $meta->gasLimit, + chainId: $meta->chainId, + gasPrice: $meta->gasPrice, + sender: $meta->sender + ), + payload: $payload, + networkId: $command->networkId ?? '0', + nonce: $command->nonce ?? Carbon::now()->toISOString(), + signers: new SignerCollection(...$signers) + ); + } +} diff --git a/src/DataMappers/SignedCommandCollectionMapper.php b/src/DataMappers/SignedCommandCollectionMapper.php new file mode 100644 index 0000000..e768588 --- /dev/null +++ b/src/DataMappers/SignedCommandCollectionMapper.php @@ -0,0 +1,31 @@ + array_map(static function (SignedCommand $command) { + return SignedCommandMapper::toArray($command); + }, $commands->toArray()) + ]; + } + + /** + * @throws JsonException + */ + public static function toString(SignedCommandCollection $commands): string + { + return json_encode(self::toArray($commands), JSON_THROW_ON_ERROR); + } +} diff --git a/src/DataMappers/SignedCommandMapper.php b/src/DataMappers/SignedCommandMapper.php new file mode 100644 index 0000000..50117ae --- /dev/null +++ b/src/DataMappers/SignedCommandMapper.php @@ -0,0 +1,65 @@ + CommandMapper::toString($command->command), + 'hash' => $command->hash, + 'sigs' => array_map(static function (Signature $signature) { + return ['sig' => $signature->signature]; + }, $command->signatures->toArray()) + ]; + } + + /** + * @throws JsonException + */ + public static function toString(SignedCommand $command): string + { + return json_encode(self::toArray($command), JSON_THROW_ON_ERROR); + } + + /** + * @throws JsonException + */ + public static function fromString(string $commandJson): SignedCommand + { + $command = json_decode($commandJson, false, 512, JSON_THROW_ON_ERROR); + + if (! isset($command->hash, $command->sigs, $command->cmd)) { + throw new InvalidArgumentException('Invalid Signed Command JSON string given'); + } + + return new SignedCommand( + hash: $command->hash, + signatures: self::getSignatures((array) $command->sigs, $command->hash), + command: CommandMapper::fromString($command->cmd), + ); + } + + private static function getSignatures(array $signatures, string $hash): SignatureCollection + { + return new SignatureCollection(...array_map(static function (array|object $signature) use ($hash) { + $signature = (array) $signature; + + return new Signature( + hash: $hash, + signature: $signature['sig'] + ); + }, $signatures)); + } +} diff --git a/src/Exceptions/MissingPayloadException.php b/src/Exceptions/MissingPayloadException.php new file mode 100644 index 0000000..cf1e859 --- /dev/null +++ b/src/Exceptions/MissingPayloadException.php @@ -0,0 +1,9 @@ +nonce = Carbon::now()->toISOString(); - } else { - $this->nonce = $nonce; - } - } - - /** - * @throws JsonException - */ - public function toString(): string - { - return json_encode($this->toArray(), JSON_THROW_ON_ERROR); - } - - public function toArray(): array - { - return [ - 'signers' => $this->signers, - 'networkId' => $this->networkId, - 'payload' => $this->payload->toArray(), - 'meta' => $this->meta->toArray(), - 'nonce' => $this->nonce, - ]; - } - - /** - * @throws InvalidType - * @throws SodiumException - */ - public function getSignedCommand(KeyPairCollection $keyPairCollection): SignedCommand - { - $this->setSigners(array_map([$this, 'getSigners'], $keyPairCollection->toArray())); - - $signatures = []; - - foreach ($keyPairCollection->toArray() as $keyPair) { - $signatures[] = Signer::signCommand($this, $keyPair); - } - - $signatureCollection = new SignatureCollection(...$signatures); - - return new SignedCommand( - hash: $signatureCollection->first()->hash, - signatures: $signatureCollection, - command: $this - ); - } - - private function getSigners(KeyPair $keyPair): string - { - return Hex::encode($keyPair->publicKey->getRawKeyMaterial()); - } - - public function setSigners(array $signers): void - { - $pubKeys = []; - - foreach ($signers as $signer) { - $pubKeys[] = ['pubKey' => $signer]; - } - - $this->signers = $pubKeys; - } - - public static function fromString(string $commandJson): self - { - $command = json_decode($commandJson, false, 512, JSON_THROW_ON_ERROR); - - if (! isset($command->signers, $command->payload, $command->meta)) { - throw new InvalidArgumentException('Invalid Command JSON string given'); - } - - $meta = $command->meta; - - if (! isset( - $meta->creationTime, - $meta->ttl, - $meta->gasLimit, - $meta->chainId, - $meta->gasPrice, - $meta->sender, - )) { - throw new InvalidArgumentException('Invalid meta object given'); - } - - $payloadType = PayloadType::from(key((array) $command->payload)); - - if ($payloadType === PayloadType::EXECUTE) { - $exec = $command->payload->exec; - - if (! isset($exec->data, $exec->code)) { - throw new InvalidArgumentException('Invalid execute command object given'); - } - - $payload = new Payload( - payloadType: $payloadType, - executePayload: new ExecutePayload( - code: $exec->code, - data: (array) ($exec->data ?? []), - ) - ); - } elseif ($payloadType === PayloadType::CONTINUE) { - $cont = $command->payload->cont; - - if (! isset($cont->data, $cont->code, $cont->pactId, $cont->rollback, $cont->step)) { - throw new InvalidArgumentException('Invalid continue command object given'); - } - - $payload = new Payload( - payloadType: $payloadType, - continuePayload: new ContinuePayload( - pactId: $cont->pactId, - rollback: $cont->rollback, - step: $cont->step, - proof: $cont->proof ?? null, - data: (array) ($cont->data ?? []), - ) - ); - } else { - throw new InvalidArgumentException('Unknown payload type'); - } - - $commandObject = new self( - meta: new Meta( - creationTime: Carbon::createFromTimestamp($meta->creationTime), - ttl: $meta->ttl, - gasLimit: $meta->gasLimit, - chainId: $meta->chainId, - gasPrice: $meta->gasPrice, - sender: $meta->sender - ), - payload: $payload, - networkId: $command->networkId ?? null, - nonce: $command->nonce ?? null - ); - - $commandObject->setSigners(array_map(static function (array|object $signer) { - $signer = (array) $signer; - - return $signer['pubKey']; - }, $command->signers)); - - return $commandObject; - } -} diff --git a/src/Pact/CommandFactory.php b/src/Pact/CommandFactory.php new file mode 100644 index 0000000..8839ec6 --- /dev/null +++ b/src/Pact/CommandFactory.php @@ -0,0 +1,136 @@ +payload->payloadType === PayloadType::EXECUTE) { + $self->withExecutePayload($command->payload->executePayload); + } + + if ($command->payload->payloadType === PayloadType::CONTINUE) { + $self->withContinuePayload($command->payload->continuePayload); + } + + return $self->withSigners($command->signers) + ->withNetworkId($command->networkId) + ->withNonce($command->nonce) + ->withMetadata($command->meta); + } + + public function addSigner(Signer $signer): self + { + if (! isset($this->signers)) { + $this->signers = new SignerCollection(); + } + + $signers = $this->signers->toArray(); + $signers[] = $signer; + + $this->signers = new SignerCollection(...$signers); + } + + public function withMetadata(Metadata $metadata): self + { + $this->metadata = $metadata; + + return $this; + } + + public function withExecutePayload(ExecutePayload $payload): self + { + $this->payload = new Payload( + payloadType: PayloadType::EXECUTE, + executePayload: $payload + ); + + return $this; + } + + public function withContinuePayload(ContinuePayload $payload): self + { + $this->payload = new Payload( + payloadType: PayloadType::CONTINUE, + continuePayload: $payload + ); + + return $this; + } + + public function withNonce(string $nonce): self + { + $this->nonce = $nonce; + + return $this; + } + + public function withNetworkId(string $networkId): self + { + $this->networkId = $networkId; + + return $this; + } + + public function withSigners(SignerCollection $signers): self + { + $this->signers = $signers; + + return $this; + } + + /** + * @throws MissingPayloadException + */ + public function make(): Command + { + if (! isset($this->payload)) { + throw new MissingPayloadException('Payload is required to build command'); + } + + if (! isset($this->metadata)) { + $this->metadata = (new MetadataFactory())->make(); + } + + if (! isset($this->nonce)) { + $this->nonce = Carbon::now()->toISOString(); + } + + if (! isset($this->networkId)) { + $this->networkId = '0'; + } + + if (! isset($this->signers)) { + $this->signers = new SignerCollection(); + } + + return new Command( + meta: $this->metadata, + payload: $this->payload, + networkId: $this->networkId, + nonce: $this->nonce, + signers: $this->signers + ); + } +} diff --git a/src/Pact/Meta.php b/src/Pact/Meta.php deleted file mode 100644 index 1a9eab3..0000000 --- a/src/Pact/Meta.php +++ /dev/null @@ -1,44 +0,0 @@ - $this->creationTime->getTimestamp(), - 'ttl' => $this->ttl, - 'gasLimit' => $this->gasLimit, - 'chainId' => $this->chainId, - 'gasPrice' => $this->gasPrice, - 'sender' => $this->sender, - ]; - } - - public static function create(?array $options = []): self - { - return new self( - creationTime: (isset($options['creationTime'])) ? Carbon::createFromTimestamp((int) $options['creationTime']) : Carbon::now(), - ttl: (int) ($options['ttl'] ?? 7200), - gasLimit: (int) ($options['gasLimit'] ?? 10000), - chainId: (string) ($options['chainId'] ?? '0'), - gasPrice: (float) ($options['gasPrice'] ?? self::MIN_GAS_PRICE), - sender: (string) ($options['sender'] ?? '') - ); - } -} diff --git a/src/Pact/MetadataFactory.php b/src/Pact/MetadataFactory.php new file mode 100644 index 0000000..f54a9b2 --- /dev/null +++ b/src/Pact/MetadataFactory.php @@ -0,0 +1,48 @@ + $option) { + if (in_array($key, array_keys(get_class_vars(self::class)), true)) { + if ($key === 'creationTime' && ! is_a($option, Carbon::class)) { + $this->$key = Carbon::createFromTimestamp((int) $option); + } else { + $this->$key = $option; + } + } + } + + return $this; + } + + public function make(): Metadata + { + if (! isset($this->creationTime)) { + $this->creationTime = Carbon::now(); + } + + return new Metadata( + creationTime: $this->creationTime, + ttl: $this->ttl, + gasLimit: $this->gasLimit, + chainId: $this->chainId, + gasPrice: $this->gasPrice, + sender: $this->sender + ); + } +} diff --git a/src/Pact/RequestKeyCollection.php b/src/Pact/RequestKeyCollection.php deleted file mode 100644 index 82bd60b..0000000 --- a/src/Pact/RequestKeyCollection.php +++ /dev/null @@ -1,36 +0,0 @@ -array = $requestKey; - } - - /** - * @return RequestKey[] - */ - public function toArray(): array - { - return $this->array; - } - - public function toPlainArray(): array - { - return array_map(static function (RequestKey $requestKey) { - return $requestKey->key; - }, $this->array); - } - - public function first(): RequestKey - { - return $this->array[0]; - } -} diff --git a/src/Pact/SignedCommand.php b/src/Pact/SignedCommand.php deleted file mode 100644 index a286da0..0000000 --- a/src/Pact/SignedCommand.php +++ /dev/null @@ -1,71 +0,0 @@ -validateSignatures(); - } - - private function validateSignatures(): void - { - foreach ($this->signatures->toArray() as $signature) { - if ($signature->hash !== $this->hash) { - throw new InvalidArgumentException("Signatures for different hashes found: {$signature->hash}, expected: {$this->hash}"); - } - } - } - - /** - * @throws JsonException - */ - public function toArray(): array - { - $signatures = []; - - foreach ($this->signatures->toArray() as $signature) { - $signatures[] = ['sig' => $signature->signature]; - } - - return [ - 'hash' => $this->hash, - 'sigs' => $signatures, - 'cmd' => $this->command->toString(), - ]; - } - - /** - * @throws JsonException - */ - public function toString(): string - { - return json_encode($this->toArray(), JSON_THROW_ON_ERROR); - } - - /** - * @throws JsonException - */ - public static function fromString(string $commandJson): self - { - $command = json_decode($commandJson, false, 512, JSON_THROW_ON_ERROR); - - if (! isset($command->hash, $command->sigs, $command->cmd)) { - throw new InvalidArgumentException('Invalid Signed Command JSON string given'); - } - - return new self( - hash: $command->hash, - signatures: SignatureCollection::fromArray((array) $command->sigs, $command->hash), - command: Command::fromString($command->cmd), - ); - } -} diff --git a/src/Pact/SignedCommandCollection.php b/src/Pact/SignedCommandCollection.php deleted file mode 100644 index ceaf50c..0000000 --- a/src/Pact/SignedCommandCollection.php +++ /dev/null @@ -1,51 +0,0 @@ -array = $signedCommand; - } - - /** - * @return SignedCommand[] - */ - public function toArray(): array - { - return $this->array; - } - - public function toPayload(): array - { - $commands = array_map([$this, 'mapToArray'], $this->array); - - return [ - 'cmds' => $commands - ]; - } - - /** - * @throws JsonException - */ - public function toPayloadString(): string - { - return json_encode($this->toPayload(), JSON_THROW_ON_ERROR); - } - - /** - * @throws JsonException - */ - private function mapToArray(SignedCommand $signedCommand): array - { - return $signedCommand->toArray(); - } -} diff --git a/src/ValueObjects/Command/Command.php b/src/ValueObjects/Command/Command.php new file mode 100644 index 0000000..8adb3c3 --- /dev/null +++ b/src/ValueObjects/Command/Command.php @@ -0,0 +1,18 @@ + $this->proof, - 'pactId' => $this->pactId, - 'rollback' => $this->rollback, - 'step' => $this->step, - 'data' => $this->data, - ]; - } } diff --git a/src/Pact/ExecutePayload.php b/src/ValueObjects/Command/Payload/ExecutePayload.php similarity index 52% rename from src/Pact/ExecutePayload.php rename to src/ValueObjects/Command/Payload/ExecutePayload.php index 2ea1e8d..04cacaa 100644 --- a/src/Pact/ExecutePayload.php +++ b/src/ValueObjects/Command/Payload/ExecutePayload.php @@ -1,6 +1,6 @@ $this->data, - 'code' => $this->code, - ]; - } } diff --git a/src/Pact/Payload.php b/src/ValueObjects/Command/Payload/Payload.php similarity index 79% rename from src/Pact/Payload.php rename to src/ValueObjects/Command/Payload/Payload.php index 5f94b58..4116295 100644 --- a/src/Pact/Payload.php +++ b/src/ValueObjects/Command/Payload/Payload.php @@ -1,6 +1,6 @@ payloadType->value => - $this->executePayload?->toArray() - ?? $this->continuePayload->toArray(), - ]; - } } diff --git a/src/Pact/PayloadType.php b/src/ValueObjects/Command/Payload/PayloadType.php similarity index 71% rename from src/Pact/PayloadType.php rename to src/ValueObjects/Command/Payload/PayloadType.php index 398bfd2..5181286 100644 --- a/src/Pact/PayloadType.php +++ b/src/ValueObjects/Command/Payload/PayloadType.php @@ -1,6 +1,6 @@ validateSignatures(); + } + + private function validateSignatures(): void + { + foreach ($this->signatures->toArray() as $signature) { + if ($signature->hash !== $this->hash) { + throw new InvalidArgumentException("Signatures for different hashes found: {$signature->hash}, expected: {$this->hash}"); + } + } + } +} diff --git a/src/ValueObjects/Command/SignedCommandCollection.php b/src/ValueObjects/Command/SignedCommandCollection.php new file mode 100644 index 0000000..72535de --- /dev/null +++ b/src/ValueObjects/Command/SignedCommandCollection.php @@ -0,0 +1,16 @@ +array = $signedCommand; + } +} diff --git a/src/ValueObjects/HasCollectionMethods.php b/src/ValueObjects/HasCollectionMethods.php new file mode 100644 index 0000000..a5b351e --- /dev/null +++ b/src/ValueObjects/HasCollectionMethods.php @@ -0,0 +1,28 @@ +array; + } + + public function first(): mixed + { + return $this->array[0]; + } + + public function get(int $offset): mixed + { + return $this->array[$offset]; + } + + public function count(): int + { + return sizeof($this->array); + } +} diff --git a/src/Pact/RequestKey.php b/src/ValueObjects/RequestKey/RequestKey.php similarity index 77% rename from src/Pact/RequestKey.php rename to src/ValueObjects/RequestKey/RequestKey.php index 057d231..fd9c79c 100644 --- a/src/Pact/RequestKey.php +++ b/src/ValueObjects/RequestKey/RequestKey.php @@ -1,6 +1,6 @@ array = $requestKey; + } + + public function toPlainArray(): array + { + return array_map(static function (RequestKey $requestKey) { + return $requestKey->key; + }, $this->toArray()); + } +} diff --git a/src/ValueObjects/Signer/Capability.php b/src/ValueObjects/Signer/Capability.php new file mode 100644 index 0000000..cb0e045 --- /dev/null +++ b/src/ValueObjects/Signer/Capability.php @@ -0,0 +1,12 @@ +array = $capabilityDescription; + } +} diff --git a/src/ValueObjects/Signer/KeyPair.php b/src/ValueObjects/Signer/KeyPair.php new file mode 100644 index 0000000..fced80f --- /dev/null +++ b/src/ValueObjects/Signer/KeyPair.php @@ -0,0 +1,12 @@ +array = $keyPair; + } +} diff --git a/src/ValueObjects/Signer/PublicKey.php b/src/ValueObjects/Signer/PublicKey.php new file mode 100644 index 0000000..0948a4a --- /dev/null +++ b/src/ValueObjects/Signer/PublicKey.php @@ -0,0 +1,24 @@ +key->getRawKeyMaterial()); + } + + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/ValueObjects/Signer/SecretKey.php b/src/ValueObjects/Signer/SecretKey.php new file mode 100644 index 0000000..b94b10d --- /dev/null +++ b/src/ValueObjects/Signer/SecretKey.php @@ -0,0 +1,13 @@ +array = $signature; + } +} diff --git a/src/ValueObjects/Signer/Signer.php b/src/ValueObjects/Signer/Signer.php new file mode 100644 index 0000000..bf009bb --- /dev/null +++ b/src/ValueObjects/Signer/Signer.php @@ -0,0 +1,12 @@ +array = $signer; + } +} diff --git a/tests/Unit/ClientTest.php b/tests/Unit/ClientTest.php index 63f1cdd..5fb4e9a 100644 --- a/tests/Unit/ClientTest.php +++ b/tests/Unit/ClientTest.php @@ -4,17 +4,24 @@ use Carbon\Carbon; use Kadena\Client; -use Kadena\Crypto\Signature; -use Kadena\Crypto\SignatureCollection; -use Kadena\Pact\Command; -use Kadena\Pact\ExecutePayload; -use Kadena\Pact\Meta; -use Kadena\Pact\Payload; -use Kadena\Pact\PayloadType; -use Kadena\Pact\RequestKey; -use Kadena\Pact\RequestKeyCollection; -use Kadena\Pact\SignedCommand; -use Kadena\Pact\SignedCommandCollection; +use Kadena\Crypto\KeyFactory; +use Kadena\DataMappers\SignedCommandCollectionMapper; +use Kadena\DataMappers\SignedCommandMapper; +use Kadena\ValueObjects\Command\Command; +use Kadena\ValueObjects\Command\Metadata; +use Kadena\ValueObjects\Command\Payload\ExecutePayload; +use Kadena\ValueObjects\Command\Payload\Payload; +use Kadena\ValueObjects\Command\Payload\PayloadType; +use Kadena\ValueObjects\Command\SignedCommand; +use Kadena\ValueObjects\Command\SignedCommandCollection; +use Kadena\ValueObjects\RequestKey\RequestKey; +use Kadena\ValueObjects\RequestKey\RequestKeyCollection; +use Kadena\ValueObjects\Signer\Capability; +use Kadena\ValueObjects\Signer\CapabilityCollection; +use Kadena\ValueObjects\Signer\Signature; +use Kadena\ValueObjects\Signer\SignatureCollection; +use Kadena\ValueObjects\Signer\Signer; +use Kadena\ValueObjects\Signer\SignerCollection; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; @@ -27,8 +34,10 @@ public function setUp(): void { parent::setUp(); + $keyPair = KeyFactory::generate(); + $command = new Command( - meta: new Meta( + meta: new Metadata( creationTime: Carbon::createFromTimestamp(0), ttl: 0, gasLimit: 0, @@ -41,26 +50,34 @@ public function setUp(): void executePayload: new ExecutePayload( code: '(+ 2 2)' ) - ) + ), + networkId: 'testnet0', + nonce: 'test', + signers: new SignerCollection(new Signer( + publicKey: $keyPair->publicKey, + capabilities: new CapabilityCollection( + new Capability( + name: 'cap.example', + arguments: [] + ) + ) + )) ); - $command->setSigners(['public-key']); - - $signature = new Signature( - hash: 'hash', - signature: 'signature', - publicKey: 'public-key', + $this->signedCommand = new SignedCommand( + hash: 'test-hash', + signatures: new SignatureCollection(new Signature( + hash: 'test-hash', + signature: 'test-signature' + )), + command: $command ); - - $signatures = new SignatureCollection($signature); - - $this->signedCommand = new SignedCommand('hash', $signatures, $command); } /** @test */ public function it_should_send_a_signed_command_to_the_local_endpoint_and_return_a_response_object(): void { - $expectedRequestData = $this->signedCommand->toArray(); + $expectedRequestData = SignedCommandMapper::toArray($this->signedCommand); $expectedResponseData = [ 'gas' => 123, @@ -116,7 +133,7 @@ public function it_should_send_a_signed_command_to_the_local_endpoint_and_return /** @test */ public function it_should_send_a_collection_of_signed_commands_to_the_send_endpoint_and_return_a_collection_of_request_keys(): void { - $expectedRequestData = (new SignedCommandCollection($this->signedCommand))->toPayload(); + $expectedRequestData = SignedCommandCollectionMapper::toArray(new SignedCommandCollection($this->signedCommand)); $requestKeyString = 'y3aWL72-3wAy7vL9wcegGXnstH0lHi-q-cfxkhD5JCw'; diff --git a/tests/Unit/Crypto/CommandSignerTest.php b/tests/Unit/Crypto/CommandSignerTest.php new file mode 100644 index 0000000..fc11bef --- /dev/null +++ b/tests/Unit/Crypto/CommandSignerTest.php @@ -0,0 +1,36 @@ +withExecutePayload(new ExecutePayload( + code: '(+ 2 2)' + ))->make(); + + $signed = CommandSigner::sign($command, new KeyPairCollection($keyPair)); + + $expectedHash = Base64UrlSafe::encodeUnpadded(Hash::generic(CommandMapper::toString($command))); + + $this->assertInstanceOf(SignedCommand::class, $signed); + $this->assertSame($expectedHash, $signed->hash); + $this->assertTrue(MessageSigner::verifySignature(Hash::generic(CommandMapper::toString($command)), $signed->signatures->first()->signature, $keyPair->publicKey->key)); + } +} diff --git a/tests/Unit/Crypto/KeyFactoryTest.php b/tests/Unit/Crypto/KeyFactoryTest.php new file mode 100644 index 0000000..a7af8fd --- /dev/null +++ b/tests/Unit/Crypto/KeyFactoryTest.php @@ -0,0 +1,22 @@ +assertInstanceOf(KeyPair::class, $keyPair); + $this->assertInstanceOf(PublicKey::class, $keyPair->publicKey); + $this->assertInstanceOf(SecretKey::class, $keyPair->secretKey); + } +} diff --git a/tests/Unit/Crypto/KeyPairCollectionTest.php b/tests/Unit/Crypto/KeyPairCollectionTest.php deleted file mode 100644 index f58567e..0000000 --- a/tests/Unit/Crypto/KeyPairCollectionTest.php +++ /dev/null @@ -1,20 +0,0 @@ -assertEquals([$keyPair1, $keyPair2], $keyPairCollection->toArray()); - } -} diff --git a/tests/Unit/Crypto/KeyPairTest.php b/tests/Unit/Crypto/KeyPairTest.php deleted file mode 100644 index 3460965..0000000 --- a/tests/Unit/Crypto/KeyPairTest.php +++ /dev/null @@ -1,21 +0,0 @@ -assertInstanceOf(KeyPair::class, $keyPair); - $this->assertInstanceOf(SignaturePublicKey::class, $keyPair->publicKey); - $this->assertInstanceOf(SignatureSecretKey::class, $keyPair->secretKey); - } -} diff --git a/tests/Unit/Crypto/MessageSignerTest.php b/tests/Unit/Crypto/MessageSignerTest.php new file mode 100644 index 0000000..61e85ac --- /dev/null +++ b/tests/Unit/Crypto/MessageSignerTest.php @@ -0,0 +1,50 @@ +assertInstanceOf(Signature::class, $signature); + $this->assertSame($expectedHash, $signature->hash); + $this->assertTrue(MessageSigner::verifySignature(Hash::generic($message), $signature->signature, $keyPair->publicKey->key)); + } + + /** @test */ + public function it_should_sign_a_hash_with_a_key_pair_and_return_a_signed_message_object(): void + { + $message = 'test message'; + $keyPair = KeyFactory::generate(); + $hash = Hash::generic($message); + $signature = MessageSigner::signHash($hash, $keyPair); + + $this->assertInstanceOf(Signature::class, $signature); + $this->assertSame(Base64UrlSafe::encodeUnpadded($hash), $signature->hash); + $this->assertTrue(MessageSigner::verifySignature(Hash::generic($message), $signature->signature, $keyPair->publicKey->key)); + } + + /** @test */ + public function it_should_verify_a_signature_with_a_public_key_and_message(): void + { + $keyPair = KeyFactory::generate(); + $message = 'test message'; + $signature = MessageSigner::sign($message, $keyPair); + + $this->assertTrue(MessageSigner::verifySignature(Hash::generic($message), $signature->signature, $keyPair->publicKey->key)); + } +} diff --git a/tests/Unit/Crypto/SignatureCollectionTest.php b/tests/Unit/Crypto/SignatureCollectionTest.php deleted file mode 100644 index 49a937d..0000000 --- a/tests/Unit/Crypto/SignatureCollectionTest.php +++ /dev/null @@ -1,45 +0,0 @@ -assertSame($signature1, $signatureCollection->first()); - } - - /** @test */ - public function it_should_return_all_signatures(): void - { - $signature1 = new Signature('hash', 'sig1'); - $signature2 = new Signature('hash', 'sig2'); - $signatureCollection = new SignatureCollection($signature1, $signature2); - - $this->assertEquals([$signature1, $signature2], $signatureCollection->toArray()); - } - - /** @test */ - public function it_should_create_a_new_signature_collection_from_an_array(): void - { - $signatures = [ - ['sig' => 'sig1'], - ['sig' => 'sig2'] - ]; - - $signatureCollection = SignatureCollection::fromArray($signatures, 'hash'); - - $this->assertInstanceOf(SignatureCollection::class, $signatureCollection); - $this->assertSame('hash', $signatureCollection->first()->hash); - $this->assertSame('sig1', $signatureCollection->first()->signature); - } -} diff --git a/tests/Unit/Crypto/SignerTest.php b/tests/Unit/Crypto/SignerTest.php deleted file mode 100644 index 909f694..0000000 --- a/tests/Unit/Crypto/SignerTest.php +++ /dev/null @@ -1,87 +0,0 @@ -assertInstanceOf(Signature::class, $signature); - $this->assertSame($expectedHash, $signature->hash); - $this->assertTrue(Signer::verifySignature(Hash::generic($message), $signature->signature, $keyPair->publicKey)); - } - - /** @test */ - public function it_should_sign_a_hash_with_a_key_pair_and_return_a_signed_message_object(): void - { - $message = 'test message'; - $keyPair = KeyPair::generate(); - $hash = Hash::generic($message); - $signature = Signer::signHash($hash, $keyPair); - - $this->assertInstanceOf(Signature::class, $signature); - $this->assertSame(Base64UrlSafe::encodeUnpadded($hash), $signature->hash); - $this->assertTrue(Signer::verifySignature(Hash::generic($message), $signature->signature, $keyPair->publicKey)); - } - - /** @test */ - public function it_should_verify_a_signature_with_a_public_key_and_message(): void - { - $keyPair = KeyPair::generate(); - $message = 'test message'; - $signature = Signer::sign($message, $keyPair); - - $this->assertTrue(Signer::verifySignature(Hash::generic($message), $signature->signature, $keyPair->publicKey)); - } - - /** @test */ - public function it_should_sign_a_command_with_a_key_pair_and_return_a_signature_object(): void - { - $keyPair = KeyPair::generate(); - - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $signature = Signer::signCommand($command, $keyPair); - - $expectedHash = Base64UrlSafe::encodeUnpadded(Hash::generic($command->toString())); - - $this->assertInstanceOf(Signature::class, $signature); - $this->assertSame($expectedHash, $signature->hash); - $this->assertTrue(Signer::verifySignature(Hash::generic($command->toString()), $signature->signature, $keyPair->publicKey)); - } -} diff --git a/tests/Unit/DataMappers/CommandMapperTest.php b/tests/Unit/DataMappers/CommandMapperTest.php new file mode 100644 index 0000000..a2a212a --- /dev/null +++ b/tests/Unit/DataMappers/CommandMapperTest.php @@ -0,0 +1,111 @@ +command = new Command( + meta: new Metadata( + creationTime: Carbon::createFromTimestamp(0), + ttl: 0, + gasLimit: 0, + chainId: '', + gasPrice: 0, + sender: '' + ), + payload: new Payload( + payloadType: PayloadType::EXECUTE, + executePayload: new ExecutePayload( + code: '(+ 2 2)' + ) + ), + networkId: 'testnet0', + nonce: 'test', + signers: new SignerCollection(new Signer( + publicKey: $keyPair->publicKey, + capabilities: new CapabilityCollection( + new Capability( + name: 'cap.example', + arguments: [] + ) + ) + )) + ); + + $this->commandArray = [ + 'networkId' => 'testnet0', + 'payload' => [ + 'exec' => [ + 'data' => [], + 'code' => '(+ 2 2)' + ] + ], + 'meta' => [ + 'creationTime' => 0, + 'ttl' => 0, + 'gasLimit' => 0, + 'chainId' => '', + 'gasPrice' => 0, + 'sender' => '' + ], + 'nonce' => 'test', + 'signers' => [ + [ + 'pubKey' => $keyPair->publicKey->toString(), + 'clist' => [ + [ + 'name' => 'cap.example', + 'args' => [] + ] + ] + ] + ], + ]; + } + /** @test */ + public function it_should_be_able_to_map_a_command_to_an_array(): void + { + $actual = CommandMapper::toArray($this->command); + + $this->assertEquals($this->commandArray, $actual); + } + + /** @test */ + public function it_should_be_able_to_map_a_command_to_a_string(): void + { + $actual = CommandMapper::toString($this->command); + + $this->assertEquals(json_encode($this->commandArray, JSON_THROW_ON_ERROR), $actual); + } + + /** @test */ + public function it_should_be_able_to_map_a_string_to_a_command(): void + { + $actual = CommandMapper::fromString(json_encode($this->commandArray, JSON_THROW_ON_ERROR)); + + $this->assertEquals($this->command, $actual); + } +} diff --git a/tests/Unit/DataMappers/SignedCommandCollectionMapperTest.php b/tests/Unit/DataMappers/SignedCommandCollectionMapperTest.php new file mode 100644 index 0000000..ed86bec --- /dev/null +++ b/tests/Unit/DataMappers/SignedCommandCollectionMapperTest.php @@ -0,0 +1,125 @@ +publicKey, + capabilities: new CapabilityCollection( + new Capability( + name: 'cap.example', + arguments: [] + ) + ) + )) + ); + + $commandArray = [ + 'networkId' => 'testnet0', + 'payload' => [ + 'exec' => [ + 'data' => [], + 'code' => '(+ 2 2)' + ] + ], + 'meta' => [ + 'creationTime' => 0, + 'ttl' => 0, + 'gasLimit' => 0, + 'chainId' => '', + 'gasPrice' => 0, + 'sender' => '' + ], + 'nonce' => 'test', + 'signers' => [ + [ + 'pubKey' => $keyPair->publicKey->toString(), + 'clist' => [ + [ + 'name' => 'cap.example', + 'args' => [] + ] + ] + ] + ], + ]; + + $this->signedCommand = new SignedCommand( + hash: 'test-hash', + signatures: new SignatureCollection(new Signature( + hash: 'test-hash', + signature: 'test-signature' + )), + command: $command + ); + + $this->signedCommandArray = [ + 'cmd' => json_encode($commandArray, JSON_THROW_ON_ERROR), + 'hash' => 'test-hash', + 'sigs' => [ + ['sig' => 'test-signature'] + ], + ]; + } + + /** @test */ + public function it_should_be_able_to_map_a_signed_command_collection_to_an_array(): void + { + $actual = SignedCommandCollectionMapper::toArray(new SignedCommandCollection($this->signedCommand)); + + $this->assertEquals(['cmds' => [$this->signedCommandArray]], $actual); + } + + /** @test */ + public function it_should_be_able_to_map_a_signed_command_collection_to_a_string(): void + { + $actual = SignedCommandCollectionMapper::toString(new SignedCommandCollection($this->signedCommand)); + + $this->assertEquals(json_encode(['cmds' => [$this->signedCommandArray]], JSON_THROW_ON_ERROR), $actual); + } +} diff --git a/tests/Unit/DataMappers/SignedCommandMapperTest.php b/tests/Unit/DataMappers/SignedCommandMapperTest.php new file mode 100644 index 0000000..76a80d8 --- /dev/null +++ b/tests/Unit/DataMappers/SignedCommandMapperTest.php @@ -0,0 +1,132 @@ +publicKey, + capabilities: new CapabilityCollection( + new Capability( + name: 'cap.example', + arguments: [] + ) + ) + )) + ); + + $commandArray = [ + 'networkId' => 'testnet0', + 'payload' => [ + 'exec' => [ + 'data' => [], + 'code' => '(+ 2 2)' + ] + ], + 'meta' => [ + 'creationTime' => 0, + 'ttl' => 0, + 'gasLimit' => 0, + 'chainId' => '', + 'gasPrice' => 0, + 'sender' => '' + ], + 'nonce' => 'test', + 'signers' => [ + [ + 'pubKey' => $keyPair->publicKey->toString(), + 'clist' => [ + [ + 'name' => 'cap.example', + 'args' => [] + ] + ] + ] + ], + ]; + + $this->signedCommand = new SignedCommand( + hash: 'test-hash', + signatures: new SignatureCollection(new Signature( + hash: 'test-hash', + signature: 'test-signature' + )), + command: $command + ); + + $this->signedCommandArray = [ + 'cmd' => json_encode($commandArray, JSON_THROW_ON_ERROR), + 'hash' => 'test-hash', + 'sigs' => [ + ['sig' => 'test-signature'] + ], + ]; + } + + /** @test */ + public function it_should_be_able_to_map_a_signed_command_to_an_array(): void + { + $actual = SignedCommandMapper::toArray($this->signedCommand); + + $this->assertEquals($this->signedCommandArray, $actual); + } + + /** @test */ + public function it_should_be_able_to_map_a_signed_command_to_a_string(): void + { + $actual = SignedCommandMapper::toString($this->signedCommand); + + $this->assertEquals(json_encode($this->signedCommandArray, JSON_THROW_ON_ERROR), $actual); + } + + /** @test */ + public function it_should_be_able_to_map_a_string_to_a_signed_command(): void + { + $actual = SignedCommandMapper::fromString(json_encode($this->signedCommandArray, JSON_THROW_ON_ERROR)); + + $this->assertEquals($this->signedCommand, $actual); + } +} diff --git a/tests/Unit/Pact/CommandFactoryTest.php b/tests/Unit/Pact/CommandFactoryTest.php new file mode 100644 index 0000000..3ad8d19 --- /dev/null +++ b/tests/Unit/Pact/CommandFactoryTest.php @@ -0,0 +1,126 @@ +keyPair = KeyFactory::generate(); + } + + /** @test */ + public function it_should_be_able_to_construct_a_command_with_default_options_from_a_execute_payload(): void + { + $factory = new CommandFactory(); + $payload = new ExecutePayload( + code: '(+ 2 2)' + ); + + $expected = new Command( + meta: (new MetadataFactory())->make(), + payload: new Payload( + payloadType: PayloadType::EXECUTE, + executePayload: $payload + ), + networkId: '0', + nonce: Carbon::now()->toISOString(), + signers: new SignerCollection(), + ); + + $actual = $factory->withExecutePayload($payload)->make(); + + $this->assertEquals($expected, $actual); + } + + /** @test */ + public function it_should_be_able_to_construct_a_command_with_default_options_from_a_continue_payload(): void + { + $factory = new CommandFactory(); + $payload = new ContinuePayload( + pactId: 'pact-id', + rollback: false, + step: 0, + proof: 'proof', + data: [] + ); + + $expected = new Command( + meta: (new MetadataFactory())->make(), + payload: new Payload( + payloadType: PayloadType::CONTINUE, + continuePayload: $payload + ), + networkId: '0', + nonce: Carbon::now()->toISOString(), + signers: new SignerCollection(), + ); + + $actual = $factory->withContinuePayload($payload)->make(); + + $this->assertEquals($expected, $actual); + } + + /** @test */ + public function it_should_be_able_to_construct_a_command_with_defined_options(): void + { + $factory = new CommandFactory(); + $payload = new ExecutePayload( + code: '(+ 2 2)' + ); + $signers = new SignerCollection( + new Signer( + publicKey: $this->keyPair->publicKey, + capabilities: new CapabilityCollection( + new Capability( + name: 'test.cap', + arguments: [] + ) + ) + ) + ); + $meta = (new MetadataFactory())->make(); + + $expected = new Command( + meta: $meta, + payload: new Payload( + payloadType: PayloadType::EXECUTE, + executePayload: $payload + ), + networkId: 'network-0', + nonce: 'test-nonce', + signers: $signers, + ); + + $actual = $factory->withExecutePayload($payload) + ->withNonce('test-nonce') + ->withSigners($signers) + ->withMetadata($meta) + ->withNetworkId('network-0') + ->make(); + + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/Unit/Pact/CommandTest.php b/tests/Unit/Pact/CommandTest.php deleted file mode 100644 index 02ceaa0..0000000 --- a/tests/Unit/Pact/CommandTest.php +++ /dev/null @@ -1,284 +0,0 @@ -assertEquals(Carbon::now()->toISOString(), $command->nonce); - } - - /** @test */ - public function it_should_cast_to_array(): void - { - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $expected = [ - 'signers' => [], - 'networkId' => null, - 'payload' => [ - 'exec' => [ - 'data' => [], - 'code' => '(+ 2 2)' - ] - ], - 'meta' => [ - 'creationTime' => 0, - 'ttl' => 0, - 'gasLimit' => 0, - 'chainId' => '', - 'gasPrice' => 0, - 'sender' => '' - ], - 'nonce' => '2021-11-26T12:30:00.000000Z' - ]; - - $this->assertEquals($expected, $command->toArray()); - } - - /** @test */ - public function it_should_cast_to_string(): void - { - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $expected = json_encode([ - 'signers' => [], - 'networkId' => null, - 'payload' => [ - 'exec' => [ - 'data' => [], - 'code' => '(+ 2 2)' - ] - ], - 'meta' => [ - 'creationTime' => 0, - 'ttl' => 0, - 'gasLimit' => 0, - 'chainId' => '', - 'gasPrice' => 0, - 'sender' => '' - ], - 'nonce' => '2021-11-26T12:30:00.000000Z' - ], JSON_THROW_ON_ERROR); - - $this->assertEquals($expected, $command->toString()); - } - - /** @test */ - public function it_should_construct_from_string(): void - { - $expected = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $jsonCommand = json_encode([ - 'signers' => [], - 'networkId' => null, - 'payload' => [ - 'exec' => [ - 'data' => [], - 'code' => '(+ 2 2)' - ] - ], - 'meta' => [ - 'creationTime' => 0, - 'ttl' => 0, - 'gasLimit' => 0, - 'chainId' => '', - 'gasPrice' => 0, - 'sender' => '' - ], - 'nonce' => '2021-11-26T12:30:00.000000Z' - ], JSON_THROW_ON_ERROR); - - $this->assertEquals($expected, Command::fromString($jsonCommand)); - } - - /** @test */ - public function it_should_be_able_to_set_signers(): void - { - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $expectedWithoutSigners = [ - 'signers' => [], - 'networkId' => null, - 'payload' => [ - 'exec' => [ - 'data' => [], - 'code' => '(+ 2 2)' - ] - ], - 'meta' => [ - 'creationTime' => 0, - 'ttl' => 0, - 'gasLimit' => 0, - 'chainId' => '', - 'gasPrice' => 0, - 'sender' => '' - ], - 'nonce' => '2021-11-26T12:30:00.000000Z' - ]; - - $this->assertEquals($expectedWithoutSigners, $command->toArray()); - - $expectedWithSigners = [ - 'signers' => [ - ['pubKey' => 'public-key1'], - ['pubKey' => 'public-key2'], - ], - 'networkId' => null, - 'payload' => [ - 'exec' => [ - 'data' => [], - 'code' => '(+ 2 2)' - ] - ], - 'meta' => [ - 'creationTime' => 0, - 'ttl' => 0, - 'gasLimit' => 0, - 'chainId' => '', - 'gasPrice' => 0, - 'sender' => '' - ], - 'nonce' => '2021-11-26T12:30:00.000000Z' - ]; - - $command->setSigners(['public-key1', 'public-key2']); - - $this->assertEquals($expectedWithSigners, $command->toArray()); - } - - /** @test */ - public function it_should_return_a_signed_command_object_when_using_get_signed_command(): void - { - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $keyPair = KeyPair::generate(); - - $command->setSigners([Hex::encode($keyPair->publicKey->getRawKeyMaterial())]); - - $message = $command->toString(); - $expectedHash = Base64UrlSafe::encodeUnpadded(Hash::generic($message)); - - $signedCommand = $command->getSignedCommand(new KeyPairCollection($keyPair)); - - $this->assertInstanceOf(SignedCommand::class, $signedCommand); - $this->assertSame($expectedHash, $signedCommand->hash); - $this->assertTrue(Signer::verifySignature(Hash::generic($message), $signedCommand->signatures->first()->signature, $keyPair->publicKey)); - } -} diff --git a/tests/Unit/Pact/ContinuePayloadTest.php b/tests/Unit/Pact/ContinuePayloadTest.php deleted file mode 100644 index e4d4405..0000000 --- a/tests/Unit/Pact/ContinuePayloadTest.php +++ /dev/null @@ -1,31 +0,0 @@ - 'value']; - - $continuePayload = new ContinuePayload($pactId, $rollback, $step, $proof, $data); - - $expectedArray = [ - 'proof' => $proof, - 'pactId' => $pactId, - 'rollback' => $rollback, - 'step' => $step, - 'data' => $data - ]; - - $this->assertSame($expectedArray, $continuePayload->toArray()); - } -} diff --git a/tests/Unit/Pact/ExecutePayloadTest.php b/tests/Unit/Pact/ExecutePayloadTest.php deleted file mode 100644 index cf881ba..0000000 --- a/tests/Unit/Pact/ExecutePayloadTest.php +++ /dev/null @@ -1,24 +0,0 @@ - 'value']; - - $executePayload = new ExecutePayload($code, $data); - $expectedArray = [ - 'data' => $data, - 'code' => $code, - ]; - - $this->assertSame($expectedArray, $executePayload->toArray()); - } -} diff --git a/tests/Unit/Pact/MetaTest.php b/tests/Unit/Pact/MetaTest.php deleted file mode 100644 index 7fd5b61..0000000 --- a/tests/Unit/Pact/MetaTest.php +++ /dev/null @@ -1,83 +0,0 @@ - $creationTime->getTimestamp(), - 'ttl' => $ttl, - 'gasLimit' => $gasLimit, - 'chainId' => $chainId, - 'gasPrice' => $gasPrice, - 'sender' => $sender - ]; - - $this->assertSame($expectedArray, $meta->toArray()); - } - - /** @test */ - public function it_should_be_able_to_be_constructed_from_an_options_array(): void - { - $creationTime = Carbon::now(); - $ttl = 300; - $gasLimit = 50; - $chainId = 'test-chain-id'; - $gasPrice = 1.25; - $sender = 'test-sender'; - - $expected = new Meta($creationTime, $ttl, $gasLimit, $chainId, $gasPrice, $sender); - - $options = [ - 'creationTime' => $creationTime->getTimestamp(), - 'ttl' => $ttl, - 'gasLimit' => $gasLimit, - 'chainId' => $chainId, - 'gasPrice' => $gasPrice, - 'sender' => $sender - ]; - - $actual = Meta::create($options); - - $this->assertEquals($expected, $actual); - } - - /** @test */ - public function it_should_be_able_to_be_constructed_using_the_create_method_with_default_options(): void - { - $creationTime = Carbon::now(); - $ttl = 7200; - $gasLimit = 10000; - $chainId = '0'; - $gasPrice = 1e-8; - $sender = ''; - - $expected = new Meta($creationTime, $ttl, $gasLimit, $chainId, $gasPrice, $sender); - - $actual = Meta::create(); - - $this->assertEquals($expected, $actual); - } -} diff --git a/tests/Unit/Pact/MetadataFactoryTest.php b/tests/Unit/Pact/MetadataFactoryTest.php new file mode 100644 index 0000000..1fcdee6 --- /dev/null +++ b/tests/Unit/Pact/MetadataFactoryTest.php @@ -0,0 +1,61 @@ +make(); + + $this->assertEquals($expected, $actual); + } + + /** @test */ + public function it_should_be_able_to_construct_a_metadata_object_with_supplied_options(): void + { + $expected = new Metadata( + creationTime: Carbon::now()->subDay(), + ttl: 3600, + gasLimit: 15000, + chainId: '2', + gasPrice: 1e-7, + sender: 'sender' + ); + + $actual = (new MetadataFactory()) + ->withOptions([ + 'creationTime' => Carbon::now()->subDay()->getTimestamp(), + 'ttl' => 3600, + 'gasLimit' => 15000, + 'chainId' => '2', + 'gasPrice' => 1e-7, + 'sender' => 'sender', + ]) + ->make(); + + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/Unit/Pact/PayloadTest.php b/tests/Unit/Pact/PayloadTest.php deleted file mode 100644 index 75e39d3..0000000 --- a/tests/Unit/Pact/PayloadTest.php +++ /dev/null @@ -1,60 +0,0 @@ -expectException(InvalidArgumentException::class); - - new Payload(PayloadType::CONTINUE, null, new ExecutePayload('test', [])); - } - - /** @test */ - public function it_should_validate_execute_payload(): void - { - $this->expectException(InvalidArgumentException::class); - - new Payload(PayloadType::EXECUTE, new ContinuePayload('test', true, 1), null); - } - - /** @test */ - public function it_should_create_and_return_valid_execute_payload(): void - { - $payload = new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: 'test', - data: ['key' => 'value'] - ) - ); - - $this->assertSame([PayloadType::EXECUTE->value => ['data' => ['key' => 'value'], 'code' => 'test']], $payload->toArray()); - } - - /** @test */ - public function it_should_create_and_return_valid_continue_payload(): void - { - $payload = new Payload( - payloadType: PayloadType::CONTINUE, - continuePayload: new ContinuePayload( - pactId: 'test', - rollback: true, - step: 1, - proof: 'proof', - data: ['key' => 'value'] - ) - ); - - $this->assertSame([PayloadType::CONTINUE->value => ['proof' => 'proof', 'pactId' => 'test', 'rollback' => true, 'step' => 1, 'data' => ['key' => 'value']]], $payload->toArray()); - } -} diff --git a/tests/Unit/Pact/RequestKeyCollectionTest.php b/tests/Unit/Pact/RequestKeyCollectionTest.php deleted file mode 100644 index a660754..0000000 --- a/tests/Unit/Pact/RequestKeyCollectionTest.php +++ /dev/null @@ -1,43 +0,0 @@ -assertEquals([$requestKey1, $requestKey2], $requestKeyCollection->toArray()); - } - - /** @test */ - public function it_should_get_first_request_key(): void - { - $requestKey1 = new RequestKey('key1'); - $requestKey2 = new RequestKey('key2'); - - $requestKeyCollection = new RequestKeyCollection($requestKey1, $requestKey2); - - $this->assertEquals($requestKey1, $requestKeyCollection->first()); - } - - /** @test */ - public function it_should_convert_to_plain_array(): void - { - $requestKey1 = new RequestKey('key1'); - $requestKey2 = new RequestKey('key2'); - - $requestKeyCollection = new RequestKeyCollection($requestKey1, $requestKey2); - - $this->assertEquals(['key1', 'key2'], $requestKeyCollection->toPlainArray()); - } -} diff --git a/tests/Unit/Pact/SignedCommandCollectionTest.php b/tests/Unit/Pact/SignedCommandCollectionTest.php deleted file mode 100644 index 194b7c3..0000000 --- a/tests/Unit/Pact/SignedCommandCollectionTest.php +++ /dev/null @@ -1,151 +0,0 @@ -setSigners(['public-key']); - - $signature = new Signature( - hash: 'hash', - signature: 'signature', - publicKey: 'public-key', - ); - - $signatures = new SignatureCollection($signature); - - $signedCommand = new SignedCommand('hash', $signatures, $command); - - $signedCommandCollection = new SignedCommandCollection($signedCommand); - - $this->assertEquals([$signedCommand], $signedCommandCollection->toArray()); - } - - /** @test */ - public function it_should_convert_in_to_a_payload_array(): void - { - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $command->setSigners(['public-key']); - - $signature = new Signature( - hash: 'hash', - signature: 'signature', - publicKey: 'public-key', - ); - - $signatures = new SignatureCollection($signature); - - $signedCommand = new SignedCommand('hash', $signatures, $command); - - $signedCommandCollection = new SignedCommandCollection($signedCommand); - - $expected = [ - 'cmds' => [ - [ - 'hash' => 'hash', - 'sigs' => [ - [ - 'sig' => 'signature' - ] - ], - 'cmd' => '{"signers":[{"pubKey":"public-key"}],"networkId":null,"payload":{"exec":{"data":[],"code":"(+ 2 2)"}},"meta":{"creationTime":0,"ttl":0,"gasLimit":0,"chainId":"","gasPrice":0,"sender":""},"nonce":"2021-11-26T12:30:00.000000Z"}' - ] - ] - ]; - - $this->assertEquals($expected, $signedCommandCollection->toPayload()); - } - - /** @test */ - public function it_should_convert_in_to_a_json_payload_string(): void - { - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $command->setSigners(['public-key']); - - $signature = new Signature( - hash: 'hash', - signature: 'signature', - publicKey: 'public-key', - ); - - $signatures = new SignatureCollection($signature); - - $signedCommand = new SignedCommand('hash', $signatures, $command); - - $signedCommandCollection = new SignedCommandCollection($signedCommand); - - $expected = '{"cmds":[{"hash":"hash","sigs":[{"sig":"signature"}],"cmd":"{\"signers\":[{\"pubKey\":\"public-key\"}],\"networkId\":null,\"payload\":{\"exec\":{\"data\":[],\"code\":\"(+ 2 2)\"}},\"meta\":{\"creationTime\":0,\"ttl\":0,\"gasLimit\":0,\"chainId\":\"\",\"gasPrice\":0,\"sender\":\"\"},\"nonce\":\"2021-11-26T12:30:00.000000Z\"}"}]}'; - $this->assertEquals($expected, $signedCommandCollection->toPayloadString()); - } -} diff --git a/tests/Unit/Pact/SignedCommandTest.php b/tests/Unit/Pact/SignedCommandTest.php deleted file mode 100644 index b0733d7..0000000 --- a/tests/Unit/Pact/SignedCommandTest.php +++ /dev/null @@ -1,175 +0,0 @@ -setSigners(['public-key']); - - $signature = new Signature( - hash: 'hash', - signature: 'signature', - publicKey: 'public-key', - ); - - $signatures = new SignatureCollection($signature); - - $signedCommand = new SignedCommand('hash', $signatures, $command); - - $commandJson = $signedCommand->toString(); - - $signedCommandFromJson = SignedCommand::fromString($commandJson); - - $this->assertEquals($signedCommand->command, $signedCommandFromJson->command); - $this->assertEquals($signedCommand->hash, $signedCommandFromJson->hash); - } - - /** @test */ - public function it_should_validate_signatures_when_creating(): void - { - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $command->setSigners(['public-key']); - - $signature1 = new Signature( - hash: 'hash', - signature: 'signature', - publicKey: 'public-key', - ); - - $signature2 = new Signature( - hash: 'incorrect', - signature: 'signature', - publicKey: 'public-key', - ); - - $signatures = new SignatureCollection($signature1, $signature2); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Signatures for different hashes found: incorrect, expected: hash'); - - new SignedCommand('hash', $signatures, $command); - } - - /** @test */ - public function it_should_return_the_expected_array(): void - { - $signature1 = new Signature('hash1', 'signature1'); - $signature2 = new Signature('hash1', 'signature2'); - $signatureCollection = new SignatureCollection($signature1, $signature2); - - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $signedCommand = new SignedCommand('hash1', $signatureCollection, $command); - - $expectedArray = [ - 'hash' => 'hash1', - 'sigs' => [ - ['sig' => 'signature1'], - ['sig' => 'signature2'], - ], - 'cmd' => $command->toString(), - ]; - - $this->assertSame($expectedArray, $signedCommand->toArray()); - } - - /** @test */ - public function it_should_return_the_expected_string(): void - { - $signature1 = new Signature('hash1', 'signature1'); - $signature2 = new Signature('hash1', 'signature2'); - $signatureCollection = new SignatureCollection($signature1, $signature2); - - $command = new Command( - meta: new Meta( - creationTime: Carbon::createFromTimestamp(0), - ttl: 0, - gasLimit: 0, - chainId: '', - gasPrice: 0, - sender: '' - ), - payload: new Payload( - payloadType: PayloadType::EXECUTE, - executePayload: new ExecutePayload( - code: '(+ 2 2)' - ) - ) - ); - - $signedCommand = new SignedCommand('hash1', $signatureCollection, $command); - - $expectedString = '{"hash":"hash1","sigs":[{"sig":"signature1"},{"sig":"signature2"}],"cmd":"{\"signers\":[],\"networkId\":null,\"payload\":{\"exec\":{\"data\":[],\"code\":\"(+ 2 2)\"}},\"meta\":{\"creationTime\":0,\"ttl\":0,\"gasLimit\":0,\"chainId\":\"\",\"gasPrice\":0,\"sender\":\"\"},\"nonce\":\"2021-11-26T12:30:00.000000Z\"}"}'; - $this->assertSame($expectedString, $signedCommand->toString()); - } -} diff --git a/tests/Unit/ValueObjects/HasCollectionMethodsTest.php b/tests/Unit/ValueObjects/HasCollectionMethodsTest.php new file mode 100644 index 0000000..e8ba7b0 --- /dev/null +++ b/tests/Unit/ValueObjects/HasCollectionMethodsTest.php @@ -0,0 +1,88 @@ +assertEquals($stringArray, $collection->toArray()); + } + + /** @test */ + public function it_should_be_able_to_return_the_first_element_of_a_collection(): void + { + $stringArray = [ + 'first', + 'second', + 'third', + 'fourth', + ]; + + $collection = new TestCollection(...$stringArray); + + $this->assertEquals('first', $collection->first()); + } + + /** @test */ + public function it_should_be_able_to_return_a_collection_element_at_a_given_key(): void + { + $stringArray = [ + 'first', + 'second', + 'third', + 'fourth', + ]; + + $collection = new TestCollection(...$stringArray); + + $this->assertEquals('third', $collection->get(2)); + } + + /** @test */ + public function it_should_be_able_to_return_the_int_count_of_the_amount_of_elements_in_the_collection(): void + { + $stringArray = [ + 'first', + 'second', + 'third', + 'fourth', + ]; + + $collection = new TestCollection(...$stringArray); + + $this->assertEquals(4, $collection->count()); + } +} + +final class TestCollection implements Collection +{ + use HasCollectionMethods; + + public function __construct(string ...$strings) + { + $this->array = $strings; + } +}