Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions js/stateless.js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ Install this package in your project by running the following terminal command:
npm install --save @lightprotocol/stateless.js
```

### Helius mainnet

Use the mainnet RPC root URL for both Solana and Photon calls:

```typescript
import { createRpc } from '@lightprotocol/stateless.js';

const rpc = createRpc('https://mainnet.helius-rpc.com/?api-key=YOUR_KEY');
const slot = await rpc.getIndexerSlot();
```

The client posts JSON-RPC requests to this URL, with the method name in the
request body. Do not append a method path such as `/getIndexerSlot`.
The legacy Helius hostname is not required. A separate Photon endpoint can
still be supplied as the second argument to `createRpc`.

## Documentation and Examples

For a more detailed documentation on usage, please check [the respective section at the ZK Compression documentation.](https://www.zkcompression.com/developers/typescript-client)
Expand Down
10 changes: 8 additions & 2 deletions js/stateless.js/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,9 @@ async function getCompressedTokenAccountsByOwnerOrDelegate(
*
* @param endpointOrWeb3JsConnection endpoint to the solana cluster or
* Connection object
* @param compressionApiEndpoint Endpoint to the compression server
* @param compressionApiEndpoint JSON-RPC URL of the compression server.
* For Helius, use https://mainnet.helius-rpc.com/?api-key=KEY.
* Method names are sent in the body, not the URL.
* @param proverEndpoint Endpoint to the prover server. defaults
* to endpoint
* @param connectionConfig Optional connection config
Expand Down Expand Up @@ -301,7 +303,11 @@ export function wrapBigNumbersAsStrings(text: string): string {
});
}

/** @internal */
/**
* POST JSON-RPC to the configured URL unchanged, including authentication and
* any proxy path. Helius Gatekeeper routes by the method in the request body.
* @internal
*/
export const rpcRequest = async (
rpcEndpoint: string,
method: string,
Expand Down
122 changes: 122 additions & 0 deletions js/stateless.js/tests/unit/rpc/json-rpc-transport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { Connection } from '@solana/web3.js';
import { createRpc, rpcRequest } from '../../../src/rpc';

describe('Photon JSON-RPC transport', () => {
afterEach(() => vi.unstubAllGlobals());

const mockResponse = (result: unknown, status = 200) => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify(result), {
status,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
return fetchMock;
};

it.each([
'https://mainnet.helius-rpc.com?api-key=TEST_KEY',
'https://mainnet.helius-rpc.com/?api-key=TEST_KEY',
'https://devnet.helius-rpc.com/?api-key=TEST_KEY',
'https://mainnet.legacy.helius-rpc.com/?api-key=TEST_KEY',
'http://127.0.0.1:8784',
'http://127.0.0.1:8784/',
'https://rpc.example.com/photon?region=eu&api-key=key%2B%2F%3D%26value&tag=one&tag=two',
'https://rpc.example.com/photon/?region=eu',
])('posts to the configured URL unchanged: %s', async endpoint => {
const fetchMock = mockResponse({
jsonrpc: '2.0',
id: 'test-account',
result: 'ok',
});

await expect(createRpc(endpoint).getIndexerHealth()).resolves.toBe(
'ok',
);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 'test-account',
method: 'getIndexerHealth',
params: [],
}),
});
});

it('uses an explicit compression URL with a web3 Connection', async () => {
const endpoint = 'https://mainnet.helius-rpc.com/?api-key=TEST_KEY';
const fetchMock = mockResponse({
jsonrpc: '2.0',
id: 'test-account',
result: 123,
});
const rpc = createRpc(
new Connection('http://127.0.0.1:8899'),
endpoint,
);

await expect(rpc.getIndexerSlot()).resolves.toBe(123);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock.mock.calls[0][0]).toBe(endpoint);
expect(JSON.parse(fetchMock.mock.calls[0][1].body).method).toBe(
'getIndexerSlot',
);
});

it.each<[string, Record<string, unknown>]>([
['getCompressedAccount', { hash: '11111111111111111111111111111111' }],
[
'getCompressedAccountV2',
{ hash: '11111111111111111111111111111111' },
],
['getValidityProof', { hashes: [], newAddressesWithTrees: [] }],
['getValidityProofV2', { hashes: [], newAddressesWithTrees: [] }],
])('carries %s and its parameters in the body', async (method, params) => {
const endpoint = 'https://mainnet.helius-rpc.com/?api-key=TEST_KEY';
const response = { jsonrpc: '2.0', id: 'test-account', result: null };
const fetchMock = mockResponse(response);

await expect(rpcRequest(endpoint, method, params)).resolves.toEqual(
response,
);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock.mock.calls[0][0]).toBe(endpoint);
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({
jsonrpc: '2.0',
id: 'test-account',
method,
params,
});
});

it('surfaces JSON-RPC errors without retrying a REST or legacy URL', async () => {
const fetchMock = mockResponse({
jsonrpc: '2.0',
id: 'test-account',
error: { code: -32601, message: 'Method not found' },
});

await expect(
createRpc(
'https://mainnet.helius-rpc.com/?api-key=TEST_KEY',
).getIndexerHealth(),
).rejects.toThrow('Method not found');
expect(fetchMock).toHaveBeenCalledTimes(1);
});

it('surfaces HTTP errors', async () => {
const fetchMock = mockResponse({ error: 'Unauthorized' }, 401);

await expect(
createRpc(
'https://mainnet.helius-rpc.com/?api-key=TEST_KEY',
).getIndexerHealth(),
).rejects.toThrow('HTTP error! status: 401');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
});
4 changes: 4 additions & 0 deletions sdk-libs/photon-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Fixed

- Send Photon calls as JSON-RPC POSTs to the configured RPC URL instead of appending `/methodName`. This supports `https://mainnet.helius-rpc.com/?api-key=YOUR_KEY` through Helius Gatekeeper without a legacy hostname. API keys and other query parameters are preserved, including percent-encoded values.

### Breaking Changes

- **Simplified `Configuration` struct.** The API key is now embedded in the URL as a query parameter:
Expand Down
Loading
Loading