calls++; if ($this->failure !== null) { throw $this->failure; } return $this->reply ?? ''; } } expect( Address::toBytes(str_repeat('ab', 20) . '.cltc') === str_repeat("\xab", 20) . ".\x02", 'Short-address encoding differs from the Rust wallet format.', ); $transport = new class implements TransportInterface { public string $handshake = ''; public string $request = ''; public function exchange( string $handshake, string $request, string $uid, Closure $validateHandshake, ): string { $this->handshake = $handshake; $this->request = $request; return 'ok'; } }; $crypto = new CallbackCrypto( fn (string $message): string => str_repeat("\x11", 32), fn (string $message, string $privateKey, string $publicKey): string => str_repeat("\x22", 666), fn (string $message, string $signature, string $publicKey): bool => true, ); $credentials = new Credentials(str_repeat("\x33", 897), str_repeat("\x44", 1281)); $client = new Client($transport, $crypto, $credentials); expect($client->blockByHeight(42) === 'ok', 'Client did not return the RPC payload.'); expect(strlen($transport->handshake) === 1587, 'Handshake request has the wrong size.'); expect(substr($transport->handshake, 0, 2) === "\xac\xed", 'Handshake marker is wrong.'); expect(ord($transport->request[0]) === Command::BLOCK_BY_HEIGHT, 'RPC command is wrong.'); expect(unpack('Vheight', substr($transport->request, 4, 4))['height'] === 42, 'Height encoding is wrong.'); $client->storageLookupCost(str_repeat('01', 32), 'all', str_repeat('ab', 20) . '.cltc'); expect(ord($transport->request[0]) === Command::STORAGE_LOOKUP_COST, 'Storage command is wrong.'); expect(strlen($transport->request) === 4 + 32 + 50 + 22, 'Storage cost payload has the wrong size.'); $client->registerOwnedWallet(str_repeat('ab', 20) . '.cltc'); expect(ord($transport->request[0]) === Command::REGISTER_WALLET, 'Wallet registration command is wrong.'); expect(strlen($transport->request) === 4 + 22 + 897 + 666, 'Wallet registration payload has the wrong size.'); $transfer = (new TransferBuilder($crypto, $credentials))->create( str_repeat('ab', 20) . '.cltc', str_repeat('cd', 20) . '.cltc', 'CLTC', 2_500_000, 2_500, timestamp: 1_700_000_000, ); expect(strlen($transfer->bytes) === 750, 'Transfer transaction has the wrong byte size.'); expect( $transfer->unsignedJson === '{"txtype":2,"time":1700000000,"value":2500000,"coin":"CLTC ","nft_series":0,"sender":"' . str_repeat('ab', 20) . '.cltc","receiver":"' . str_repeat('cd', 20) . '.cltc","txfee":2500}', 'Transfer signing JSON does not match Rust struct serialization.', ); $address = str_repeat('ab', 20) . '.cltc'; $assets = new AssetBuilder($crypto, $credentials); expect(strlen($assets->createToken($address, 'TEST', 1000, true, 100, 1)->bytes) === 725, 'Token size is wrong.'); expect(strlen($assets->issueToken($address, 'TEST', 1000, 100, 1)->bytes) === 724, 'Token issue size is wrong.'); expect(strlen($assets->createNft($address, false, false, 'ART', 'cid', 1, 'description', 100, 1)->bytes) === 922, 'NFT size is wrong.'); expect(strlen($assets->burn($address, 'TEST', 0, 1, 100, 1)->bytes) === 728, 'Burn size is wrong.'); expect(strlen($assets->vanity($address, str_repeat('a', 20) . '.cltc', 100, 1)->bytes) === 723, 'Vanity size is wrong.'); $governance = new GovernanceBuilder($crypto, $credentials); expect(strlen($governance->proposal(str_repeat('01', 32), $address, 100, 1)->bytes) === 733, 'Proposal size is wrong.'); expect(strlen($governance->proposalVote(str_repeat('01', 32), $address, true, 100, 1)->bytes) === 734, 'Proposal vote size is wrong.'); expect(strlen($governance->activationVote(str_repeat('01', 32), str_repeat('02', 32), 'CLP-0001.md', $address, true, 100, 1)->bytes) === 866, 'Activation vote size is wrong.'); $storage = new StorageBuilder($crypto, $credentials); expect(strlen($storage->createKey($address, 100, 1)->bytes) === 701, 'Storage key size is wrong.'); expect(strlen($storage->boolean(str_repeat('01', 32), 'active', true, $address, 100, 1)->bytes) === 784, 'Storage bool size is wrong.'); expect(strlen($storage->u32(str_repeat('01', 32), 'count', 42, $address, 100, 1)->bytes) === 787, 'Storage u32 size is wrong.'); expect(strlen($storage->i64(str_repeat('01', 32), 'balance', -42, $address, 100, 1)->bytes) === 791, 'Storage i64 size is wrong.'); expect(strlen($storage->u128(str_repeat('01', 32), 'large', '340282366920938463463374607431768211455', $address, 100, 1)->bytes) === 799, 'Storage u128 size is wrong.'); expect(strlen($storage->i128(str_repeat('01', 32), 'negative', '-170141183460469231731687303715884105728', $address, 100, 1)->bytes) === 799, 'Storage i128 size is wrong.'); expect(bin2hex(substr($storage->u128(str_repeat('01', 32), 'large', '340282366920938463463374607431768211455', $address, 100, 1)->bytes, 87, 16)) === str_repeat('ff', 16), 'u128 maximum encoding is wrong.'); expect(strlen($storage->string(str_repeat('01', 32), 'bio', 'hello', $address, str_repeat('0', 64), 100, 1)->bytes) === 995, 'Storage string size is wrong.'); expect(strlen($storage->delete(str_repeat('01', 32), 'bio', $address, 100, 1)->bytes) === 783, 'Storage delete size is wrong.'); $misc = new MiscellaneousBuilder($crypto, $credentials); expect(strlen($misc->marketing(1, 'banner', 'keyword', 'website', 1, 1, 100, 100, $address, 100, 1)->bytes) === 861, 'Marketing size is wrong.'); expect(strlen($misc->collateralClaim(str_repeat('01', 32), $address, 100, 1)->bytes) === 733, 'Collateral claim size is wrong.'); expect(strlen($misc->loanPayment(1000, str_repeat('01', 32), $address, 100, 100, 1)->bytes) === 781, 'Loan payment size is wrong.'); $agreements = new AgreementBuilder($crypto, $credentials); $swap = $agreements->swap(1000, 'CLTC', 0, 100, 'TEST', 0, 200, $address, str_repeat('cd', 20) . '.cltc', 1, 2, 3, 4, 1); $swap = $swap->signNext($crypto, $credentials); expect(strlen($swap->toSignedTransaction()->bytes) === 1471, 'Swap size is wrong.'); expect(DualSignedTransaction::import($swap->export())->isComplete(), 'Swap export/import lost signatures.'); $loan = $agreements->loan('CLTC', 1000, $address, 'TEST', 2000, str_repeat('cd', 20) . '.cltc', 'm', 12, 100, 2, 300, 100, 1); $loan = $loan->signNext($crypto, $credentials); expect(strlen($loan->toSignedTransaction()->bytes) === 1492, 'Loan size is wrong.'); $failedEndpoint = new TestTransport( failure: new TransportException('Connection refused.'), ); $workingEndpoint = new TestTransport(reply: 'pool reply'); $endpointPool = new EndpointPoolTransport( [ 'offline-node' => $failedEndpoint, 'working-node' => $workingEndpoint, ], randomize: false, ); $pooledClient = new Client($endpointPool, $crypto, $credentials); expect( $pooledClient->blockHeight() === 'pool reply', 'Endpoint pool did not return the successful fallback reply.', ); expect($failedEndpoint->calls === 1, 'Endpoint pool did not try the first endpoint.'); expect($workingEndpoint->calls === 1, 'Endpoint pool did not try the fallback endpoint.'); expect( $endpointPool->lastSuccessfulEndpointId() === 'working-node', 'Endpoint pool did not record the endpoint that answered.', ); expect( $endpointPool->endpointIds() === ['offline-node', 'working-node'], 'Endpoint pool did not preserve its endpoint identifiers.', ); expect( $endpointPool->transportFor('working-node') === $workingEndpoint, 'Endpoint pool could not return a specifically requested transport.', ); $protocolFailure = new TestTransport( failure: new ProtocolException('Invalid RPC reply.'), ); $unusedEndpoint = new TestTransport(reply: 'must not run'); $protocolPool = new EndpointPoolTransport( [ 'invalid-node' => $protocolFailure, 'unused-node' => $unusedEndpoint, ], randomize: false, ); $protocolClient = new Client($protocolPool, $crypto, $credentials); $protocolRejected = false; try { $protocolClient->blockHeight(); } catch (ProtocolException) { $protocolRejected = true; } expect($protocolRejected, 'Endpoint pool swallowed a protocol error.'); expect( $unusedEndpoint->calls === 0, 'Endpoint pool retried a request after a protocol error.', ); $allFailedPool = new EndpointPoolTransport( [ 'offline-one' => new TestTransport( failure: new TransportException('First endpoint failed.'), ), 'offline-two' => new TestTransport( failure: new TransportException('Second endpoint failed.'), ), ], randomize: false, ); $allFailedClient = new Client($allFailedPool, $crypto, $credentials); $allFailedMessage = ''; try { $allFailedClient->blockHeight(); } catch (TransportException $error) { $allFailedMessage = $error->getMessage(); } expect( str_contains($allFailedMessage, 'offline-one') && str_contains($allFailedMessage, 'offline-two'), 'Endpoint pool failure did not identify every failed endpoint.', ); $emptyPoolRejected = false; try { new EndpointPoolTransport([]); } catch (InvalidArgumentException) { $emptyPoolRejected = true; } expect($emptyPoolRejected, 'Endpoint pool accepted an empty endpoint list.'); echo "All protocol tests passed.\n";