Skip to content

Commit

Permalink
add search method to Stream and Distributor Solana clients (#193)
Browse files Browse the repository at this point in the history
* add search method to Stream and Distributor Solana clients
  • Loading branch information
Yolley authored Sep 6, 2024
1 parent 847bcc1 commit 426489d
Show file tree
Hide file tree
Showing 12 changed files with 82 additions and 9 deletions.
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"packages": [
"packages/*"
],
"version": "6.3.10",
"version": "6.4.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@streamflow/common",
"version": "6.3.10",
"version": "6.4.0",
"description": "Common utilities and types used by streamflow packages.",
"homepage": "https://github.com/streamflow-finance/js-sdk/",
"main": "dist/index.js",
Expand Down
5 changes: 5 additions & 0 deletions packages/common/solana/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export interface ThrottleParams {
sendThrottler?: PQueue;
}

export interface IProgramAccount<T> {
publicKey: PublicKey;
account: T;
}

export class TransactionFailedError extends Error {
constructor(m: string) {
super(m);
Expand Down
14 changes: 14 additions & 0 deletions packages/distributor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ const solanaParams = {
const clawbackRes = await client.clawback({ id: res.metadataId }, solanaParams);
```

## Search Airdrops

As all the data is stored on-chain you can also search Airdrops with `searchDistributors` method:

```javascript
// All parameters are optional, so in theory you just fetch all Distributors
const params = {
mint: "BZLbGTNCSFfoth2GYDtwr7e4imWzpR5jqcUuGEwr646K",
admin: "s3pWmY359mDrNRnDBZ3v5TrrqqxvxiW2t4U2WZyxRoA"
};
// Return an Array of objects {publicKey: PublicKey, account: Distributor}
const disributors = await client.searchDistributors(params);
```

## Handling errors

`GenericStreamClient` wraps all errors when making on-chain calls with `ContractError` error class:
Expand Down
2 changes: 1 addition & 1 deletion packages/distributor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@streamflow/distributor",
"version": "6.3.10",
"version": "6.4.0",
"description": "JavaScript SDK to interact with Streamflow Airdrop protocol.",
"homepage": "https://github.com/streamflow-finance/js-sdk/",
"main": "dist/index.js",
Expand Down
10 changes: 6 additions & 4 deletions packages/distributor/solana/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
prepareTransaction,
getMintAndProgram,
buildSendThrottler,
IProgramAccount,
} from "@streamflow/common/solana";

import { DISTRIBUTOR_ADMIN_OFFSET, DISTRIBUTOR_MINT_OFFSET, DISTRIBUTOR_PROGRAM_ID } from "./constants";
Expand Down Expand Up @@ -365,7 +366,7 @@ export default class SolanaDistributorClient {
return MerkleDistributor.fetchMultiple(this.connection, distributorPublicKeys, this.programId);
}

public async searchDistributors(data: ISearchDistributors): Promise<MerkleDistributor[]> {
public async searchDistributors(data: ISearchDistributors): Promise<IProgramAccount<MerkleDistributor>[]> {
const filters: MemcmpFilter[] = [{ memcmp: { offset: 0, bytes: bs58.encode(MerkleDistributor.discriminator) } }];
if (data.mint) {
filters.push({
Expand All @@ -385,8 +386,9 @@ export default class SolanaDistributorClient {
}
const accounts = await this.connection.getProgramAccounts(this.programId, { filters });

return accounts.map(({ account }) => {
return MerkleDistributor.decode(account.data);
});
return accounts.map(({ pubkey, account }) => ({
publicKey: pubkey,
account: MerkleDistributor.decode(account.data),
}));
}
}
2 changes: 1 addition & 1 deletion packages/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@streamflow/eslint-config",
"version": "6.3.10",
"version": "6.4.0",
"license": "ISC",
"main": "index.js",
"files": [
Expand Down
16 changes: 16 additions & 0 deletions packages/stream/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,22 @@ try {
}
```

## Search Solana Streams

Solana RPC is pretty rich in what data it can allow to filter by, so we expose a separate `searchStreams` method on `SolanaStreamClient`:

```javascript
// All parameters are optional, so in theory you can just fetch all Streams
const params = {
mint: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
sender: "AKp8CxGbhsrsEsCFUtx7e3MWyW7SWi1uuSqv6N4BEohJ",
recipient: "9mqcpDjCHCPmttJp2t477oJ71NdAvJeSus8BcCrrvwy5",
}
// nativeStreamClient is exposed on a GenericStreamClient, you can also use SolanaStreamClient directly
// Return an Array of objects {publicKey: PublicKey, account: Stream}
const streams = await client.nativeStreamClient.searchStreams(params);
```

## Handling errors

`GenericStreamClient` wraps all errors when making on-chain calls with `ContractError` error class:
Expand Down
2 changes: 1 addition & 1 deletion packages/stream/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@streamflow/stream",
"version": "6.3.10",
"version": "6.4.0",
"description": "JavaScript SDK to interact with Streamflow protocol.",
"homepage": "https://github.com/streamflow-finance/js-sdk/",
"main": "dist/index.js",
Expand Down
22 changes: 22 additions & 0 deletions packages/stream/solana/StreamClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ConnectionConfig,
TransactionMessage,
VersionedTransaction,
MemcmpFilter,
} from "@solana/web3.js";
import {
CheckAssociatedTokenAccountsData,
Expand All @@ -29,6 +30,7 @@ import {
executeTransaction,
executeMultipleTransactions,
buildSendThrottler,
IProgramAccount,
} from "@streamflow/common/solana";
import * as borsh from "borsh";

Expand All @@ -40,6 +42,7 @@ import {
ICreateStreamSolanaExt,
IInteractStreamSolanaExt,
ITopUpStreamSolanaExt,
ISearchStreams,
} from "./types";
import {
decodeStream,
Expand All @@ -59,6 +62,7 @@ import {
PARTNER_ORACLE_PROGRAM_ID,
FEES_METADATA_SEED,
PARTNERS_SCHEMA,
STREAM_STRUCT_OFFSETS,
} from "./constants";
import {
withdrawStreamInstruction,
Expand Down Expand Up @@ -890,6 +894,24 @@ export default class SolanaStreamClient extends BaseStreamClient {
return sortedStreams.filter((stream) => stream[1].type === type);
}

public async searchStreams(data: ISearchStreams): Promise<IProgramAccount<Stream>[]> {
const filters: MemcmpFilter[] = Object.entries(data)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.filter(([_, value]) => value) // Only keep entries where the value is truthy
.map(([key, value]) => ({
memcmp: {
offset: STREAM_STRUCT_OFFSETS[key as keyof ISearchStreams],
bytes: value,
},
}));
const accounts = await this.connection.getProgramAccounts(this.programId, { filters });

return accounts.map(({ pubkey, account }) => ({
publicKey: pubkey,
account: new Contract(decodeStream(account.data)),
}));
}

/**
* Attempts updating the stream auto withdrawal params and amount per period
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/stream/solana/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { PublicKey } from "@solana/web3.js";

import { ICluster, SolanaContractErrorCode } from "../common/types";
import { ISearchStreams } from "./types";

export const TX_FINALITY_CONFIRMED = "confirmed";

export const STREAM_STRUCT_OFFSET_SENDER = 49;
export const STREAM_STRUCT_OFFSET_RECIPIENT = 113;
export const STREAM_STRUCT_OFFSET_MINT = 177;

export const STREAM_STRUCT_OFFSETS: Record<keyof ISearchStreams, number> = {
mint: STREAM_STRUCT_OFFSET_MINT,
recipient: STREAM_STRUCT_OFFSET_RECIPIENT,
sender: STREAM_STRUCT_OFFSET_SENDER,
};

// Defined: https://github.com/streamflow-finance/protocol/blob/main/programs/protocol/src/state.rs#L25
export const CREATE_PARAMS_PADDING = 126;
Expand Down
6 changes: 6 additions & 0 deletions packages/stream/solana/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import { buildStreamType, calculateUnlockedAmount } from "../common/contractUtil
import { IRecipient, Stream, StreamType } from "../common/types";
import { getNumberFromBN } from "../common/utils";

export interface ISearchStreams {
mint?: string;
sender?: string;
recipient?: string;
}

export interface Account {
pubkey: PublicKey;
account: AccountInfo<Buffer>;
Expand Down

0 comments on commit 426489d

Please sign in to comment.