Remove wallet custody from PHP RPC

This commit is contained in:
viraladmin 2026-07-27 14:24:58 -06:00
parent 3e60837ec3
commit 2644759ce9
19 changed files with 228 additions and 179 deletions

128
README.md
View File

@ -20,7 +20,7 @@ The library exposes application-safe public RPC commands, including:
- Marketing records - Marketing records
- On-chain data storage - On-chain data storage
- Governance proposals - Governance proposals
- Transaction creation, signing, and broadcasting - Transaction creation and broadcasting with application-provided signatures
Miner relay commands, network mapping changes, monitor-state commands, node Miner relay commands, network mapping changes, monitor-state commands, node
setup synchronization, and server-owner IP controls are intentionally not setup synchronization, and server-owner IP controls are intentionally not
@ -29,14 +29,13 @@ exposed.
## Requirements ## Requirements
- PHP 8.1 or newer using a 64-bit build - PHP 8.1 or newer using a 64-bit build
- PHP GD
- PHP OpenSSL
- The `skein` module from Contractless PHP Modules - The `skein` module from Contractless PHP Modules
- The `oqsphp` module from Contractless PHP Modules - The `oqsphp` module from Contractless PHP Modules
Contractless uses the same Falcon key and signature sizes as Contractless uses the same Falcon key and signature sizes as
`Falcon-padded-512`, but its FN-DSA signing mode requires a small message `Falcon-padded-512`, but its FN-DSA signing mode requires a small message
adaptation. `NativeCrypto` performs that adaptation automatically. adaptation. `NativeCrypto` performs that adaptation when verifying node
responses. The application that owns a wallet remains responsible for signing.
Install and test the Contractless PHP Modules before installing this package: Install and test the Contractless PHP Modules before installing this package:
@ -44,10 +43,10 @@ https://contractless.dev/contractless/Contractless-PHP-Modules
## Installation ## Installation
Install version `0.3` through Packagist: Install version `1.0` through Packagist:
```bash ```bash
composer require contractless/contractless-php-rpc:^0.3.0 -W composer require contractless/contractless-php-rpc:^1.0.0 -W
``` ```
Composer installs the package under `vendor/` and generates the required Composer installs the package under `vendor/` and generates the required
@ -62,12 +61,12 @@ require __DIR__ . '/vendor/autoload.php';
use Contractless\Rpc\Client; use Contractless\Rpc\Client;
use Contractless\Rpc\Crypto\NativeCrypto; use Contractless\Rpc\Crypto\NativeCrypto;
use Contractless\Rpc\Protocol\HandshakeProof;
use Contractless\Rpc\Transport\StreamTransport; use Contractless\Rpc\Transport\StreamTransport;
use Contractless\Rpc\Wallet\Credentials;
$credentials = Credentials::fromWalletFile( $handshake = HandshakeProof::fromHex(
'/private/path/contractless.wallet', $applicationPublicKeyHex,
'wallet decryption key', $applicationHandshakeSignatureHex,
); );
$crypto = new NativeCrypto(); $crypto = new NativeCrypto();
@ -79,10 +78,14 @@ $client = new Client(
timeout: 10.0, timeout: 10.0,
), ),
$crypto, $crypto,
$credentials, $handshake,
); );
``` ```
The handshake signature is the application's 666-byte Falcon signature over
the 32-byte Skein-256 digest of `aced`. The RPC package does not create that
signature and never receives a wallet file, decryption key, or private key.
Use port `50050` for the default testnet RPC and `50055` for the default Use port `50050` for the default testnet RPC and `50055` for the default
mainnet RPC unless the node operator configured another port. mainnet RPC unless the node operator configured another port.
@ -124,7 +127,7 @@ $transport = new EndpointPoolTransport([
$client = new Client( $client = new Client(
$transport, $transport,
$crypto, $crypto,
$credentials, $handshake,
); );
``` ```
@ -155,7 +158,7 @@ $selectedTransport = $transport->transportFor($endpointId);
$selectedClient = new Client( $selectedClient = new Client(
$selectedTransport, $selectedTransport,
$crypto, $crypto,
$credentials, $handshake,
); );
``` ```
@ -296,17 +299,15 @@ $result = $client->validateMessage(
); );
``` ```
Register the wallet loaded into `$credentials`: Wallet registration requires the application to provide the public key and
registration signature:
```php ```php
$result = $client->registerOwnedWallet($address); $result = $client->registerWallet(
``` $address,
$publicKeyBytes,
The lower-level registration method is available when an application has $signatureHex,
already created the required signature: );
```php
$result = $client->registerWallet($address, $signatureHex);
``` ```
## Token, NFT, RWA, Swap, And Loan RPC ## Token, NFT, RWA, Swap, And Loan RPC
@ -395,10 +396,26 @@ including its voting and activation information.
## Create And Broadcast A Transfer ## Create And Broadcast A Transfer
Transaction builders receive an application-owned signature provider:
```php
use Contractless\Rpc\Signing\CallbackSignatureProvider;
$signer = new CallbackSignatureProvider(
function (string $digest) use ($applicationWallet): string {
return $applicationWallet->signDigest($digest);
},
);
```
The callback receives only the 32-byte transaction digest. How the application
loads, decrypts, stores, or remotely accesses its signing key is outside this
package. The callback must return a 666-byte raw Falcon signature.
```php ```php
use Contractless\Rpc\Transaction\TransferBuilder; use Contractless\Rpc\Transaction\TransferBuilder;
$transfer = (new TransferBuilder($crypto, $credentials))->create( $transfer = (new TransferBuilder($crypto, $signer))->create(
sender: $sender, sender: $sender,
receiver: $receiver, receiver: $receiver,
coin: 'CLTC', coin: 'CLTC',
@ -418,7 +435,7 @@ The same builder transfers base currency, tokens, NFTs, and RWAs. Set
```php ```php
use Contractless\Rpc\Transaction\AssetBuilder; use Contractless\Rpc\Transaction\AssetBuilder;
$assets = new AssetBuilder($crypto, $credentials); $assets = new AssetBuilder($crypto, $signer);
$createToken = $assets->createToken( $createToken = $assets->createToken(
creator: $address, creator: $address,
@ -471,7 +488,7 @@ signs the transaction:
```php ```php
use Contractless\Rpc\Transaction\AgreementBuilder; use Contractless\Rpc\Transaction\AgreementBuilder;
$agreements = new AgreementBuilder($crypto, $credentials); $agreements = new AgreementBuilder($crypto, $signer);
$swap = $agreements->swap( $swap = $agreements->swap(
expiration: time() + 3600, expiration: time() + 3600,
@ -514,8 +531,7 @@ $exported = json_encode(
); );
``` ```
The second party imports and signs it using their own `$crypto` and The second party imports and signs it using its own signature provider:
`$credentials`:
```php ```php
use Contractless\Rpc\Transaction\DualSignedTransaction; use Contractless\Rpc\Transaction\DualSignedTransaction;
@ -524,7 +540,7 @@ $swap = DualSignedTransaction::import(
json_decode($exported, true, flags: JSON_THROW_ON_ERROR), json_decode($exported, true, flags: JSON_THROW_ON_ERROR),
); );
$swap = $swap->signNext($crypto, $credentials); $swap = $swap->signNext($crypto, $secondPartySigner);
if ($swap->isComplete()) { if ($swap->isComplete()) {
$client->submitSignedTransaction($swap->toSignedTransaction()); $client->submitSignedTransaction($swap->toSignedTransaction());
@ -536,7 +552,7 @@ if ($swap->isComplete()) {
```php ```php
use Contractless\Rpc\Transaction\MiscellaneousBuilder; use Contractless\Rpc\Transaction\MiscellaneousBuilder;
$miscellaneous = new MiscellaneousBuilder($crypto, $credentials); $miscellaneous = new MiscellaneousBuilder($crypto, $signer);
$payment = $miscellaneous->loanPayment( $payment = $miscellaneous->loanPayment(
amount: 100_000_000, amount: 100_000_000,
@ -579,7 +595,7 @@ $client->submitSignedTransaction($marketing);
```php ```php
use Contractless\Rpc\Transaction\StorageBuilder; use Contractless\Rpc\Transaction\StorageBuilder;
$storage = new StorageBuilder($crypto, $credentials); $storage = new StorageBuilder($crypto, $signer);
$createKey = $storage->createKey( $createKey = $storage->createKey(
address: $address, address: $address,
@ -643,7 +659,7 @@ of the preceding string transaction.
```php ```php
use Contractless\Rpc\Transaction\GovernanceBuilder; use Contractless\Rpc\Transaction\GovernanceBuilder;
$governance = new GovernanceBuilder($crypto, $credentials); $governance = new GovernanceBuilder($crypto, $signer);
$proposal = $governance->proposal( $proposal = $governance->proposal(
proposalHash: $proposalDocumentHash, proposalHash: $proposalDocumentHash,
@ -687,42 +703,23 @@ $unsignedSigningJson = $transaction->unsignedJson;
Use `submitSignedTransaction()` to broadcast the exact bytes without converting Use `submitSignedTransaction()` to broadcast the exact bytes without converting
them to hexadecimal first. them to hexadecimal first.
## Wallet Files ## Wallet Custody
Load a normal Contractless wallet: This package does not read Contractless wallet files, decode encrypted private
key images, accept wallet decryption keys, or store private keys.
```php Applications are responsible for:
$credentials = Credentials::fromWalletFile(
'/private/path/contractless.wallet',
'wallet decryption key',
);
```
The loader: - Loading or obtaining their wallet material
- Protecting private keys and decryption keys
- Creating the handshake signature
- Creating transaction and registration signatures
- Supplying only completed signatures and public data to this package
- Decodes every supported Contractless private-key image orientation A faucet may keep its wallet loader in the faucet application. A browser wallet
- Verifies the encrypted payload HMAC may sign locally in the browser. A hosted application may use a hardware or
- Decrypts the Falcon private key remote signer. All three can use the same RPC package because the connection
- Proves that the public and private keys match layer does not control wallet custody.
- Verifies the wallet's canonical short address
Wallet files and decryption keys must remain outside public web directories,
logs, repositories, and client-visible configuration.
Verify a wallet before using it:
```bash
php vendor/contractless/contractless-php-rpc/examples/load_wallet.php \
/private/path/contractless.wallet \
'wallet decryption key'
```
Applications that already manage protected raw keys may load hexadecimal
Falcon keys directly:
```php
$credentials = Credentials::fromHex($publicKeyHex, $privateKeyHex);
```
## Direct Public Command Calls ## Direct Public Command Calls
@ -753,7 +750,7 @@ validate and encode the command payload correctly.
## Error Handling ## Error Handling
RPC transport, protocol, validation, and wallet failures throw exceptions: RPC transport, protocol, and validation failures throw exceptions:
```php ```php
try { try {
@ -763,8 +760,7 @@ try {
} }
``` ```
Do not expose wallet errors, filesystem paths, private material, or internal Do not expose internal node replies directly to public users.
node replies directly to public users.
## Testing ## Testing

View File

@ -5,9 +5,7 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": ">=8.1", "php": ">=8.1",
"ext-gd": "*",
"ext-json": "*", "ext-json": "*",
"ext-openssl": "*",
"ext-oqsphp": "*", "ext-oqsphp": "*",
"ext-skein": "*" "ext-skein": "*"
}, },

23
composer.lock generated Normal file
View File

@ -0,0 +1,23 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9177fad695a02a8cb027a5064822e3cb",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.1",
"ext-json": "*",
"ext-oqsphp": "*",
"ext-skein": "*"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

View File

@ -9,9 +9,9 @@ use Contractless\Rpc\Exception\ProtocolException;
use Contractless\Rpc\Protocol\Address; use Contractless\Rpc\Protocol\Address;
use Contractless\Rpc\Protocol\Binary; use Contractless\Rpc\Protocol\Binary;
use Contractless\Rpc\Protocol\Command; use Contractless\Rpc\Protocol\Command;
use Contractless\Rpc\Protocol\HandshakeProof;
use Contractless\Rpc\Protocol\RequestBuilder; use Contractless\Rpc\Protocol\RequestBuilder;
use Contractless\Rpc\Transport\TransportInterface; use Contractless\Rpc\Transport\TransportInterface;
use Contractless\Rpc\Wallet\Credentials;
final class Client final class Client
{ {
@ -26,7 +26,7 @@ final class Client
public function __construct( public function __construct(
private readonly TransportInterface $transport, private readonly TransportInterface $transport,
private readonly CryptoInterface $crypto, private readonly CryptoInterface $crypto,
private readonly Credentials $credentials, private readonly HandshakeProof $handshakeProof,
) { ) {
} }
@ -175,29 +175,21 @@ final class Client
return $this->call(Command::CONTRACT_BY_ADDRESS, Address::toBytes($address)); return $this->call(Command::CONTRACT_BY_ADDRESS, Address::toBytes($address));
} }
public function registerWallet(string $address, string $signature): string public function registerWallet(
string $address,
string $publicKey,
string $signature,
): string
{ {
Binary::length($publicKey, 897, 'Falcon public key');
return $this->call( return $this->call(
Command::REGISTER_WALLET, Command::REGISTER_WALLET,
Address::toBytes($address) Address::toBytes($address)
. $this->credentials->publicKey . $publicKey
. RequestBuilder::signature($signature), . RequestBuilder::signature($signature),
); );
} }
public function registerOwnedWallet(string $address): string
{
$signedPayload = Binary::u8(Command::REGISTER_WALLET)
. Address::toBytes($address)
. $this->credentials->publicKey;
$signature = $this->crypto->sign(
$this->crypto->skein256($signedPayload),
$this->credentials->privateKey,
$this->credentials->publicKey,
);
return $this->registerWallet($address, bin2hex($signature));
}
public function vanityLookup(string $address): string public function vanityLookup(string $address): string
{ {
return $this->call(Command::VANITY_LOOKUP, Address::toBytes($address)); return $this->call(Command::VANITY_LOOKUP, Address::toBytes($address));
@ -291,16 +283,9 @@ final class Client
private function buildHandshake(): string private function buildHandshake(): string
{ {
$digest = $this->crypto->skein256('aced');
$signature = $this->crypto->sign(
$digest,
$this->credentials->privateKey,
$this->credentials->publicKey,
);
return self::CLIENT_MARKER return self::CLIENT_MARKER
. $signature . $this->handshakeProof->signature
. $this->credentials->publicKey . $this->handshakeProof->publicKey
. Binary::u32(time()) . Binary::u32(time())
. str_repeat("\0", 18); . str_repeat("\0", 18);
} }

View File

@ -11,7 +11,6 @@ final class CallbackCrypto implements CryptoInterface
{ {
public function __construct( public function __construct(
private readonly Closure $skein, private readonly Closure $skein,
private readonly Closure $signer,
private readonly Closure $verifier, private readonly Closure $verifier,
) { ) {
} }
@ -25,15 +24,6 @@ final class CallbackCrypto implements CryptoInterface
return $digest; return $digest;
} }
public function sign(string $message, string $privateKey, string $publicKey): string
{
$signature = ($this->signer)($message, $privateKey, $publicKey);
if (!is_string($signature) || strlen($signature) !== 666) {
throw new ProtocolException('The signing callback must return exactly 666 raw bytes.');
}
return $signature;
}
public function verify(string $message, string $signature, string $publicKey): bool public function verify(string $message, string $signature, string $publicKey): bool
{ {
return (bool) ($this->verifier)($message, $signature, $publicKey); return (bool) ($this->verifier)($message, $signature, $publicKey);

View File

@ -9,9 +9,6 @@ interface CryptoInterface
/** Return the raw 32-byte Skein-256 digest. */ /** Return the raw 32-byte Skein-256 digest. */
public function skein256(string $message): string; public function skein256(string $message): string;
/** Sign raw Contractless message bytes and return a 666-byte signature. */
public function sign(string $message, string $privateKey, string $publicKey): string;
/** Verify raw Contractless message bytes against a 666-byte signature. */ /** Verify raw Contractless message bytes against a 666-byte signature. */
public function verify( public function verify(
string $message, string $message,

View File

@ -40,19 +40,6 @@ final class NativeCrypto implements CryptoInterface
return $digest; return $digest;
} }
public function sign(string $message, string $privateKey, string $publicKey): string
{
Binary::length($privateKey, 1281, 'Falcon private key');
Binary::length($publicKey, 897, 'Falcon public key');
$adapted = $this->adaptMessage($message, $publicKey);
$signature = '';
$status = $this->falcon->sign($signature, $adapted, $privateKey);
if ($status !== 0 || strlen($signature) !== 666) {
throw new ContractlessException('Falcon-padded-512 signing failed.');
}
return $signature;
}
public function verify(string $message, string $signature, string $publicKey): bool public function verify(string $message, string $signature, string $publicKey): bool
{ {
Binary::length($signature, 666, 'Falcon signature'); Binary::length($signature, 666, 'Falcon signature');

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Contractless\Rpc\Protocol;
final class HandshakeProof
{
public function __construct(
public readonly string $publicKey,
public readonly string $signature,
) {
Binary::length($publicKey, 897, 'Falcon public key');
Binary::length($signature, 666, 'Falcon handshake signature');
}
public static function fromHex(string $publicKey, string $signature): self
{
return new self(
Binary::hex($publicKey, 897, 'Falcon public key'),
Binary::hex($signature, 666, 'Falcon handshake signature'),
);
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Contractless\Rpc\Signing;
use Closure;
use Contractless\Rpc\Exception\ProtocolException;
final class CallbackSignatureProvider implements SignatureProviderInterface
{
public function __construct(private readonly Closure $signer)
{
}
public function sign(string $digest): string
{
if (strlen($digest) !== 32) {
throw new ProtocolException('Signing requires a 32-byte digest.');
}
$signature = ($this->signer)($digest);
if (!is_string($signature) || strlen($signature) !== 666) {
throw new ProtocolException(
'The signing callback must return exactly 666 raw bytes.',
);
}
return $signature;
}
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Contractless\Rpc\Signing;
interface SignatureProviderInterface
{
/** Sign a raw 32-byte Contractless digest and return 666 raw bytes. */
public function sign(string $digest): string;
}

View File

@ -8,13 +8,13 @@ use Contractless\Rpc\Crypto\CryptoInterface;
use Contractless\Rpc\Exception\ProtocolException; use Contractless\Rpc\Exception\ProtocolException;
use Contractless\Rpc\Protocol\Address; use Contractless\Rpc\Protocol\Address;
use Contractless\Rpc\Protocol\Binary; use Contractless\Rpc\Protocol\Binary;
use Contractless\Rpc\Wallet\Credentials; use Contractless\Rpc\Signing\SignatureProviderInterface;
final class AgreementBuilder final class AgreementBuilder
{ {
public function __construct( public function __construct(
private readonly CryptoInterface $crypto, private readonly CryptoInterface $crypto,
private readonly Credentials $credentials, private readonly SignatureProviderInterface $signer,
) { ) {
} }
@ -53,7 +53,7 @@ final class AgreementBuilder
. Binary::u64($tip1) . Binary::u64($tip2) . Binary::u64($tip1) . Binary::u64($tip2)
. Binary::u64($fee1) . Binary::u64($fee2); . Binary::u64($fee1) . Binary::u64($fee2);
return (new DualSignedTransaction($json, $bytes, $hash)) return (new DualSignedTransaction($json, $bytes, $hash))
->signNext($this->crypto, $this->credentials); ->signNext($this->crypto, $this->signer);
} }
public function loan( public function loan(
@ -96,7 +96,7 @@ final class AgreementBuilder
. Binary::u8($gracePeriod) . Binary::u64($maxLateValue) . Binary::u8($gracePeriod) . Binary::u64($maxLateValue)
. Binary::u64($fee); . Binary::u64($fee);
return (new DualSignedTransaction($json, $bytes, $hash, storesHash: true)) return (new DualSignedTransaction($json, $bytes, $hash, storesHash: true))
->signNext($this->crypto, $this->credentials); ->signNext($this->crypto, $this->signer);
} }
private function json(array $unsigned): string private function json(array $unsigned): string

View File

@ -7,16 +7,19 @@ namespace Contractless\Rpc\Transaction;
use Contractless\Rpc\Crypto\CryptoInterface; use Contractless\Rpc\Crypto\CryptoInterface;
use Contractless\Rpc\Protocol\Address; use Contractless\Rpc\Protocol\Address;
use Contractless\Rpc\Protocol\Binary; use Contractless\Rpc\Protocol\Binary;
use Contractless\Rpc\Wallet\Credentials; use Contractless\Rpc\Signing\SignatureProviderInterface;
final class AssetBuilder final class AssetBuilder
{ {
use SignsTransactions; use SignsTransactions;
public function __construct(CryptoInterface $crypto, Credentials $credentials) public function __construct(
CryptoInterface $crypto,
SignatureProviderInterface $signer,
)
{ {
$this->crypto = $crypto; $this->crypto = $crypto;
$this->credentials = $credentials; $this->signer = $signer;
} }
public function createToken( public function createToken(

View File

@ -6,7 +6,7 @@ namespace Contractless\Rpc\Transaction;
use Contractless\Rpc\Crypto\CryptoInterface; use Contractless\Rpc\Crypto\CryptoInterface;
use Contractless\Rpc\Exception\ProtocolException; use Contractless\Rpc\Exception\ProtocolException;
use Contractless\Rpc\Wallet\Credentials; use Contractless\Rpc\Signing\SignatureProviderInterface;
final class DualSignedTransaction final class DualSignedTransaction
{ {
@ -20,7 +20,10 @@ final class DualSignedTransaction
) { ) {
} }
public function signNext(CryptoInterface $crypto, Credentials $credentials): self public function signNext(
CryptoInterface $crypto,
SignatureProviderInterface $signer,
): self
{ {
$recalculatedHash = $crypto->skein256($this->unsignedJson); $recalculatedHash = $crypto->skein256($this->unsignedJson);
if (!hash_equals($this->hash, $recalculatedHash)) { if (!hash_equals($this->hash, $recalculatedHash)) {
@ -28,7 +31,7 @@ final class DualSignedTransaction
'The exported transaction hash does not match its unsigned transaction.', 'The exported transaction hash does not match its unsigned transaction.',
); );
} }
$signature = $crypto->sign($this->hash, $credentials->privateKey, $credentials->publicKey); $signature = $signer->sign($this->hash);
if ($this->signature1 === '') { if ($this->signature1 === '') {
return new self( return new self(
$this->unsignedJson, $this->unsignedJson,

View File

@ -8,16 +8,19 @@ use Contractless\Rpc\Crypto\CryptoInterface;
use Contractless\Rpc\Protocol\Address; use Contractless\Rpc\Protocol\Address;
use Contractless\Rpc\Protocol\Binary; use Contractless\Rpc\Protocol\Binary;
use Contractless\Rpc\Protocol\RequestBuilder; use Contractless\Rpc\Protocol\RequestBuilder;
use Contractless\Rpc\Wallet\Credentials; use Contractless\Rpc\Signing\SignatureProviderInterface;
final class GovernanceBuilder final class GovernanceBuilder
{ {
use SignsTransactions; use SignsTransactions;
public function __construct(CryptoInterface $crypto, Credentials $credentials) public function __construct(
CryptoInterface $crypto,
SignatureProviderInterface $signer,
)
{ {
$this->crypto = $crypto; $this->crypto = $crypto;
$this->credentials = $credentials; $this->signer = $signer;
} }
public function proposal( public function proposal(

View File

@ -8,16 +8,19 @@ use Contractless\Rpc\Crypto\CryptoInterface;
use Contractless\Rpc\Protocol\Address; use Contractless\Rpc\Protocol\Address;
use Contractless\Rpc\Protocol\Binary; use Contractless\Rpc\Protocol\Binary;
use Contractless\Rpc\Protocol\RequestBuilder; use Contractless\Rpc\Protocol\RequestBuilder;
use Contractless\Rpc\Wallet\Credentials; use Contractless\Rpc\Signing\SignatureProviderInterface;
final class MiscellaneousBuilder final class MiscellaneousBuilder
{ {
use SignsTransactions; use SignsTransactions;
public function __construct(CryptoInterface $crypto, Credentials $credentials) public function __construct(
CryptoInterface $crypto,
SignatureProviderInterface $signer,
)
{ {
$this->crypto = $crypto; $this->crypto = $crypto;
$this->credentials = $credentials; $this->signer = $signer;
} }
public function marketing( public function marketing(
@ -88,11 +91,7 @@ final class MiscellaneousBuilder
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
); );
$hash = $this->crypto->skein256($json); $hash = $this->crypto->skein256($json);
$signature = $this->crypto->sign( $signature = $this->signer->sign($hash);
$hash,
$this->credentials->privateKey,
$this->credentials->publicKey,
);
$binary = Binary::u8(8) . Binary::u32($timestamp) . Binary::u64($amount) $binary = Binary::u8(8) . Binary::u32($timestamp) . Binary::u64($amount)
. RequestBuilder::hash($contractHash, 'Loan contract hash') . RequestBuilder::hash($contractHash, 'Loan contract hash')
. Address::toBytes($address) . Binary::u64($tip) . Binary::u64($fee) . Address::toBytes($address) . Binary::u64($tip) . Binary::u64($fee)

View File

@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Contractless\Rpc\Transaction; namespace Contractless\Rpc\Transaction;
use Contractless\Rpc\Crypto\CryptoInterface; use Contractless\Rpc\Crypto\CryptoInterface;
use Contractless\Rpc\Wallet\Credentials; use Contractless\Rpc\Signing\SignatureProviderInterface;
trait SignsTransactions trait SignsTransactions
{ {
private CryptoInterface $crypto; private CryptoInterface $crypto;
private Credentials $credentials; private SignatureProviderInterface $signer;
private function sign(array $unsigned, string $binary): SignedTransaction private function sign(array $unsigned, string $binary): SignedTransaction
{ {
@ -19,11 +19,7 @@ trait SignsTransactions
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
); );
$digest = $this->crypto->skein256($json); $digest = $this->crypto->skein256($json);
$signature = $this->crypto->sign( $signature = $this->signer->sign($digest);
$digest,
$this->credentials->privateKey,
$this->credentials->publicKey,
);
return new SignedTransaction($binary . $signature, $signature, $json); return new SignedTransaction($binary . $signature, $signature, $json);
} }
} }

View File

@ -9,16 +9,19 @@ use Contractless\Rpc\Exception\ProtocolException;
use Contractless\Rpc\Protocol\Address; use Contractless\Rpc\Protocol\Address;
use Contractless\Rpc\Protocol\Binary; use Contractless\Rpc\Protocol\Binary;
use Contractless\Rpc\Protocol\RequestBuilder; use Contractless\Rpc\Protocol\RequestBuilder;
use Contractless\Rpc\Wallet\Credentials; use Contractless\Rpc\Signing\SignatureProviderInterface;
final class StorageBuilder final class StorageBuilder
{ {
use SignsTransactions; use SignsTransactions;
public function __construct(CryptoInterface $crypto, Credentials $credentials) public function __construct(
CryptoInterface $crypto,
SignatureProviderInterface $signer,
)
{ {
$this->crypto = $crypto; $this->crypto = $crypto;
$this->credentials = $credentials; $this->signer = $signer;
} }
public function createKey( public function createKey(
@ -206,11 +209,7 @@ final class StorageBuilder
. ',"address":' . json_encode($address, JSON_THROW_ON_ERROR) . ',"address":' . json_encode($address, JSON_THROW_ON_ERROR)
. ',"txfee":' . $fee . '}'; . ',"txfee":' . $fee . '}';
$digest = $this->crypto->skein256($json); $digest = $this->crypto->skein256($json);
$signature = $this->crypto->sign( $signature = $this->signer->sign($digest);
$digest,
$this->credentials->privateKey,
$this->credentials->publicKey,
);
$binary = Binary::u8($type) . Binary::u32($timestamp) $binary = Binary::u8($type) . Binary::u32($timestamp)
. RequestBuilder::hash($storageKey, 'Storage key') . $key . $valueBytes . RequestBuilder::hash($storageKey, 'Storage key') . $key . $valueBytes
. Address::toBytes($address) . Binary::u64($fee); . Address::toBytes($address) . Binary::u64($fee);

View File

@ -7,13 +7,13 @@ namespace Contractless\Rpc\Transaction;
use Contractless\Rpc\Crypto\CryptoInterface; use Contractless\Rpc\Crypto\CryptoInterface;
use Contractless\Rpc\Protocol\Address; use Contractless\Rpc\Protocol\Address;
use Contractless\Rpc\Protocol\Binary; use Contractless\Rpc\Protocol\Binary;
use Contractless\Rpc\Wallet\Credentials; use Contractless\Rpc\Signing\SignatureProviderInterface;
final class TransferBuilder final class TransferBuilder
{ {
public function __construct( public function __construct(
private readonly CryptoInterface $crypto, private readonly CryptoInterface $crypto,
private readonly Credentials $credentials, private readonly SignatureProviderInterface $signer,
) { ) {
} }
@ -42,11 +42,7 @@ final class TransferBuilder
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
$digest = $this->crypto->skein256($unsigned); $digest = $this->crypto->skein256($unsigned);
$signature = $this->crypto->sign( $signature = $this->signer->sign($digest);
$digest,
$this->credentials->privateKey,
$this->credentials->publicKey,
);
$bytes = Binary::u8(2) $bytes = Binary::u8(2)
. Binary::u32($timestamp) . Binary::u32($timestamp)

View File

@ -21,6 +21,8 @@ use Contractless\Rpc\Exception\ProtocolException;
use Contractless\Rpc\Exception\TransportException; use Contractless\Rpc\Exception\TransportException;
use Contractless\Rpc\Protocol\Address; use Contractless\Rpc\Protocol\Address;
use Contractless\Rpc\Protocol\Command; use Contractless\Rpc\Protocol\Command;
use Contractless\Rpc\Protocol\HandshakeProof;
use Contractless\Rpc\Signing\CallbackSignatureProvider;
use Contractless\Rpc\Transport\EndpointPoolTransport; use Contractless\Rpc\Transport\EndpointPoolTransport;
use Contractless\Rpc\Transport\TransportInterface; use Contractless\Rpc\Transport\TransportInterface;
use Contractless\Rpc\Transaction\TransferBuilder; use Contractless\Rpc\Transaction\TransferBuilder;
@ -30,7 +32,6 @@ use Contractless\Rpc\Transaction\StorageBuilder;
use Contractless\Rpc\Transaction\MiscellaneousBuilder; use Contractless\Rpc\Transaction\MiscellaneousBuilder;
use Contractless\Rpc\Transaction\AgreementBuilder; use Contractless\Rpc\Transaction\AgreementBuilder;
use Contractless\Rpc\Transaction\DualSignedTransaction; use Contractless\Rpc\Transaction\DualSignedTransaction;
use Contractless\Rpc\Wallet\Credentials;
function expect(bool $condition, string $message): void function expect(bool $condition, string $message): void
{ {
@ -87,11 +88,16 @@ $transport = new class implements TransportInterface {
$crypto = new CallbackCrypto( $crypto = new CallbackCrypto(
fn (string $message): string => str_repeat("\x11", 32), 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, fn (string $message, string $signature, string $publicKey): bool => true,
); );
$credentials = new Credentials(str_repeat("\x33", 897), str_repeat("\x44", 1281)); $signer = new CallbackSignatureProvider(
$client = new Client($transport, $crypto, $credentials); 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($client->blockByHeight(42) === 'ok', 'Client did not return the RPC payload.');
expect(strlen($transport->handshake) === 1587, 'Handshake request has the wrong size.'); expect(strlen($transport->handshake) === 1587, 'Handshake request has the wrong size.');
@ -103,11 +109,15 @@ $client->storageLookupCost(str_repeat('01', 32), 'all', str_repeat('ab', 20) . '
expect(ord($transport->request[0]) === Command::STORAGE_LOOKUP_COST, 'Storage command is wrong.'); 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.'); expect(strlen($transport->request) === 4 + 32 + 50 + 22, 'Storage cost payload has the wrong size.');
$client->registerOwnedWallet(str_repeat('ab', 20) . '.cltc'); $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(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.'); expect(strlen($transport->request) === 4 + 22 + 897 + 666, 'Wallet registration payload has the wrong size.');
$transfer = (new TransferBuilder($crypto, $credentials))->create( $transfer = (new TransferBuilder($crypto, $signer))->create(
str_repeat('ab', 20) . '.cltc', str_repeat('ab', 20) . '.cltc',
str_repeat('cd', 20) . '.cltc', str_repeat('cd', 20) . '.cltc',
'CLTC', 'CLTC',
@ -127,19 +137,19 @@ expect(
); );
$address = str_repeat('ab', 20) . '.cltc'; $address = str_repeat('ab', 20) . '.cltc';
$assets = new AssetBuilder($crypto, $credentials); $assets = new AssetBuilder($crypto, $signer);
expect(strlen($assets->createToken($address, 'TEST', 1000, true, 100, 1)->bytes) === 725, 'Token size is wrong.'); 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->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->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->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.'); expect(strlen($assets->vanity($address, str_repeat('a', 20) . '.cltc', 100, 1)->bytes) === 723, 'Vanity size is wrong.');
$governance = new GovernanceBuilder($crypto, $credentials); $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->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->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.'); 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); $storage = new StorageBuilder($crypto, $signer);
expect(strlen($storage->createKey($address, 100, 1)->bytes) === 701, 'Storage key size is wrong.'); 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->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->u32(str_repeat('01', 32), 'count', 42, $address, 100, 1)->bytes) === 787, 'Storage u32 size is wrong.');
@ -150,19 +160,19 @@ expect(bin2hex(substr($storage->u128(str_repeat('01', 32), 'large', '34028236692
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->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.'); expect(strlen($storage->delete(str_repeat('01', 32), 'bio', $address, 100, 1)->bytes) === 783, 'Storage delete size is wrong.');
$misc = new MiscellaneousBuilder($crypto, $credentials); $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->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->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.'); 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); $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 = $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); $swap = $swap->signNext($crypto, $signer);
expect(strlen($swap->toSignedTransaction()->bytes) === 1471, 'Swap size is wrong.'); expect(strlen($swap->toSignedTransaction()->bytes) === 1471, 'Swap size is wrong.');
expect(DualSignedTransaction::import($swap->export())->isComplete(), 'Swap export/import lost signatures.'); 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 = $agreements->loan('CLTC', 1000, $address, 'TEST', 2000, str_repeat('cd', 20) . '.cltc', 'm', 12, 100, 2, 300, 100, 1);
$loan = $loan->signNext($crypto, $credentials); $loan = $loan->signNext($crypto, $signer);
expect(strlen($loan->toSignedTransaction()->bytes) === 1492, 'Loan size is wrong.'); expect(strlen($loan->toSignedTransaction()->bytes) === 1492, 'Loan size is wrong.');
$failedEndpoint = new TestTransport( $failedEndpoint = new TestTransport(
@ -176,7 +186,7 @@ $endpointPool = new EndpointPoolTransport(
], ],
randomize: false, randomize: false,
); );
$pooledClient = new Client($endpointPool, $crypto, $credentials); $pooledClient = new Client($endpointPool, $crypto, $handshakeProof);
expect( expect(
$pooledClient->blockHeight() === 'pool reply', $pooledClient->blockHeight() === 'pool reply',
'Endpoint pool did not return the successful fallback reply.', 'Endpoint pool did not return the successful fallback reply.',
@ -207,7 +217,7 @@ $protocolPool = new EndpointPoolTransport(
], ],
randomize: false, randomize: false,
); );
$protocolClient = new Client($protocolPool, $crypto, $credentials); $protocolClient = new Client($protocolPool, $crypto, $handshakeProof);
$protocolRejected = false; $protocolRejected = false;
try { try {
$protocolClient->blockHeight(); $protocolClient->blockHeight();
@ -231,7 +241,7 @@ $allFailedPool = new EndpointPoolTransport(
], ],
randomize: false, randomize: false,
); );
$allFailedClient = new Client($allFailedPool, $crypto, $credentials); $allFailedClient = new Client($allFailedPool, $crypto, $handshakeProof);
$allFailedMessage = ''; $allFailedMessage = '';
try { try {
$allFailedClient->blockHeight(); $allFailedClient->blockHeight();