From 67d50052e38b43f831eb5edc4c7f4e34c81319fc Mon Sep 17 00:00:00 2001 From: refring Date: Sat, 6 Dec 2025 20:27:40 +0100 Subject: [PATCH 1/4] feat: Added WalletRpcClient::setSubaddressLookahead() method --- .../SetSubaddressLookaheadRequest.php | 37 ++++++++++ .../SetSubaddressLookaheadResponse.php | 16 +++++ src/WalletRpcClient.php | 15 ++++ tests/TestHelper.php | 6 ++ tests/integration/TransferTest.php | 70 +++++++++++++++++++ tests/unit/WalletRpcDeserializationTest.php | 9 +++ tests/unit/WalletRpcSerializationTest.php | 15 ++++ 7 files changed, 168 insertions(+) create mode 100644 src/WalletRpc/SetSubaddressLookaheadRequest.php create mode 100644 src/WalletRpc/SetSubaddressLookaheadResponse.php diff --git a/src/WalletRpc/SetSubaddressLookaheadRequest.php b/src/WalletRpc/SetSubaddressLookaheadRequest.php new file mode 100644 index 0000000..800b169 --- /dev/null +++ b/src/WalletRpc/SetSubaddressLookaheadRequest.php @@ -0,0 +1,37 @@ +password = $password; + $self->major = $major; + $self->minor = $minor; + return new RpcRequest('set_subaddress_lookahead', $self); + } +} diff --git a/src/WalletRpc/SetSubaddressLookaheadResponse.php b/src/WalletRpc/SetSubaddressLookaheadResponse.php new file mode 100644 index 0000000..fdef687 --- /dev/null +++ b/src/WalletRpc/SetSubaddressLookaheadResponse.php @@ -0,0 +1,16 @@ +handleRequest(GetDefaultFeePriorityRequest::create(), GetDefaultFeePriorityResponse::class); } + + /** + * Set the subaddress lookahead depth for scanning. + * + * @param int $major The number of accounts to look ahead. + * @param int $minor The number of subaddresses to look ahead per account. + * @param ?string $password Wallet password. + * @throws MoneroRpcException + */ + public function setSubaddressLookahead(int $major, int $minor, ?string $password = null): SetSubaddressLookaheadResponse + { + return $this->handleRequest(SetSubaddressLookaheadRequest::create($major, $minor, $password), SetSubaddressLookaheadResponse::class); + } } diff --git a/tests/TestHelper.php b/tests/TestHelper.php index 0427a15..e4acb18 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -43,6 +43,12 @@ class TestHelper final public const WALLET_PWD_1 = 'b4r0l0'; final public const WALLET_PWD_2 = 'b4r0l0-2'; + /** + * Subaddress at index (0, 999) for WALLET_1_MNEMONIC + * Used to test set_subaddress_lookahead functionality + */ + final public const MAINNET_SUBADDRESS_0_999 = '8A6tQYX2JX2CSvEZAEtPpMa3NwFNDLtRFCaSEcigzNQn53H8H5dkPFHHYwaTxjVw4NFV9xuahPqjmDbnVfhbFb3T1pKv6xH'; + public static function getRandomWalletName(): string { return hash('sha256', (string) random_int(0, PHP_INT_MAX)); diff --git a/tests/integration/TransferTest.php b/tests/integration/TransferTest.php index 9371d82..7860484 100644 --- a/tests/integration/TransferTest.php +++ b/tests/integration/TransferTest.php @@ -8,10 +8,12 @@ use PHPUnit\Framework\TestCase; use RefRing\MoneroRpcPhp\ClientBuilder; use RefRing\MoneroRpcPhp\DaemonRpcClient; +use RefRing\MoneroRpcPhp\Exception\AddressNotInWalletException; use RefRing\MoneroRpcPhp\Exception\InvalidDestinationException; use RefRing\MoneroRpcPhp\Model\Address; use RefRing\MoneroRpcPhp\Model\Amount; use RefRing\MoneroRpcPhp\Tests\TestHelper; +use RefRing\MoneroRpcPhp\Tests\Util\StdOutLogger; use RefRing\MoneroRpcPhp\WalletRpc\Model\Destination; use RefRing\MoneroRpcPhp\WalletRpc\Model\TransferType; use RefRing\MoneroRpcPhp\WalletRpc\TransferResponse; @@ -198,4 +200,72 @@ public function testScanTx(TransferResponse $transferResponse): void $this->expectNotToPerformAssertions(); $result = self::$walletRpcClient->scanTx([$transferResponse->txHash]); } + + public function testSubaddressLookahead(): void + { + // Restore wallet 1 for this test + self::$walletRpcClient->restoreDeterministicWallet('', '', self::$seeds[0]); + self::$walletRpcClient->refresh(); + + // The subaddress at index (0, 999) should not be accessible with default lookahead + $address0_999 = new Address(TestHelper::MAINNET_SUBADDRESS_0_999); + + // Verify the address is not in the current lookahead table + $addressNotFound = false; + try { + self::$walletRpcClient->getAddressIndex($address0_999); + } catch (AddressNotInWalletException) { + $addressNotFound = true; + } + $this->assertTrue($addressNotFound, 'Address at index (0, 999) should not be in the lookahead table before extending it'); + + // Extend the lookahead to include index 999 + self::$walletRpcClient->setSubaddressLookahead(50, 1000, ""); + + // Now the address should be accessible + $result = self::$walletRpcClient->getAddressIndex($address0_999); + $this->assertSame(0, $result->index->major); + $this->assertSame(999, $result->index->minor); + + // Transfer to the subaddress at index (0, 999) + $transferAmount = 454545454545; + $destination = new Destination($address0_999, new Amount($transferAmount)); + + // Use wallet 2 to transfer (restore it first) + self::$walletRpcClient->restoreDeterministicWallet('', '', self::$seeds[1]); + self::$daemonRpcClient->generateBlocks(100, TestHelper::MAINNET_ADDRESS_2); + self::$walletRpcClient->refresh(); + + $balance = self::$walletRpcClient->getBalance(); + $this->assertGreaterThan($transferAmount, $balance->balance, 'Wallet 2 should have enough balance for transfer'); + + self::$walletRpcClient->transfer($destination); + + // Mine a block to confirm the transaction + self::$daemonRpcClient->generateBlocks(1, TestHelper::MAINNET_ADDRESS_1); + + // Switch back to wallet 1 and verify the balance + self::$walletRpcClient->restoreDeterministicWallet('', '', self::$seeds[0]); + // Re-apply lookahead since wallet was restored + self::$walletRpcClient->setSubaddressLookahead(50, 1000); + self::$walletRpcClient->refresh(); + + $balanceResult = self::$walletRpcClient->getBalance(0, [999]); + $this->assertNotEmpty($balanceResult->perSubaddress, 'Balance info for subaddress should exist'); + + $balanceInfo0_999 = null; + foreach ($balanceResult->perSubaddress as $balanceInfo) { + if ($balanceInfo->accountIndex === 0 && $balanceInfo->addressIndex === 999) { + $balanceInfo0_999 = $balanceInfo; + break; + } + } + + $this->assertNotNull($balanceInfo0_999, 'Balance info for address (0, 999) not found'); + $this->assertSame(TestHelper::MAINNET_SUBADDRESS_0_999, (string) $balanceInfo0_999->address); + $this->assertSame($transferAmount, $balanceInfo0_999->balance); + $this->assertSame(0, $balanceInfo0_999->unlockedBalance); + $this->assertSame('', $balanceInfo0_999->label); + $this->assertSame(1, $balanceInfo0_999->numUnspentOutputs); + } } diff --git a/tests/unit/WalletRpcDeserializationTest.php b/tests/unit/WalletRpcDeserializationTest.php index 6d8fe7f..a9b70a3 100644 --- a/tests/unit/WalletRpcDeserializationTest.php +++ b/tests/unit/WalletRpcDeserializationTest.php @@ -73,6 +73,7 @@ use RefRing\MoneroRpcPhp\WalletRpc\SetAccountTagDescriptionResponse; use RefRing\MoneroRpcPhp\WalletRpc\SetAttributeResponse; use RefRing\MoneroRpcPhp\WalletRpc\SetDaemonResponse; +use RefRing\MoneroRpcPhp\WalletRpc\SetSubaddressLookaheadResponse; use RefRing\MoneroRpcPhp\WalletRpc\SetTxNotesResponse; use RefRing\MoneroRpcPhp\WalletRpc\SignMultisigResponse; use RefRing\MoneroRpcPhp\WalletRpc\SignResponse; @@ -878,6 +879,14 @@ public function testGetDefaultFeePriority() $this->assertSame($responseFlat, $response->toJson()); } + public function testSetSubaddressLookahead() + { + $jsonResponse = '{"id":"0","jsonrpc":"2.0","result":{}}'; + $response = SetSubaddressLookaheadResponse::fromJsonString($jsonResponse, "result"); + $responseFlat = $this->comparableJson($jsonResponse); + $this->assertSame($responseFlat, $response->toJson(JSON_FORCE_OBJECT)); + } + public static function comparableJson(string $json): string { return json_encode(json_decode($json, flags: JSON_BIGINT_AS_STRING)->result); diff --git a/tests/unit/WalletRpcSerializationTest.php b/tests/unit/WalletRpcSerializationTest.php index 41266f5..52cc863 100644 --- a/tests/unit/WalletRpcSerializationTest.php +++ b/tests/unit/WalletRpcSerializationTest.php @@ -80,6 +80,7 @@ use RefRing\MoneroRpcPhp\WalletRpc\SetAccountTagDescriptionRequest; use RefRing\MoneroRpcPhp\WalletRpc\SetAttributeRequest; use RefRing\MoneroRpcPhp\WalletRpc\SetDaemonRequest; +use RefRing\MoneroRpcPhp\WalletRpc\SetSubaddressLookaheadRequest; use RefRing\MoneroRpcPhp\WalletRpc\SetTxNotesRequest; use RefRing\MoneroRpcPhp\WalletRpc\SignMultisigRequest; use RefRing\MoneroRpcPhp\WalletRpc\SignRequest; @@ -898,4 +899,18 @@ public function testGetDefaultFeePriority() $request = GetDefaultFeePriorityRequest::create(); $this->assertSame($expected, $request->toJson()); } + + public function testSetSubaddressLookahead() + { + $expected = '{"jsonrpc":"2.0","id":"0","method":"set_subaddress_lookahead","params":{"password":"testpassword","major_idx":50,"minor_idx":200}}'; + $request = SetSubaddressLookaheadRequest::create(50, 200, 'testpassword'); + $this->assertSame($expected, $request->toJson()); + } + + public function testSetSubaddressLookaheadWithoutPassword() + { + $expected = '{"jsonrpc":"2.0","id":"0","method":"set_subaddress_lookahead","params":{"major_idx":50,"minor_idx":200}}'; + $request = SetSubaddressLookaheadRequest::create(50, 200); + $this->assertSame($expected, $request->toJson()); + } } From 69be6bad9e21e452f707c4ab4dca8ec395e389db Mon Sep 17 00:00:00 2001 From: refring Date: Sun, 7 Dec 2025 10:28:51 +0100 Subject: [PATCH 2/4] fix: Added kexIsDone property to IsMultisigResponse --- src/WalletRpc/IsMultisigResponse.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/WalletRpc/IsMultisigResponse.php b/src/WalletRpc/IsMultisigResponse.php index 5b37d17..016e9f4 100644 --- a/src/WalletRpc/IsMultisigResponse.php +++ b/src/WalletRpc/IsMultisigResponse.php @@ -24,6 +24,9 @@ class IsMultisigResponse implements JsonDataSerializable #[Json] public bool $ready; + #[Json('kex_is_done')] + public bool $kexIsDone; + /** * Amount of signature needed to sign a transfer. */ From b8fd240ddb172783f777f2055446201ea9e1ef18 Mon Sep 17 00:00:00 2001 From: refring Date: Sun, 7 Dec 2025 10:29:41 +0100 Subject: [PATCH 3/4] chore: Use separate wallet rpc servers for different wallets --- phpunit.xml.dist | 1 + tests/docker-compose.yml | 11 ++++++++ tests/integration/TransferTest.php | 44 ++++++++++++++++++------------ 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 81bfd80..019dc78 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,6 +21,7 @@ + diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index c7dfaa0..5e1ee10 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -21,6 +21,17 @@ services: ports: - "18083:18083" + monero-wallet-rpc-2: + image: ghcr.io/refring/monero-containers/monero-wallet-rpc:${MONERO_VERSION:-0.18.4.4} + environment: + MONERO_DAEMON_ADDRESS: monerod:18081 + MONERO_DAEMON_HOST: monerod:18081 + WALLET_RPC_PORT: 18086 + depends_on: + - "monerod" + ports: + - "18086:18086" + monerod-rpc-authentication: image: ghcr.io/refring/monero-containers/monerod:${MONERO_VERSION:-0.18.4.4} depends_on: diff --git a/tests/integration/TransferTest.php b/tests/integration/TransferTest.php index 7860484..589024e 100644 --- a/tests/integration/TransferTest.php +++ b/tests/integration/TransferTest.php @@ -12,8 +12,9 @@ use RefRing\MoneroRpcPhp\Exception\InvalidDestinationException; use RefRing\MoneroRpcPhp\Model\Address; use RefRing\MoneroRpcPhp\Model\Amount; +use RefRing\MoneroRpcPhp\Tests\Attribute\RequiresMoneroVersion; use RefRing\MoneroRpcPhp\Tests\TestHelper; -use RefRing\MoneroRpcPhp\Tests\Util\StdOutLogger; +use RefRing\MoneroRpcPhp\Tests\Trait\RequiresMoneroVersionTrait; use RefRing\MoneroRpcPhp\WalletRpc\Model\Destination; use RefRing\MoneroRpcPhp\WalletRpc\Model\TransferType; use RefRing\MoneroRpcPhp\WalletRpc\TransferResponse; @@ -21,12 +22,15 @@ final class TransferTest extends TestCase { + use RequiresMoneroVersionTrait; + public static array $seeds = [TestHelper::WALLET_1_MNEMONIC, TestHelper::WALLET_2_MNEMONIC]; public static array $wallets = []; private static DaemonRpcClient $daemonRpcClient; private static WalletRpcClient $walletRpcClient; + private static WalletRpcClient $walletRpcClient2; public static int $runningBalance = 0; @@ -46,11 +50,24 @@ public static function setUpBeforeClass(): void ->buildDaemonClient(); self::$walletRpcClient = (new ClientBuilder(getenv('WALLET_RPC_URL'))) ->buildWalletClient(); + self::$walletRpcClient2 = (new ClientBuilder(getenv('WALLET_RPC_URL_2'))) + ->buildWalletClient(); self::$walletRpcClient->restoreDeterministicWallet('', '', self::$seeds[0]); + self::$walletRpcClient2->restoreDeterministicWallet('', '', self::$seeds[1]); self::$daemonRpcClient->generateBlocks(100, TestHelper::MAINNET_ADDRESS_1); } + protected function setUp(): void + { + $this->checkMoneroVersionRequirements(); + } + + protected static function getDaemonRpcClient(): DaemonRpcClient + { + return self::$daemonRpcClient; + } + public function testWallet(): void { self::$walletRpcClient->refresh(); @@ -183,10 +200,9 @@ public function testRawTransactions(): void $this->assertFalse($result->overspend); $this->assertFalse($result->feeTooLow); - self::$walletRpcClient->restoreDeterministicWallet('', '', self::$seeds[1]); - self::$walletRpcClient->refresh(); + self::$walletRpcClient2->refresh(); - $result = self::$walletRpcClient->getTransfers(true, true, true, true, true); + $result = self::$walletRpcClient2->getTransfers(true, true, true, true, true); $this->assertCount(0, $result->in); $this->assertCount(0, $result->out); $this->assertCount(0, $result->pending); @@ -201,12 +217,9 @@ public function testScanTx(TransferResponse $transferResponse): void $result = self::$walletRpcClient->scanTx([$transferResponse->txHash]); } + #[RequiresMoneroVersion('0.18.4.1')] public function testSubaddressLookahead(): void { - // Restore wallet 1 for this test - self::$walletRpcClient->restoreDeterministicWallet('', '', self::$seeds[0]); - self::$walletRpcClient->refresh(); - // The subaddress at index (0, 999) should not be accessible with default lookahead $address0_999 = new Address(TestHelper::MAINNET_SUBADDRESS_0_999); @@ -232,25 +245,20 @@ public function testSubaddressLookahead(): void $destination = new Destination($address0_999, new Amount($transferAmount)); // Use wallet 2 to transfer (restore it first) - self::$walletRpcClient->restoreDeterministicWallet('', '', self::$seeds[1]); self::$daemonRpcClient->generateBlocks(100, TestHelper::MAINNET_ADDRESS_2); - self::$walletRpcClient->refresh(); - $balance = self::$walletRpcClient->getBalance(); + self::$walletRpcClient2->refresh(); + $balance = self::$walletRpcClient2->getBalance(); $this->assertGreaterThan($transferAmount, $balance->balance, 'Wallet 2 should have enough balance for transfer'); - self::$walletRpcClient->transfer($destination); + self::$walletRpcClient2->transfer($destination); // Mine a block to confirm the transaction - self::$daemonRpcClient->generateBlocks(1, TestHelper::MAINNET_ADDRESS_1); + self::$daemonRpcClient->generateBlocks(5, TestHelper::MAINNET_ADDRESS_1); - // Switch back to wallet 1 and verify the balance - self::$walletRpcClient->restoreDeterministicWallet('', '', self::$seeds[0]); - // Re-apply lookahead since wallet was restored - self::$walletRpcClient->setSubaddressLookahead(50, 1000); self::$walletRpcClient->refresh(); - $balanceResult = self::$walletRpcClient->getBalance(0, [999]); + $balanceResult = self::$walletRpcClient->getBalance(0); $this->assertNotEmpty($balanceResult->perSubaddress, 'Balance info for subaddress should exist'); $balanceInfo0_999 = null; From 6f84a5cb69aa33590097b65b882939c2d61b36ae Mon Sep 17 00:00:00 2001 From: refring Date: Sun, 7 Dec 2025 11:01:15 +0100 Subject: [PATCH 4/4] chore: Added BasicWalletTest::testUpdateLookahead() --- tests/integration/BasicWalletTest.php | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/integration/BasicWalletTest.php b/tests/integration/BasicWalletTest.php index 34608da..d954f8b 100644 --- a/tests/integration/BasicWalletTest.php +++ b/tests/integration/BasicWalletTest.php @@ -24,6 +24,8 @@ use RefRing\MoneroRpcPhp\Exception\TagNotFoundException; use RefRing\MoneroRpcPhp\Exception\WalletExistsException; use RefRing\MoneroRpcPhp\Model\Address; +use RefRing\MoneroRpcPhp\Tests\Attribute\RequiresMoneroVersion; +use RefRing\MoneroRpcPhp\Tests\Trait\RequiresMoneroVersionTrait; use RefRing\MoneroRpcPhp\Model\Amount; use RefRing\MoneroRpcPhp\Tests\KeyPairHelper; use RefRing\MoneroRpcPhp\Tests\TestHelper; @@ -36,6 +38,8 @@ class BasicWalletTest extends TestCase { + use RequiresMoneroVersionTrait; + private static WalletRpcClient $rpcClient; private static DaemonRpcClient $daemonRpcClient; @@ -57,6 +61,16 @@ public static function setUpBeforeClass(): void self::$daemonRpcClient = (new ClientBuilder(getenv('DAEMON_RPC_URL')))->buildDaemonClient(); } + protected function setUp(): void + { + $this->checkMoneroVersionRequirements(); + } + + protected static function getDaemonRpcClient(): DaemonRpcClient + { + return self::$daemonRpcClient; + } + public function testGetVersion(): void { $version = self::$rpcClient->getVersion(); @@ -557,4 +571,31 @@ public function testQueryKey(string $seed): void $result = self::$rpcClient->queryKey(QueryKeyType::MNEMONIC); $this->assertSame($seed, $result->key); } + + #[RequiresMoneroVersion('0.18.4.1')] + public function testUpdateLookahead(): void + { + // Restore the test wallet + self::$rpcClient->restoreDeterministicWallet('', '', TestHelper::WALLET_1_MNEMONIC); + + // The subaddress at index (0, 999) should not be accessible with default lookahead + $address0_999 = new Address(TestHelper::MAINNET_SUBADDRESS_0_999); + + // Verify the address is not in the current lookahead table + $addressNotFound = false; + try { + self::$rpcClient->getAddressIndex($address0_999); + } catch (AddressNotInWalletException) { + $addressNotFound = true; + } + $this->assertTrue($addressNotFound, 'Address at index (0, 999) should not be in the lookahead table before extending it'); + + // Update the lookahead and verify the high index address is now in the table + self::$rpcClient->setSubaddressLookahead(50, 1000); + + $result = self::$rpcClient->getAddressIndex($address0_999); + $this->assertSame(0, $result->index->major); + $this->assertSame(999, $result->index->minor); + } + }