Skip to content

Commit 35bfe08

Browse files
Dynamic Airdop fees (#308)
* Airdrop Dynamic Fees * Lint fixes * Bump * Minor fixes * Alpha release * chore: prepare publish (built & versioned) * Proper version bump * Remove pnpm scrap * Removed unused stuff * Cleanup * PR fixes * API failsafe
1 parent 94c0898 commit 35bfe08

File tree

16 files changed

+377
-14
lines changed

16 files changed

+377
-14
lines changed

.vscode/settings.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@
88
"pattern": "./packages/*/"
99
}
1010
],
11-
"cSpell.words": ["Blockhash", "merkle", "permissionless", "Solana", "Streamflow", "Unstake"],
11+
"cSpell.words": [
12+
"Blockhash",
13+
"lamports",
14+
"merkle",
15+
"permissionless",
16+
"Solana",
17+
"Streamflow",
18+
"Unstake"
19+
],
1220
"typescript.tsdk": "node_modules/typescript/lib",
1321
"javascript.preferences.importModuleSpecifierEnding": "js",
1422
"typescript.preferences.importModuleSpecifierEnding": "js",

lerna.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"packages": [
33
"packages/*"
44
],
5-
"version": "8.9.0",
5+
"version": "9.0.0",
66
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
77
"command": {
88
"run": {

packages/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./types.js";
22
export * from "./lib/assertions.js";
33
export * from "./lib/utils.js";
4+
export * from "./lib/fetch-token-price.js";
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ICluster } from "../types.js";
2+
3+
export type TokensPricesResponse = {
4+
data: Record<string, TokenPriceResult>;
5+
};
6+
7+
export type TokenPriceResult = { id: string; value: number | undefined };
8+
9+
export interface FetchTokenPriceOptions {
10+
fetchImpl?: typeof fetch;
11+
timeoutMs?: number;
12+
}
13+
14+
export const fetchTokenPrice = async (
15+
mintId: string,
16+
cluster: ICluster = ICluster.Mainnet,
17+
options?: FetchTokenPriceOptions,
18+
): Promise<TokenPriceResult> => {
19+
const url = `https://token-api.streamflow.finance/price?ids=${encodeURIComponent(mintId)}&cluster=${encodeURIComponent(cluster)}`;
20+
21+
const impl = options?.fetchImpl ?? fetch;
22+
const controller = new AbortController();
23+
const timeout = options?.timeoutMs
24+
? setTimeout(() => controller.abort(), options.timeoutMs)
25+
: undefined;
26+
27+
try {
28+
const res = await impl(url, {
29+
headers: { "Content-Type": "application/json" },
30+
signal: controller.signal,
31+
});
32+
if (!res.ok) {
33+
throw new Error(`Price API error: ${res.status} ${res.statusText}`);
34+
}
35+
const json = (await res.json()) as TokensPricesResponse;
36+
const entry = json?.data?.[mintId];
37+
return { id: mintId, value: typeof entry?.value === "number" ? entry.value : undefined };
38+
} finally {
39+
if (timeout) clearTimeout(timeout);
40+
}
41+
};

packages/common/lib/utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,22 @@ export function sleep(ms: number): Promise<void> {
6161
}
6262

6363
export const divCeilN = (n: bigint, d: bigint): bigint => n / d + (n % d ? BigInt(1) : BigInt(0));
64+
65+
/**
66+
* Multiply a bigint by a JS number using string-based fixed-point to avoid overflow/precision issues.
67+
* Returns floor(x * y).
68+
*/
69+
export function multiplyBigIntByNumber(x: bigint, y: number, scaleDigits = 9): bigint {
70+
if (!Number.isFinite(y) || y === 0) return 0n;
71+
const isNegative = (x < 0n) !== (y < 0);
72+
const absX = x < 0n ? -x : x;
73+
const absY = Math.abs(y);
74+
75+
const s = absY.toFixed(scaleDigits);
76+
const [intPart, fracPartRaw = ""] = s.split(".");
77+
const fracPart = fracPartRaw.padEnd(scaleDigits, "0").slice(0, scaleDigits);
78+
const yScaled = BigInt(intPart + fracPart); // absY * 10^scaleDigits
79+
const scale = 10n ** BigInt(scaleDigits);
80+
const product = (absX * yScaled) / scale;
81+
return isNegative ? -product : product;
82+
}

packages/common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@streamflow/common",
3-
"version": "8.9.0",
3+
"version": "9.0.0",
44
"description": "Common utilities and types used by streamflow packages.",
55
"homepage": "https://github.com/streamflow-finance/js-sdk/",
66
"type": "module",

packages/distributor/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@streamflow/distributor",
3-
"version": "8.9.0",
3+
"version": "9.0.0",
44
"description": "JavaScript SDK to interact with Streamflow Airdrop protocol.",
55
"homepage": "https://github.com/streamflow-finance/js-sdk/",
66
"main": "./dist/cjs/index.cjs",

packages/distributor/solana/clients/BaseDistributorClient.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
DISTRIBUTOR_PROGRAM_ID,
3535
STREAMFLOW_TREASURY_PUBLIC_KEY,
3636
} from "../constants.js";
37+
import { MINIMUM_FEE_FALLBACK, resolveAirdropFeeLamportsUsingApi } from "../fees.js";
3738
import MerkleDistributorIDL from "../descriptor/idl/merkle_distributor.json";
3839
import type { MerkleDistributor as MerkleDistributorProgramType } from "../descriptor/merkle_distributor.js";
3940
import { MerkleDistributor } from "../generated/accounts/index.js";
@@ -90,6 +91,8 @@ export default abstract class BaseDistributorClient {
9091

9192
public merkleDistributorProgram: Program<MerkleDistributorProgramType>;
9293

94+
protected cluster: ICluster;
95+
9396
public constructor({
9497
clusterUrl,
9598
cluster = ICluster.Mainnet,
@@ -99,6 +102,7 @@ export default abstract class BaseDistributorClient {
99102
sendThrottler,
100103
}: IInitOptions) {
101104
this.commitment = commitment;
105+
this.cluster = cluster;
102106
this.connection = new Connection(clusterUrl, this.commitment);
103107
this.programId = programId !== "" ? new PublicKey(programId) : new PublicKey(DISTRIBUTOR_PROGRAM_ID[cluster]);
104108
this.sendThrottler = sendThrottler ?? buildSendThrottler(sendRate);
@@ -360,12 +364,40 @@ export default abstract class BaseDistributorClient {
360364
ixs.push(claimLocked(accounts, this.programId));
361365
}
362366

363-
ixs.push(
364-
this.prepareClaimFeeInstruction(
365-
extParams.feePayer ?? extParams.invoker.publicKey,
366-
typeof _serviceTransfer === "bigint" ? _serviceTransfer : undefined,
367-
),
368-
);
367+
// Determine fee: prefer service internal (if provided by service), else fetch params from API
368+
// and compute dynamically through resolver, else default minimum
369+
let feeLamports = typeof _serviceTransfer === "bigint" ? _serviceTransfer : undefined;
370+
if (!feeLamports) {
371+
try {
372+
const { mint: mintAccount } = await getMintAndProgram(this.connection, distributor.mint);
373+
// Backward-compatible: compute claimable amount if not provided by the caller
374+
// Prefer explicit field if present; otherwise default to unlocked + locked inputs
375+
const claimableAmountRaw = (data as unknown as {
376+
// optional for legacy callers
377+
claimableAmount?: BN | bigint | number | string;
378+
})?.claimableAmount;
379+
// Default legacy fields to 0 when missing
380+
const unlockedRaw = (data as unknown as { amountUnlocked?: BN | number | string })?.amountUnlocked;
381+
const lockedRaw = (data as unknown as { amountLocked?: BN | number | string })?.amountLocked;
382+
const unlocked = unlockedRaw != null ? new BN(unlockedRaw) : new BN(0);
383+
const locked = lockedRaw != null ? new BN(lockedRaw) : new BN(0);
384+
const computedFallback = unlocked.add(locked);
385+
const claimableAmount =
386+
claimableAmountRaw != null
387+
? BigInt(claimableAmountRaw.toString())
388+
: BigInt(computedFallback.toString());
389+
390+
feeLamports = await resolveAirdropFeeLamportsUsingApi({
391+
distributorAddress: distributorPublicKey.toBase58(),
392+
mintAccount,
393+
claimableAmount,
394+
cluster: this.cluster,
395+
});
396+
} catch (_) {
397+
feeLamports = MINIMUM_FEE_FALLBACK;
398+
}
399+
}
400+
ixs.push(this.prepareClaimFeeInstruction(extParams.feePayer ?? extParams.invoker.publicKey, feeLamports));
369401

370402
return ixs;
371403
}

0 commit comments

Comments
 (0)