diff --git a/README.md b/README.md index 7511400..b47d6fe 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,237 @@ -# php-scale-codec v2.0 +# php-scale-codec -PHP SCALE Codec for Substrate - Version 2.0 +[![PHP Version](https://img.shields.io/badge/PHP-8.2%2B-777bb4)](https://php.net) +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -## Architecture Overview +PHP implementation of the **SCALE (Simple Concatenated Aggregate Little-Endian)** codec used in Substrate-based blockchains like Polkadot, Kusama, and Substrate-native chains. -### Directory Structure +## Features +- ✅ Full SCALE codec implementation +- ✅ All primitive types (U8-U128, I8-I128, Bool, String) +- ✅ Compact integer encoding +- ✅ Compound types (Vec, Option, Tuple, Struct, Enum) +- ✅ Metadata v12-v15 support +- ✅ Extrinsic building and signing +- ✅ Event parsing +- ✅ polkadot.js compatible + +## Installation + +```bash +composer require gmajor/substrate-codec-php ``` -src/ -├── Bytes/ # Byte manipulation utilities -│ └── ScaleBytes.php -├── Encoder/ # Encoder interfaces -│ └── EncoderInterface.php -├── Decoder/ # Decoder interfaces -│ └── DecoderInterface.php -├── Types/ # Type definitions and implementations -│ ├── ScaleType.php # PHP 8.2 enum for SCALE types -│ ├── TypeInterface.php # Core type interface -│ ├── AbstractType.php # Base type implementation -│ ├── TypeRegistry.php # Type registration and lookup -│ ├── TypeFactory.php # Type creation factory -│ ├── BoolType.php -│ └── NullType.php -├── Metadata/ # Metadata parsing (Phase 3) -├── Extrinsic/ # Extrinsic handling (Phase 3) -└── Exception/ # Exception classes - ├── ScaleEncodeException.php - ├── ScaleDecodeException.php - └── InvalidTypeException.php - -tests/ -├── Bytes/ -│ └── ScaleBytesTest.php -└── Types/ - ├── ScaleTypeTest.php - └── TypeRegistryTest.php + +## Requirements + +- PHP 8.2+ +- ext-gmp (for large integer handling) +- ext-json +- ext-sodium + +## Quick Start + +### Basic Encoding/Decoding + +```php +get('U32'); +$encoded = $u32->encode(12345); +echo $encoded->toHex(); // 0x39300000 + +// Decode it back +$bytes = ScaleBytes::fromHex('0x39300000'); +$decoded = $u32->decode($bytes); +echo $decoded; // 12345 ``` -## Key Components +### Compact Integers + +```php +$compact = $registry->get('Compact'); + +// Small values (0-63): 1 byte +echo $compact->encode(42)->toHex(); // 0xa8 -### ScaleType Enum (PHP 8.2) +// Medium values (64-16383): 2 bytes +echo $compact->encode(100)->toHex(); // 0x9101 + +// Large values +echo $compact->encode(1000000)->toHex(); // 0x02c0843d00 +``` + +### Boolean ```php -use Substrate\ScaleCodec\Types\ScaleType; +$bool = $registry->get('Bool'); -$type = ScaleType::U32; -$type->getByteSize(); // 4 -$type->isUnsignedInt(); // true +$encoded = $bool->encode(true); // 0x01 +$encoded = $bool->encode(false); // 0x00 ``` -### TypeRegistry +### String/Text ```php -use Substrate\ScaleCodec\Types\{TypeRegistry, BoolType}; +$string = $registry->get('String'); +$encoded = $string->encode('Hello, Substrate!'); +// 0x2048656c6c6f2c2053756273747261746521 +``` -$registry = new TypeRegistry(); -$registry->register('bool', new BoolType($registry)); -$boolType = $registry->get('bool'); +### Vectors + +```php +// Vec +$vecU8 = (new VecType($registry))->setElementType($registry->get('U8')); +$encoded = $vecU8->encode([1, 2, 3, 4, 5]); +// 0x140102030405 + +$decoded = $vecU8->decode(ScaleBytes::fromHex('0x140102030405')); +// [1, 2, 3, 4, 5] ``` -### ScaleBytes +### Options ```php -use Substrate\ScaleCodec\Bytes\ScaleBytes; +$optionU32 = (new OptionType($registry))->setInnerType($registry->get('U32')); + +// Some value +$encoded = $optionU32->encode(42); // 0x012a000000 -$bytes = ScaleBytes::fromHex('0x01020304'); -$bytes->readBytes(2); // [1, 2] -$bytes->remaining(); // 2 -$bytes->toHex(); // '0x01020304' +// None value +$encoded = $optionU32->encode(null); // 0x00 ``` -## Installation +### Structs + +```php +$struct = (new StructType($registry))->setFields([ + 'id' => $registry->get('U32'), + 'name' => $registry->get('String'), + 'active' => $registry->get('Bool'), +]); + +$encoded = $struct->encode([ + 'id' => 1, + 'name' => 'Alice', + 'active' => true, +]); +``` + +## Advanced Usage + +### Metadata Parsing + +```php +use Substrate\ScaleCodec\Metadata\MetadataParser; + +$parser = new MetadataParser(); +$metadata = $parser->parse($metadataHex); + +// Access pallets +$systemPallet = $metadata->getPallet('System'); + +// Get events +$events = $metadata->getPalletEvents('Balances'); +``` + +### Extrinsic Building + +```php +use Substrate\ScaleCodec\Extrinsic\ExtrinsicBuilder; + +$builder = new ExtrinsicBuilder($registry); +$extrinsic = $builder + ->setVersion(4) + ->setPallet('Balances') + ->setFunction('transfer') + ->setArgs([ + 'dest' => $accountId, + 'value' => 1000000000, + ]) + ->sign($keypair) + ->build(); +``` + +### Working with Large Integers + +U64 and U128 return strings for values exceeding PHP_INT_MAX: + +```php +$u64 = $registry->get('U64'); + +// Max U64 value +$encoded = $u64->encode('18446744073709551615'); +$decoded = $u64->decode($bytes); +echo $decoded; // '18446744073709551615' (string) +``` + +## Documentation + +- [API Reference](docs/API.md) - Complete API documentation +- [Types Reference](docs/TYPES.md) - Detailed type documentation +- [Static Analysis](docs/STATIC_ANALYSIS.md) - PHPStan configuration + +## Testing ```bash -composer require gmajor/substrate-codec-php +# Run unit tests +make test + +# Run with coverage +make coverage + +# Run compatibility tests +make compat + +# Run static analysis +make stan + +# Run code style check +make sniff ``` -## Requirements +## Compatibility -- PHP 8.2+ -- ext-gmp -- ext-json -- ext-sodium +This library maintains compatibility with [polkadot.js SCALE codec](https://polkadot.js.org/docs/api/). Run compatibility tests: + +```bash +php compat/tests/php-compatibility-test.php +``` + +## Benchmarks + +```bash +make bench # Standard benchmarks +make bench-quick # Quick benchmarks +make bench-full # Full benchmarks +``` + +## Contributing + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/my-feature` +3. Run tests: `make test` +4. Run static analysis: `make stan` +5. Run code style: `make sniff` +6. Commit changes: `git commit -am "feat: my feature"` +7. Push and create a PR ## License -MIT License +MIT License. See [LICENSE](LICENSE) for details. + +## Related + +- [SCALE Codec Specification](https://docs.substrate.io/reference/scale-codec/) +- [Substrate Documentation](https://docs.substrate.io/) +- [polkadot.js API](https://polkadot.js.org/docs/api/) diff --git a/docs/BEST_PRACTICES.md b/docs/BEST_PRACTICES.md new file mode 100644 index 0000000..1920749 --- /dev/null +++ b/docs/BEST_PRACTICES.md @@ -0,0 +1,357 @@ +# Best Practices Guide + +This guide covers best practices for using php-scale-codec effectively. + +## Table of Contents + +1. [Type Handling](#type-handling) +2. [Memory Management](#memory-management) +3. [Error Handling](#error-handling) +4. [Performance](#performance) +5. [Security](#security) +6. [Testing](#testing) + +--- + +## Type Handling + +### Use TypeRegistry for Type Lookup + +```php +// Good: Centralize type management +$registry = new TypeRegistry(); +$u32 = $registry->get('U32'); + +// Avoid: Creating types directly +$u32 = new U32Type(); +``` + +### Handle Large Integers Properly + +```php +// U64/U128 may exceed PHP_INT_MAX +$u64 = $registry->get('U64'); + +// Good: Use string for large values +$encoded = $u64->encode('18446744073709551615'); + +// Good: Decode returns string for large values +$value = $u64->decode($bytes); // Returns string, not int + +// Use GMP for arithmetic on large values +$sum = gmp_add($value, '1000'); +``` + +### Type Validation + +```php +// Validate before encoding +if ($u8->isValid($value)) { + $encoded = $u8->encode($value); +} else { + throw new InvalidArgumentException("Invalid U8 value: $value"); +} +``` + +--- + +## Memory Management + +### Reuse ScaleBytes for Large Data + +```php +// Good: Process in chunks for large data +$bytes = ScaleBytes::fromHex($largeHexString); +while (!$bytes->isEmpty()) { + $chunk = $bytes->readBytes(1024); + // Process chunk +} + +// Avoid: Loading entire data into memory +$allData = $bytes->readAllBytes(); +``` + +### Reset ScaleBytes for Reuse + +```php +$bytes = ScaleBytes::fromHex('0x010203'); + +// Read some bytes +$first = $bytes->readBytes(2); // [1, 2] + +// Reset to beginning if needed +$bytes->reset(); +$again = $bytes->readBytes(2); // [1, 2] again +``` + +--- + +## Error Handling + +### Use Specific Exceptions + +```php +use Substrate\ScaleCodec\Exception\ScaleEncodeException; +use Substrate\ScaleCodec\Exception\ScaleDecodeException; +use Substrate\ScaleCodec\Exception\InvalidTypeException; + +try { + $encoded = $type->encode($data); +} catch (ScaleEncodeException $e) { + // Handle encoding error + error_log("Encoding failed: " . $e->getMessage()); +} catch (InvalidTypeException $e) { + // Handle type mismatch + error_log("Invalid type: " . $e->getMessage()); +} +``` + +### Validate Input Data + +```php +function encodeAccountBalance($balance): ScaleBytes { + if (!is_string($balance) && !is_int($balance)) { + throw new InvalidArgumentException("Balance must be string or int"); + } + + if (gmp_cmp($balance, '0') < 0) { + throw new InvalidArgumentException("Balance cannot be negative"); + } + + return $this->u128->encode($balance); +} +``` + +--- + +## Performance + +### Cache Type Instances + +```php +// Good: Cache frequently used types +class ScaleService { + private TypeRegistry $registry; + private ?ScaleTypeInterface $cachedU32 = null; + + public function encodeU32(int $value): ScaleBytes { + if ($this->cachedU32 === null) { + $this->cachedU32 = $this->registry->get('U32'); + } + return $this->cachedU32->encode($value); + } +} +``` + +### Batch Operations + +```php +// Good: Batch multiple operations +$encoded = ''; +foreach ($items as $item) { + $encoded .= $type->encode($item)->toBytes(); +} + +// Or use Vec for collections +$vec = (new VecType($registry))->setElementType($type); +$encoded = $vec->encode($items); +``` + +### Use Compact for Large Integers + +```php +// Compact encoding is more efficient for small-to-medium values +$compact = $registry->get('Compact'); + +// 0-63: 1 byte +// 64-16383: 2 bytes +// 16384-1073741823: 4 bytes +// >1073741823: variable + +$encoded = $compact->encode(42); // Only 1 byte +``` + +--- + +## Security + +### Never Hardcode Private Keys + +```php +// Bad: Hardcoded private key +$keypair = Keypair::fromSeed('0x1234...'); + +// Good: Load from environment +$seed = getenv('SUBSTRATE_SEED'); +$keypair = Keypair::fromSeed($seed); + +// Better: Use hardware wallet or key management service +``` + +### Validate External Input + +```php +function decodeUserInput(string $hexInput) { + // Validate hex format + if (!preg_match('/^0x[0-9a-fA-F]+$/', $hexInput)) { + throw new InvalidArgumentException("Invalid hex format"); + } + + // Limit input size + if (strlen($hexInput) > 100000) { + throw new InvalidArgumentException("Input too large"); + } + + return $this->decode(ScaleBytes::fromHex($hexInput)); +} +``` + +### Use Secure Random for Nonces + +```php +// Good: Use secure random +$nonce = random_int(0, PHP_INT_MAX); + +// Bad: Predictable random +$nonce = rand(); +``` + +--- + +## Testing + +### Unit Test All Custom Encodings + +```php +class MyTypeTest extends TestCase { + public function testEncodeDecode(): void { + $type = new MyCustomType(); + + $original = ['field1' => 123, 'field2' => 'test']; + $encoded = $type->encode($original); + $decoded = $type->decode(ScaleBytes::fromHex($encoded->toHex())); + + $this->assertEquals($original, $decoded); + } +} +``` + +### Use Compatibility Tests + +```php +// Compare against known-good polkadot.js output +public function testCompatibilityWithPolkadotJs(): void { + $knownGood = '0x12345678'; // From polkadot.js + + $type = $this->registry->get('U32'); + $encoded = $type->encode(305419896); + + $this->assertEquals($knownGood, $encoded->toHex()); +} +``` + +### Test Edge Cases + +```php +public function testEdgeCases(): void { + $u8 = $this->registry->get('U8'); + + // Min value + $this->assertEquals('0x00', $u8->encode(0)->toHex()); + + // Max value + $this->assertEquals('0xff', $u8->encode(255)->toHex()); + + // Overflow should throw + $this->expectException(ScaleEncodeException::class); + $u8->encode(256); +} +``` + +--- + +## Common Patterns + +### Builder Pattern for Complex Types + +```php +class CallBuilder { + private StructType $callType; + private array $data = []; + + public function __construct(TypeRegistry $registry) { + $this->callType = (new StructType($registry)) + ->setFields([ + 'palletIndex' => $registry->get('U8'), + 'callIndex' => $registry->get('U8'), + 'args' => $registry->get('Bytes'), + ]); + } + + public function setPallet(int $index): self { + $this->data['palletIndex'] = $index; + return $this; + } + + public function setCall(int $index): self { + $this->data['callIndex'] = $index; + return $this; + } + + public function setArgs(array $args): self { + $this->data['args'] = $args; + return $this; + } + + public function build(): ScaleBytes { + return $this->callType->encode($this->data); + } +} +``` + +### Factory Pattern for Type Creation + +```php +class TypeFactory { + private TypeRegistry $registry; + + public function createVec(string $elementType): VecType { + return (new VecType($this->registry)) + ->setElementType($this->registry->get($elementType)); + } + + public function createOption(string $innerType): OptionType { + return (new OptionType($this->registry)) + ->setInnerType($this->registry->get($innerType)); + } +} +``` + +--- + +## Debugging Tips + +### Enable Verbose Logging + +```php +// Log encoding operations +$encoded = $type->encode($value); +error_log("Encoded {$value} to " . $encoded->toHex()); + +// Log decoding operations +$bytes = ScaleBytes::fromHex($hex); +error_log("Decoding from position " . $bytes->getPosition()); +$decoded = $type->decode($bytes); +error_log("Decoded to " . json_encode($decoded)); +``` + +### Inspect Byte Representation + +```php +$bytes = ScaleBytes::fromHex('0x0102030405'); + +// Get remaining bytes +while (!$bytes->isEmpty()) { + echo sprintf("Position %d: 0x%02x\n", $bytes->getPosition(), $bytes->readByte()); +} +``` diff --git a/examples/01-basic-encoding.php b/examples/01-basic-encoding.php new file mode 100644 index 0000000..93b6858 --- /dev/null +++ b/examples/01-basic-encoding.php @@ -0,0 +1,102 @@ +get('U8'); +echo "U8 encode(255): " . $u8->encode(255)->toHex() . "\n"; +echo "U8 encode(0): " . $u8->encode(0)->toHex() . "\n"; + +// U32 (32-bit unsigned) +$u32 = $registry->get('U32'); +echo "U32 encode(12345): " . $u32->encode(12345)->toHex() . "\n"; + +// U64 (64-bit unsigned) - Note: returns string for large values +$u64 = $registry->get('U64'); +echo "U64 encode('18446744073709551615'): " . $u64->encode('18446744073709551615')->toHex() . "\n"; + +// ============================================ +// Signed Integers +// ============================================ +echo "\n--- Signed Integers ---\n\n"; + +$i8 = $registry->get('I8'); +echo "I8 encode(-1): " . $i8->encode(-1)->toHex() . "\n"; +echo "I8 encode(-128): " . $i8->encode(-128)->toHex() . "\n"; +echo "I8 encode(127): " . $i8->encode(127)->toHex() . "\n"; + +$i32 = $registry->get('I32'); +echo "I32 encode(-1): " . $i32->encode(-1)->toHex() . "\n"; + +// ============================================ +// Boolean +// ============================================ +echo "\n--- Boolean ---\n\n"; + +$bool = $registry->get('Bool'); +echo "Bool encode(true): " . $bool->encode(true)->toHex() . "\n"; +echo "Bool encode(false): " . $bool->encode(false)->toHex() . "\n"; + +// ============================================ +// String/Text +// ============================================ +echo "\n--- String ---\n\n"; + +$string = $registry->get('String'); +echo "String encode('Hello'): " . $string->encode('Hello')->toHex() . "\n"; +echo "String encode('区块链'): " . $string->encode('区块链')->toHex() . "\n"; + +// ============================================ +// Compact Integers +// ============================================ +echo "\n--- Compact Integers ---\n\n"; + +$compact = $registry->get('Compact'); +echo "Compact encode(0): " . $compact->encode(0)->toHex() . " (1 byte)\n"; +echo "Compact encode(42): " . $compact->encode(42)->toHex() . " (1 byte)\n"; +echo "Compact encode(63): " . $compact->encode(63)->toHex() . " (1 byte, max)\n"; +echo "Compact encode(64): " . $compact->encode(64)->toHex() . " (2 bytes)\n"; +echo "Compact encode(16383): " . $compact->encode(16383)->toHex() . " (2 bytes, max)\n"; +echo "Compact encode(16384): " . $compact->encode(16384)->toHex() . " (4 bytes)\n"; + +// ============================================ +// Decoding Examples +// ============================================ +echo "\n--- Decoding Examples ---\n\n"; + +// Decode U32 +$bytes = ScaleBytes::fromHex('0x39300000'); +$decoded = $u32->decode($bytes); +echo "U32 decode('0x39300000'): " . $decoded . "\n"; + +// Decode String +$bytes = ScaleBytes::fromHex('0x1448656c6c6f'); +$decoded = $string->decode($bytes); +echo "String decode('0x1448656c6c6f'): " . $decoded . "\n"; + +// Decode Compact +$bytes = ScaleBytes::fromHex('0xa8'); +$decoded = $compact->decode($bytes); +echo "Compact decode('0xa8'): " . $decoded . "\n"; + +echo "\n=== Examples Complete ===\n"; diff --git a/examples/02-compound-types.php b/examples/02-compound-types.php new file mode 100644 index 0000000..6d26ca4 --- /dev/null +++ b/examples/02-compound-types.php @@ -0,0 +1,132 @@ + - Vector/Array +// ============================================ +echo "--- Vec (Vector) ---\n\n"; + +// Vec +$vecU8 = (new VecType($registry))->setElementType($registry->get('U8')); + +$encoded = $vecU8->encode([1, 2, 3, 4, 5]); +echo "Vec encode([1,2,3,4,5]): " . $encoded->toHex() . "\n"; + +$decoded = $vecU8->decode(ScaleBytes::fromHex('0x140102030405')); +echo "Vec decode: " . json_encode($decoded) . "\n"; + +// Vec +$vecU32 = (new VecType($registry))->setElementType($registry->get('U32')); +$encoded = $vecU32->encode([0, 1, 2, 3]); +echo "Vec encode([0,1,2,3]): " . $encoded->toHex() . "\n"; + +// Empty vector +$encoded = $vecU8->encode([]); +echo "Vec encode([]): " . $encoded->toHex() . " (empty)\n"; + +// ============================================ +// Option - Optional Value +// ============================================ +echo "\n--- Option ---\n\n"; + +$optionU8 = (new OptionType($registry))->setInnerType($registry->get('U8')); + +// Some value +$encoded = $optionU8->encode(42); +echo "Option encode(42): " . $encoded->toHex() . " (Some)\n"; + +// None value +$encoded = $optionU8->encode(null); +echo "Option encode(null): " . $encoded->toHex() . " (None)\n"; + +// Option with decode +$decoded = $optionU8->decode(ScaleBytes::fromHex('0x012a')); +echo "Option decode('0x012a'): " . json_encode($decoded) . "\n"; + +$decoded = $optionU8->decode(ScaleBytes::fromHex('0x00')); +echo "Option decode('0x00'): " . json_encode($decoded) . " (null)\n"; + +// ============================================ +// Tuple - Fixed-size heterogeneous sequence +// ============================================ +echo "\n--- Tuple ---\n\n"; + +$tuple = (new TupleType($registry)) + ->addElementType($registry->get('U8')) + ->addElementType($registry->get('U32')) + ->addElementType($registry->get('Bool')); + +$encoded = $tuple->encode([255, 1000000, true]); +echo "Tuple(U8,U32,Bool) encode([255,1000000,true]): " . $encoded->toHex() . "\n"; + +$decoded = $tuple->decode(ScaleBytes::fromHex('0xff40420f01')); +echo "Tuple decode: " . json_encode($decoded) . "\n"; + +// ============================================ +// Struct - Named fields +// ============================================ +echo "\n--- Struct ---\n\n"; + +$personStruct = (new StructType($registry))->setFields([ + 'id' => $registry->get('U32'), + 'name' => $registry->get('String'), + 'active' => $registry->get('Bool'), +]); + +$person = [ + 'id' => 1, + 'name' => 'Alice', + 'active' => true, +]; + +$encoded = $personStruct->encode($person); +echo "Struct encode({id:1, name:'Alice', active:true}): " . $encoded->toHex() . "\n"; + +$decoded = $personStruct->decode(ScaleBytes::fromHex($encoded->toHex())); +echo "Struct decode: " . json_encode($decoded) . "\n"; + +// ============================================ +// Enum - Tagged union +// ============================================ +echo "\n--- Enum ---\n\n"; + +$resultEnum = new EnumType($registry); +$resultEnum->addVariant('Ok', 0, $registry->get('U32')); +$resultEnum->addVariant('Err', 1, $registry->get('String')); + +// Ok variant +$encoded = $resultEnum->encode(['Ok' => 42]); +echo "Enum encode({Ok: 42}): " . $encoded->toHex() . "\n"; + +// Err variant +$encoded = $resultEnum->encode(['Err' => 'error message']); +echo "Enum encode({Err: 'error message'}): " . $encoded->toHex() . "\n"; + +// Unit variant +$optionEnum = new EnumType($registry); +$optionEnum->addVariant('None', 0); +$optionEnum->addVariant('Some', 1, $registry->get('U32')); + +$encoded = $optionEnum->encode(['None' => null]); +echo "Enum encode({None}): " . $encoded->toHex() . "\n"; + +echo "\n=== Compound Types Complete ===\n"; diff --git a/examples/03-metadata.php b/examples/03-metadata.php new file mode 100644 index 0000000..078af45 --- /dev/null +++ b/examples/03-metadata.php @@ -0,0 +1,128 @@ +call('state_getMetadata'); + +$parser = new MetadataParser(); +$metadata = $parser->parse($metadataHex); + +// Access pallets +$pallets = $metadata->getPallets(); +foreach ($pallets as $pallet) { + echo "Pallet: " . $pallet->getName() . "\n"; +} + +// Get specific pallet +$systemPallet = $metadata->getPallet('System'); + +// Get events for a pallet +$events = $metadata->getPalletEvents('Balances'); + +// Get calls for a pallet +$calls = $metadata->getPalletCalls('Balances'); + +// Get storage items +$storage = $metadata->getPalletStorage('System'); +CODE; + +echo "\n\n"; + +// ============================================ +// Type Registry Usage +// ============================================ +echo "--- Type Registry ---\n\n"; + +$registry = new TypeRegistry(); + +// Register custom types +echo "Registering custom types:\n"; +echo <<< 'CODE' +// Register a simple alias +$registry->register('AccountId', $registry->get('Bytes32')); + +// Register a complex type +$registry->register('Balance', $registry->get('U128')); +CODE; + +echo "\n\n"; + +// ============================================ +// Type Factory +// ============================================ +echo "--- Type Factory ---\n\n"; + +echo "Creating parameterized types:\n"; +echo <<< 'CODE' +use Substrate\ScaleCodec\Types\TypeFactory; + +$factory = new TypeFactory($registry); + +// Create Vec +$vecU8 = $factory->create('Vec'); + +// Create Option +$optionU32 = $factory->create('Option'); + +// Create complex nested type +$complexType = $factory->create('Option>'); + +// Create tuple +$tuple = $factory->create('(U8, U32, Bool)'); +CODE; + +echo "\n\n"; + +// ============================================ +// Practical Example: Balance Transfer +// ============================================ +echo "--- Practical: Balance Transfer Call ---\n\n"; + +echo "Encoding a balance transfer call:\n"; +echo <<< 'CODE' +// From metadata, get the Balances.transfer call +$transferCall = $metadata->getCall('Balances', 'transfer'); + +// Build call data +$callData = [ + 'callIndex' => [0x05, 0x00], // Balances pallet, transfer function + 'args' => [ + 'dest' => $recipientAccountId, + 'value' => 1000000000, // 1 DOT (in plancks) + ], +]; + +// Encode the call +$encoded = $transferCall->encode($callData); +CODE; + +echo "\n\n=== Metadata Examples Complete ===\n"; diff --git a/examples/04-extrinsic.php b/examples/04-extrinsic.php new file mode 100644 index 0000000..2022ba9 --- /dev/null +++ b/examples/04-extrinsic.php @@ -0,0 +1,164 @@ +setVersion(4) // Extrinsic version + ->setPallet('Balances') // Target pallet + ->setFunction('transfer') // Function name + ->setArgs([ + 'dest' => '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + 'value' => 1000000000, // 1 DOT + ]) + ->build(); + +echo "Unsigned extrinsic: " . $extrinsic->toHex() . "\n"; +CODE; + +echo "\n\n"; + +// ============================================ +// Building a Signed Extrinsic +// ============================================ +echo "--- Signed Extrinsic ---\n\n"; + +echo "Building a signed extrinsic:\n"; +echo <<< 'CODE' +use Substrate\ScaleCodec\Crypto\Keypair; +use Substrate\ScaleCodec\Crypto\Sr25519; + +// Create or load keypair +$keypair = Keypair::fromMnemonic('your mnemonic phrase here'); +// Or from seed: +// $keypair = Keypair::fromSeed($seedBytes); + +// Get chain metadata and genesis hash +$genesisHash = '0x...'; // Chain's genesis hash +$runtimeVersion = 100; // Current runtime version + +$builder = new ExtrinsicBuilder($registry); + +$extrinsic = $builder + ->setVersion(4) + ->setPallet('Balances') + ->setFunction('transfer') + ->setArgs([ + 'dest' => '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + 'value' => 1000000000, + ]) + ->setSigner($keypair->getPublicKey()) + ->setEra(['period' => 64, 'blockNumber' => $currentBlock]) + ->setNonce($accountNonce) + ->setTip(0) + ->sign($keypair, $genesisHash, $runtimeVersion) + ->build(); + +echo "Signed extrinsic: " . $extrinsic->toHex() . "\n"; +CODE; + +echo "\n\n"; + +// ============================================ +// Transaction Era +// ============================================ +echo "--- Transaction Era (Mortality) ---\n\n"; + +echo "Setting transaction validity period:\n"; +echo <<< 'CODE' +// Immortal (valid forever - not recommended for production) +$builder->setEra(null); + +// Mortal (valid for N blocks from a starting block) +$builder->setEra([ + 'period' => 64, // Valid for 64 blocks + 'blockNumber' => 1000, // Starting from block 1000 +]); +CODE; + +echo "\n\n"; + +// ============================================ +// Batch Transactions +// ============================================ +echo "--- Batch Transactions ---\n\n"; + +echo "Building a batch call:\n"; +echo <<< 'CODE' +// Create batch call with multiple transfers +$batchBuilder = new ExtrinsicBuilder($registry); + +$extrinsic = $batchBuilder + ->setVersion(4) + ->setPallet('Utility') + ->setFunction('batch') + ->setArgs([ + 'calls' => [ + ['Balances', 'transfer', ['dest' => $addr1, 'value' => 1000]], + ['Balances', 'transfer', ['dest' => $addr2, 'value' => 2000]], + ['Balances', 'transfer', ['dest' => $addr3, 'value' => 3000]], + ], + ]) + ->sign($keypair, $genesisHash, $runtimeVersion) + ->build(); +CODE; + +echo "\n\n"; + +// ============================================ +// Submit to Chain +// ============================================ +echo "--- Submitting to Chain ---\n\n"; + +echo "After building the extrinsic, submit via RPC:\n"; +echo <<< 'CODE' +// Using WebSocket RPC +$client = new WebSocketClient('wss://rpc.polkadot.io'); + +// Submit and wait for inclusion +$result = $client->call('author_submitExtrinsic', [$extrinsic->toHex()]); +echo "Extrinsic hash: " . $result . "\n"; + +// Or watch for events +$client->subscribe('author_submitAndWatchExtrinsic', [$extrinsic->toHex()], function($event) { + if ($event['finalized']) { + echo "Transaction finalized in block: " . $event['finalized'] . "\n"; + } +}); +CODE; + +echo "\n\n=== Extrinsic Examples Complete ===\n"; diff --git a/readme.md b/readme.md deleted file mode 100644 index 7511400..0000000 --- a/readme.md +++ /dev/null @@ -1,88 +0,0 @@ -# php-scale-codec v2.0 - -PHP SCALE Codec for Substrate - Version 2.0 - -## Architecture Overview - -### Directory Structure - -``` -src/ -├── Bytes/ # Byte manipulation utilities -│ └── ScaleBytes.php -├── Encoder/ # Encoder interfaces -│ └── EncoderInterface.php -├── Decoder/ # Decoder interfaces -│ └── DecoderInterface.php -├── Types/ # Type definitions and implementations -│ ├── ScaleType.php # PHP 8.2 enum for SCALE types -│ ├── TypeInterface.php # Core type interface -│ ├── AbstractType.php # Base type implementation -│ ├── TypeRegistry.php # Type registration and lookup -│ ├── TypeFactory.php # Type creation factory -│ ├── BoolType.php -│ └── NullType.php -├── Metadata/ # Metadata parsing (Phase 3) -├── Extrinsic/ # Extrinsic handling (Phase 3) -└── Exception/ # Exception classes - ├── ScaleEncodeException.php - ├── ScaleDecodeException.php - └── InvalidTypeException.php - -tests/ -├── Bytes/ -│ └── ScaleBytesTest.php -└── Types/ - ├── ScaleTypeTest.php - └── TypeRegistryTest.php -``` - -## Key Components - -### ScaleType Enum (PHP 8.2) - -```php -use Substrate\ScaleCodec\Types\ScaleType; - -$type = ScaleType::U32; -$type->getByteSize(); // 4 -$type->isUnsignedInt(); // true -``` - -### TypeRegistry - -```php -use Substrate\ScaleCodec\Types\{TypeRegistry, BoolType}; - -$registry = new TypeRegistry(); -$registry->register('bool', new BoolType($registry)); -$boolType = $registry->get('bool'); -``` - -### ScaleBytes - -```php -use Substrate\ScaleCodec\Bytes\ScaleBytes; - -$bytes = ScaleBytes::fromHex('0x01020304'); -$bytes->readBytes(2); // [1, 2] -$bytes->remaining(); // 2 -$bytes->toHex(); // '0x01020304' -``` - -## Installation - -```bash -composer require gmajor/substrate-codec-php -``` - -## Requirements - -- PHP 8.2+ -- ext-gmp -- ext-json -- ext-sodium - -## License - -MIT License diff --git a/src/Codec/interfaces/readme.md b/src/Codec/interfaces/readme.md deleted file mode 100644 index c7ffefa..0000000 --- a/src/Codec/interfaces/readme.md +++ /dev/null @@ -1,12 +0,0 @@ -## substrate pallets custom type support - -This includes all pallets of substrate and parachain, bridges, and evm pallets custom type. -Reference https://github.com/polkadot-js/api/tree/master/packages/types/src/interfaces - - -## Resources -- [substrate](https://github.com/paritytech/substrate/tree/master/frame) -- [parity-bridges-common](https://github.com/paritytech/parity-bridges-common) -- [polkadot-common](https://github.com/paritytech/polkadot/tree/master/runtime/common/src) -- [frontier](hhttps://github.com/paritytech/frontier) -- [Polkadot.js](https://github.com/polkadot-js/api)