266 lines
11 KiB
PHP
266 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
spl_autoload_register(static function (string $class): void {
|
|
$prefix = 'Contractless\\Rpc\\';
|
|
if (!str_starts_with($class, $prefix)) {
|
|
return;
|
|
}
|
|
$path = dirname(__DIR__) . '/src/'
|
|
. str_replace('\\', '/', substr($class, strlen($prefix)))
|
|
. '.php';
|
|
if (is_file($path)) {
|
|
require $path;
|
|
}
|
|
});
|
|
|
|
use Contractless\Rpc\Client;
|
|
use Contractless\Rpc\Crypto\CallbackCrypto;
|
|
use Contractless\Rpc\Exception\ProtocolException;
|
|
use Contractless\Rpc\Exception\TransportException;
|
|
use Contractless\Rpc\Protocol\Address;
|
|
use Contractless\Rpc\Protocol\Command;
|
|
use Contractless\Rpc\Protocol\HandshakeProof;
|
|
use Contractless\Rpc\Signing\CallbackSignatureProvider;
|
|
use Contractless\Rpc\Transport\EndpointPoolTransport;
|
|
use Contractless\Rpc\Transport\TransportInterface;
|
|
use Contractless\Rpc\Transaction\TransferBuilder;
|
|
use Contractless\Rpc\Transaction\AssetBuilder;
|
|
use Contractless\Rpc\Transaction\GovernanceBuilder;
|
|
use Contractless\Rpc\Transaction\StorageBuilder;
|
|
use Contractless\Rpc\Transaction\MiscellaneousBuilder;
|
|
use Contractless\Rpc\Transaction\AgreementBuilder;
|
|
use Contractless\Rpc\Transaction\DualSignedTransaction;
|
|
|
|
function expect(bool $condition, string $message): void
|
|
{
|
|
if (!$condition) {
|
|
throw new RuntimeException($message);
|
|
}
|
|
}
|
|
|
|
final class TestTransport implements TransportInterface
|
|
{
|
|
public int $calls = 0;
|
|
|
|
public function __construct(
|
|
private readonly ?string $reply = null,
|
|
private readonly ?Throwable $failure = null,
|
|
) {
|
|
}
|
|
|
|
public function exchange(
|
|
string $handshake,
|
|
string $request,
|
|
string $uid,
|
|
Closure $validateHandshake,
|
|
): string {
|
|
$this->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 $signature, string $publicKey): bool => true,
|
|
);
|
|
$signer = new CallbackSignatureProvider(
|
|
fn (string $digest): string => str_repeat("\x22", 666),
|
|
);
|
|
$handshakeProof = new HandshakeProof(
|
|
str_repeat("\x33", 897),
|
|
str_repeat("\x22", 666),
|
|
);
|
|
$client = new Client($transport, $crypto, $handshakeProof);
|
|
|
|
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->registerWallet(
|
|
str_repeat('ab', 20) . '.cltc',
|
|
$handshakeProof->publicKey,
|
|
str_repeat('22', 666),
|
|
);
|
|
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, $signer))->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, $signer);
|
|
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, $signer);
|
|
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, $signer);
|
|
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, $signer);
|
|
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, $signer);
|
|
$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, $signer);
|
|
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, $signer);
|
|
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, $handshakeProof);
|
|
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, $handshakeProof);
|
|
$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, $handshakeProof);
|
|
$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";
|