From d748d748b014d87f586e48883ca925ef60d417ec Mon Sep 17 00:00:00 2001 From: Edouard Courty Date: Sat, 15 Mar 2025 14:50:46 +0100 Subject: [PATCH] feat(client): add support for swarm/peers command --- .gitignore | 1 - CHANGELOG.md | 10 ++ src/Client/IPFSClient.php | 34 ++++++ src/Model/Peer.php | 22 ++++ src/Model/PeerIdentity.php | 17 +++ src/Model/PeerStream.php | 13 +++ src/Transformer/AbstractTransformer.php | 6 +- src/Transformer/PeerIdentityTransformer.php | 23 ++++ src/Transformer/PeerStreamTransformer.php | 22 ++++ src/Transformer/PeerTransformer.php | 42 ++++++++ tests/Client/IPFSClientTest.php | 94 ++++++++++++++++ .../PeerIdentityTransformerTest.php | 47 ++++++++ .../Transformer/PeerStreamTransformerTest.php | 35 ++++++ tests/Transformer/PeerTransformerTest.php | 102 ++++++++++++++++++ 14 files changed, 466 insertions(+), 2 deletions(-) create mode 100644 src/Model/Peer.php create mode 100644 src/Model/PeerIdentity.php create mode 100644 src/Model/PeerStream.php create mode 100644 src/Transformer/PeerIdentityTransformer.php create mode 100644 src/Transformer/PeerStreamTransformer.php create mode 100644 src/Transformer/PeerTransformer.php create mode 100644 tests/Transformer/PeerIdentityTransformerTest.php create mode 100644 tests/Transformer/PeerStreamTransformerTest.php create mode 100644 tests/Transformer/PeerTransformerTest.php diff --git a/.gitignore b/.gitignore index 10089d8..003b5c1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ vendor examples/archive.tar -*.php composer.lock .DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8711839..261e75b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,3 +33,13 @@ This releases brings support for the `CID` IPFS identifiers. #### Updates - Enhanced the `IPFSClient::ping` unit test to handle the actual response format from IPFS nodes. + +## v1.2.0 + +This release brings support for the `swarm/peers` command. + +#### Additions + +- Added the `IPFSClient::getPeers` method, which returns a list of all the node's connected peers. + - Added `Peer`, `PeerIdentity`, `PeerStream` models and corresponding transformers + - Added corresponding tests for the new transformers and methods. diff --git a/src/Client/IPFSClient.php b/src/Client/IPFSClient.php index c3322f0..91b850b 100644 --- a/src/Client/IPFSClient.php +++ b/src/Client/IPFSClient.php @@ -6,13 +6,18 @@ use IPFS\Exception\IPFSTransportException; use IPFS\Model\File; +use IPFS\Model\ListFileEntry; use IPFS\Model\Node; +use IPFS\Model\Peer; use IPFS\Model\Ping; use IPFS\Model\Version; use IPFS\Transformer\FileLinkTransformer; use IPFS\Transformer\FileListTransformer; use IPFS\Transformer\FileTransformer; use IPFS\Transformer\NodeTransformer; +use IPFS\Transformer\PeerIdentityTransformer; +use IPFS\Transformer\PeerStreamTransformer; +use IPFS\Transformer\PeerTransformer; use IPFS\Transformer\PingTransformer; use IPFS\Transformer\VersionTransformer; @@ -81,6 +86,9 @@ public function get(string $hash, bool $archive = false, bool $compress = false, ]); } + /** + * @return ListFileEntry[] + */ public function list(string $hash): array { $response = $this->httpClient->request('POST', '/api/v0/ls', [ @@ -110,6 +118,9 @@ public function getNode(?string $nodeId = null): Node return $nodeTransformer->transform($parsedResponse); } + /** + * @return string[] + */ public function pin(string $path, bool $recursive = false, ?string $name = null): array { $response = $this->httpClient->request('POST', '/api/v0/pin/add', [ @@ -125,6 +136,9 @@ public function pin(string $path, bool $recursive = false, ?string $name = null) return $parsedResponse['Pins'] ?? []; } + /** + * @return string[] + */ public function unpin(string $path, bool $recursive = false): array { $response = $this->httpClient->request('POST', '/api/v0/pin/rm', [ @@ -185,4 +199,24 @@ public function getConfiguration(): array return json_decode($response, true); } + + /** + * @return Peer[] + */ + public function getPeers(bool $verbose = false): array + { + $response = $this->httpClient->request('POST', '/api/v0/swarm/peers', [ + 'query' => [ + 'verbose' => $verbose, + ], + ]); + + $parsedResponse = json_decode($response, true); + + $peerTransformer = new PeerTransformer( + peerIdentityTransformer: new PeerIdentityTransformer(), + peerStreamTransformer: new PeerStreamTransformer(), + ); + return $peerTransformer->transformList($parsedResponse['Peers'] ?? []); + } } diff --git a/src/Model/Peer.php b/src/Model/Peer.php new file mode 100644 index 0000000..57eba5a --- /dev/null +++ b/src/Model/Peer.php @@ -0,0 +1,22 @@ +assertParameters($input, ['ID', 'PublicKey'], true); + + return new PeerIdentity( + id: (string) $input['ID'], + publicKey: (string) $input['PublicKey'], + agentVersion: isset($input['AgentVersion']) ? (string) $input['AgentVersion'] : null, + addresses: $input['Addresses'] ?? [], + protocols: $input['Protocols'] ?? [], + ); + } +} diff --git a/src/Transformer/PeerStreamTransformer.php b/src/Transformer/PeerStreamTransformer.php new file mode 100644 index 0000000..ba38c75 --- /dev/null +++ b/src/Transformer/PeerStreamTransformer.php @@ -0,0 +1,22 @@ +assertParameters($input, ['Protocol']); + + return new PeerStream( + protocol: (string) $input['Protocol'], + ); + } +} diff --git a/src/Transformer/PeerTransformer.php b/src/Transformer/PeerTransformer.php new file mode 100644 index 0000000..6c0cdb0 --- /dev/null +++ b/src/Transformer/PeerTransformer.php @@ -0,0 +1,42 @@ +assertParameters($input, ['Addr', 'Peer']); + + $peerIdentity = null; + + try { + $peerIdentity = $this->peerIdentityTransformer->transform($input['Identify']); + } catch (\Throwable) { + // Ignore + } + + return new Peer( + address: (string) $input['Addr'], + identifier: (string) $input['Peer'], + direction: isset($input['Direction']) ? (int) $input['Direction'] : null, + identity: $peerIdentity, + latency: isset($input['Latency']) ? (float) $input['Latency'] : null, + muxer: isset($input['Muxer']) ? (string) $input['Muxer'] : null, + streams: $this->peerStreamTransformer->transformList($input['Streams'] ?? []), + ); + } +} diff --git a/tests/Client/IPFSClientTest.php b/tests/Client/IPFSClientTest.php index 58b3b5d..ffea1ae 100644 --- a/tests/Client/IPFSClientTest.php +++ b/tests/Client/IPFSClientTest.php @@ -6,6 +6,7 @@ use IPFS\Client\IPFSClient; use IPFS\Client\ScopedHttpClient; +use IPFS\Model\Peer; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -346,4 +347,97 @@ public function testGetConfiguration(): void $this->assertSame($mockReturn, $result); } + + /** + * @covers ::getPeers + */ + public function testGetPeers(): void + { + $mockReturn = [ + 'Peers' => [ + [ + 'Addr' => '/ip4/139.178.65.157/udp/4001/quic-v1', + 'Peer' => 'QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', + 'Latency' => '130.549486ms', + 'Direction' => 2, + 'Streams' => [ + [ + 'Protocol' => '/ipfs/kad/1.0.0', + ], + ], + 'Identify' => [ + 'ID' => 'QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', + 'PublicKey' => 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwin2xA7JpMY/vKGHjjupGH7AhJ451wnfhPqG4glnIKFz41NDZa/bQXk05Gw/SeUONsUUbQ5qiBR1WZR4ExzZaipuRqGkPDdgoG2b1gVtFeUharsE5mLNQP6M2mjOGXpLH/tVP8ONe4FkqKLnt9EJ1sIjRr/qxs+uCxheHepCMmzzCnpIwwOqDBhmEQDWDmX4QsosPCdco2TDzLvSJiCXhuMZ6k8MZgt9EfMjpxri7euDgBnw4JFmWFpyfJlDose5z8F84bKd5DBgWdhFObiJUyI9IEv1j7lMobHYJtu9WVLhgkLUYUnt05qLqysPpZHlnmahi8plolCByNeEvPkubAgMBAAE=', + 'Addresses' => [ + '/dns4/ny5.bootstrap.libp2p.io/tcp/443/wss/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', + ], + 'AgentVersion' => 'rust-libp1p-server/0.12.3', + 'Protocols' => [ + '/ipfs/id/1.0.0', + ], + ], + ], + [ + 'Addr' => '/ip4/139.178.65.157/udp/4001/quic-v1', + 'Peer' => 'QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + 'Latency' => '130.549486ms', + 'Direction' => 2, + 'Streams' => [ + [ + 'Protocol' => '/ipfs/dahk/1.0.0', + ], + ], + 'Identify' => [ + 'ID' => 'QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + 'PublicKey' => 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwin2xA7JpMY/vKGHjjupGH7AhJ451wnfhPqG4glnIKFz41NDZa/bQXk05Gw/SeUONsUUbQ5qiBR1WZR4ExzZaipuRqGkPDdgoG2b1gVtFeUharsE5mLNQP6M2mjOGXpLH/tVP8ONe4FkqKLnt9EJ1sIjRr/qxs+uCxheHepCMmzzCnpIwwOqDBhmEQDWDmX4QsosPCdco2TDzLvSJiCXhuMZ6k8MZgt9EfMjpxri7euDgBnw4JFmWFpyfJlDose5z8F84bKd5DBgWdhFObiJUyI9IEv1j7lMobHYJtu9WVLhgkLUYUnt05qLqysPpZHlnmahi8plolCByNeEvPkubAgMBAAE=', + 'Addresses' => [ + '/dns4/ny5.bootstrap.libp2p.io/tcp/443/wss/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + ], + 'AgentVersion' => 'rust-libp2p-server/0.12.3', + 'Protocols' => [ + '/ipfs/id/2.0.0', + ], + ], + ], + ], + ]; + + $this->httpClient + ->expects($this->once()) + ->method('request') + ->with('POST', '/api/v0/swarm/peers') + ->willReturn(json_encode($mockReturn)); + + $result = $this->client->getPeers(); + + $this->assertCount(\count($mockReturn['Peers']), $result); + + foreach ($result as $key => $peer) { + $correspondingMockData = $mockReturn['Peers'][$key]; + $this->assertPeerResponse($correspondingMockData, $peer); + } + } + + private function assertPeerResponse(array $data, Peer $peer): void + { + $this->assertSame($data['Addr'], $peer->address); + $this->assertSame($data['Peer'], $peer->identifier); + $this->assertSame($data['Direction'], $peer->direction); + $this->assertSame((float) $data['Latency'], $peer->latency); + + foreach ($peer->streams as $key => $stream) { + $correspondingResponseStream = $data['Streams'][$key]; + + $this->assertSame($stream->protocol, $correspondingResponseStream['Protocol']); + } + + $identity = $peer->identity; + $this->assertNotNull($identity); + + $this->assertSame($data['Identify']['ID'], $identity->id); + $this->assertSame($data['Identify']['AgentVersion'], $identity->agentVersion); + $this->assertSame($data['Identify']['PublicKey'], $identity->publicKey); + $this->assertSame($data['Identify']['Addresses'], $identity->addresses); + $this->assertSame($data['Identify']['Protocols'], $identity->protocols); + } } diff --git a/tests/Transformer/PeerIdentityTransformerTest.php b/tests/Transformer/PeerIdentityTransformerTest.php new file mode 100644 index 0000000..6842752 --- /dev/null +++ b/tests/Transformer/PeerIdentityTransformerTest.php @@ -0,0 +1,47 @@ +transformer = new PeerIdentityTransformer(); + } + + /** + * @covers ::transform + */ + public function testItTransforms(): void + { + $data = [ + 'ID' => 'QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + 'PublicKey' => 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwin2xA7JpMY/vKGHjjupGH7AhJ451wnfhPqG4glnIKFz41NDZa/bQXk05Gw/SeUONsUUbQ5qiBR1WZR4ExzZaipuRqGkPDdgoG2b1gVtFeUharsE5mLNQP6M2mjOGXpLH/tVP8ONe4FkqKLnt9EJ1sIjRr/qxs+uCxheHepCMmzzCnpIwwOqDBhmEQDWDmX4QsosPCdco2TDzLvSJiCXhuMZ6k8MZgt9EfMjpxri7euDgBnw4JFmWFpyfJlDose5z8F84bKd5DBgWdhFObiJUyI9IEv1j7lMobHYJtu9WVLhgkLUYUnt05qLqysPpZHlnmahi8plolCByNeEvPkubAgMBAAE=', + 'Addresses' => [ + '/dns4/ny5.bootstrap.libp2p.io/tcp/443/wss/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + ], + 'AgentVersion' => 'rust-libp2p-server/0.12.3', + 'Protocols' => [ + '/ipfs/id/1.0.0', + ], + ]; + + $result = $this->transformer->transform($data); + + $this->assertSame($data['ID'], $result->id); + $this->assertSame($data['PublicKey'], $result->publicKey); + $this->assertSame($data['Addresses'], $result->addresses); + $this->assertSame($data['AgentVersion'], $result->agentVersion); + $this->assertSame($data['Protocols'], $result->protocols); + } +} diff --git a/tests/Transformer/PeerStreamTransformerTest.php b/tests/Transformer/PeerStreamTransformerTest.php new file mode 100644 index 0000000..4c54bc8 --- /dev/null +++ b/tests/Transformer/PeerStreamTransformerTest.php @@ -0,0 +1,35 @@ +transformer = new PeerStreamTransformer(); + } + + /** + * @covers ::transform + */ + public function testItTransforms(): void + { + $data = [ + 'Protocol' => 'protocol/v1', + ]; + + $result = $this->transformer->transform($data); + + $this->assertSame($data['Protocol'], $result->protocol); + } +} diff --git a/tests/Transformer/PeerTransformerTest.php b/tests/Transformer/PeerTransformerTest.php new file mode 100644 index 0000000..5ffa3be --- /dev/null +++ b/tests/Transformer/PeerTransformerTest.php @@ -0,0 +1,102 @@ +transformer = new PeerTransformer( + peerIdentityTransformer: new PeerIdentityTransformer(), + peerStreamTransformer: new PeerStreamTransformer(), + ); + } + + /** + * @covers ::transform + */ + public function testItTransforms(): void + { + $data = [ + 'Addr' => '/ip4/139.178.65.157/udp/4001/quic-v1', + 'Peer' => 'QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + 'Identify' => [ + 'ID' => '', + 'PublicKey' => '', + 'Addresses' => null, + 'AgentVersion' => '', + 'Protocols' => null, + ], + ]; + + $peer = $this->transformer->transform($data); + + $this->assertSame($data['Addr'], $peer->address); + $this->assertSame($data['Peer'], $peer->identifier); + $this->assertNull($peer->direction); + $this->assertNull($peer->latency); + $this->assertEmpty($peer->streams); + + $this->assertNull($peer->identity); + } + + public function testItTransformsWithVerboseData(): void + { + $data = [ + 'Addr' => '/ip4/139.178.65.157/udp/4001/quic-v1', + 'Peer' => 'QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + 'Latency' => '130.549486ms', + 'Direction' => 2, + 'Streams' => [ + [ + 'Protocol' => '/ipfs/kad/1.0.0', + ], + ], + 'Identify' => [ + 'ID' => 'QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + 'PublicKey' => 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwin2xA7JpMY/vKGHjjupGH7AhJ451wnfhPqG4glnIKFz41NDZa/bQXk05Gw/SeUONsUUbQ5qiBR1WZR4ExzZaipuRqGkPDdgoG2b1gVtFeUharsE5mLNQP6M2mjOGXpLH/tVP8ONe4FkqKLnt9EJ1sIjRr/qxs+uCxheHepCMmzzCnpIwwOqDBhmEQDWDmX4QsosPCdco2TDzLvSJiCXhuMZ6k8MZgt9EfMjpxri7euDgBnw4JFmWFpyfJlDose5z8F84bKd5DBgWdhFObiJUyI9IEv1j7lMobHYJtu9WVLhgkLUYUnt05qLqysPpZHlnmahi8plolCByNeEvPkubAgMBAAE=', + 'Addresses' => [ + '/dns4/ny5.bootstrap.libp2p.io/tcp/443/wss/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + ], + 'AgentVersion' => 'rust-libp2p-server/0.12.3', + 'Protocols' => [ + '/ipfs/id/1.0.0', + ], + ], + ]; + + $peer = $this->transformer->transform($data); + + $this->assertSame($data['Addr'], $peer->address); + $this->assertSame($data['Peer'], $peer->identifier); + $this->assertSame($data['Direction'], $peer->direction); + $this->assertSame((float) $data['Latency'], $peer->latency); + + foreach ($peer->streams as $key => $stream) { + $correspondingResponseStream = $data['Streams'][$key]; + + $this->assertSame($stream->protocol, $correspondingResponseStream['Protocol']); + } + + $identity = $peer->identity; + $this->assertNotNull($identity); + + $this->assertSame($data['Identify']['ID'], $identity->id); + $this->assertSame($data['Identify']['AgentVersion'], $identity->agentVersion); + $this->assertSame($data['Identify']['PublicKey'], $identity->publicKey); + $this->assertSame($data['Identify']['Addresses'], $identity->addresses); + $this->assertSame($data['Identify']['Protocols'], $identity->protocols); + } +}