diff --git a/lerna.json b/lerna.json index d65f93fd..bb80db99 100644 --- a/lerna.json +++ b/lerna.json @@ -2,6 +2,6 @@ "packages": [ "packages/*" ], - "version": "5.7.1", + "version": "5.8.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 35235f6c..74362c46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22169,7 +22169,7 @@ }, "packages/stream": { "name": "@streamflow/stream", - "version": "5.7.1", + "version": "5.8.0", "dependencies": { "@manahippo/aptos-wallet-adapter": "1.0.6", "@mysten/sui.js": "^0.40.0", @@ -22181,6 +22181,7 @@ "@suiet/wallet-kit": "0.2.18", "aptos": "1.4.0", "bn.js": "5.2.1", + "borsh": "^2.0.0", "bs58": "5.0.0", "ethereum-checksum-address": "0.0.8", "ethers": "5.7.2", @@ -22202,6 +22203,11 @@ "typescript": "4.6.4" } }, + "packages/stream/node_modules/borsh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-2.0.0.tgz", + "integrity": "sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==" + }, "packages/stream/node_modules/jest": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/jest/-/jest-29.3.1.tgz", diff --git a/packages/stream/aptos/StreamClient.ts b/packages/stream/aptos/StreamClient.ts index 78c946d2..73004099 100644 --- a/packages/stream/aptos/StreamClient.ts +++ b/packages/stream/aptos/StreamClient.ts @@ -8,7 +8,9 @@ import { ICreateMultipleStreamData, ICreateResult, ICreateStreamData, + IGetFeesData, IGetOneData, + IFees, IMultiTransactionResult, IRecipient, ITopUpData, @@ -18,7 +20,14 @@ import { IWithdrawData, } from "../common/types"; import { APTOS_PROGRAM_IDS } from "./constants"; -import { Contract, ICreateStreamAptosExt, ITransactionAptosExt, StreamResource } from "./types"; +import { + ConfigResource, + Contract, + FeeTableResource, + ICreateStreamAptosExt, + ITransactionAptosExt, + StreamResource, +} from "./types"; import { AptosWalletWrapper } from "./wallet"; import { extractAptosErrorCode } from "./utils"; @@ -237,6 +246,31 @@ export default class AptosStreamClient extends BaseStreamClient { return { ixs: [payload], txId: hash }; } + public async getFees({ address }: IGetFeesData): Promise { + const resource = await this.client.getAccountResource( + this.programId, + `${this.programId}::fees::FeeTable` + ); + const data = resource.data as unknown as FeeTableResource; + const value = await this.client.getTableItem(data.values.handle, { + key_type: "address", + key: address, + value_type: "u64", + }); + if (!value) { + return null; + } + return { streamflowFee: Number(value) / 100, partnerFee: 0 }; + } + + public async getDefaultStreamflowFee(): Promise { + const resource = await this.client.getAccountResource( + this.programId, + `${this.programId}::admin::ConfigV2` + ); + return Number((resource.data as unknown as ConfigResource).streamflow_fees) / 100; + } + public extractErrorCode(err: Error): string | null { return extractAptosErrorCode(err.toString() ?? "Unknown error!"); } diff --git a/packages/stream/aptos/types.ts b/packages/stream/aptos/types.ts index 4ec5101a..2572110a 100644 --- a/packages/stream/aptos/types.ts +++ b/packages/stream/aptos/types.ts @@ -54,6 +54,20 @@ export interface StreamResource { withdrawn: string; } +export interface FeeTableResource { + values: { + handle: string; + }; +} + +export interface ConfigResource { + admin: string; + streamflow_fees: string; + treasury: string; + tx_fee: string; + withdrawor: string; +} + export class Contract implements Stream { magic: number; diff --git a/packages/stream/common/BaseStreamClient.ts b/packages/stream/common/BaseStreamClient.ts index 3e000574..06e8ea32 100644 --- a/packages/stream/common/BaseStreamClient.ts +++ b/packages/stream/common/BaseStreamClient.ts @@ -1,10 +1,12 @@ import { ICreateStreamData, ICreateMultipleStreamData, + IFees, IWithdrawData, ICancelData, ITransferData, ITopUpData, + IGetFeesData, IGetOneData, IUpdateData, ITransactionResult, @@ -42,6 +44,10 @@ export abstract class BaseStreamClient { abstract update(updateData: IUpdateData, chainSpecificParams: any): Promise; + abstract getFees(getFeesData: IGetFeesData, chainSpecificParams: any): Promise; + + abstract getDefaultStreamflowFee(chainSpecificParams: any): Promise; + // eslint-disable-next-line @typescript-eslint/no-unused-vars extractErrorCode(err: Error): string | null { return null; diff --git a/packages/stream/common/GenericStreamClient.ts b/packages/stream/common/GenericStreamClient.ts index 3e08d081..0e69ddf0 100644 --- a/packages/stream/common/GenericStreamClient.ts +++ b/packages/stream/common/GenericStreamClient.ts @@ -18,6 +18,8 @@ import { ICreateResult, IGetAllData, Stream, + IFees, + IGetFeesData, } from "./types"; import { handleContractError } from "./utils"; import { AptosStreamClient, ICreateStreamAptosExt, ITransactionAptosExt } from "../aptos"; @@ -262,4 +264,18 @@ export default class GenericStreamClient extends BaseStreamCli this.nativeStreamClient.extractErrorCode ); } + + /** + * Returns streamflow and partner fees for the specific wallet in % + */ + public getFees(getFeesData: IGetFeesData): Promise { + return this.nativeStreamClient.getFees(getFeesData); + } + + /** + * Returns default Streamflow Fee in % + */ + public getDefaultStreamflowFee(): Promise { + return this.nativeStreamClient.getDefaultStreamflowFee(); + } } diff --git a/packages/stream/common/types.ts b/packages/stream/common/types.ts index 01198c42..fad26bee 100644 --- a/packages/stream/common/types.ts +++ b/packages/stream/common/types.ts @@ -64,6 +64,10 @@ export interface IGetOneData { id: string; } +export interface IGetFeesData { + address: string; +} + export interface IGetAllData { address: string; type?: StreamType; @@ -83,6 +87,11 @@ export interface ITransactionResult { txId: string; } +export interface IFees { + streamflowFee: number; + partnerFee: number; +} + export interface ICreateResult extends ITransactionResult { metadataId: MetadataId; } diff --git a/packages/stream/evm/StreamClient.ts b/packages/stream/evm/StreamClient.ts index 70ca0a45..25f580fb 100644 --- a/packages/stream/evm/StreamClient.ts +++ b/packages/stream/evm/StreamClient.ts @@ -10,8 +10,10 @@ import { ICreateMultipleStreamData, ICreateResult, ICreateStreamData, + IGetFeesData, IGetAllData, IGetOneData, + IFees, IMultiTransactionResult, IRecipient, ITopUpData, @@ -26,7 +28,7 @@ import { BNB_PROGRAM_IDS, ETHEREUM_PROGRAM_IDS, POLYGON_PROGRAM_IDS } from "./co import abi from "./abi"; import ercAbi from "./ercAbi"; import { BASE_FEE } from "../common/constants"; -import { EvmContract, StreamAbiResult } from "./types"; +import { EvmContract, FeesAbiResult, StreamAbiResult } from "./types"; import { extractEvmErrorCode } from "./utils"; export default class EvmStreamClient extends BaseStreamClient { @@ -251,6 +253,22 @@ export default class EvmStreamClient extends BaseStreamClient { return extractEvmErrorCode(err.toString() ?? "Unknown error!"); } + public async getFees({ address }: IGetFeesData): Promise { + const fees: FeesAbiResult = await this.readContract.getFees(address); + if (!fees.exists) { + return null; + } + return { + streamflowFee: fees.streamflow_fee.toNumber() / 100, + partnerFee: fees.partner_fee.toNumber() / 100, + }; + } + + public async getDefaultStreamflowFee(): Promise { + const fee = await this.readContract.getStreamflowFees(); + return fee.toNumber() / 100; + } + /** * Returns StreamClient protocol program ID. */ diff --git a/packages/stream/evm/types.ts b/packages/stream/evm/types.ts index 6ddf0db4..0679d550 100644 --- a/packages/stream/evm/types.ts +++ b/packages/stream/evm/types.ts @@ -48,6 +48,12 @@ export interface StreamAbiResult { withdrawn: BigNumber; } +export interface FeesAbiResult { + exists: boolean; + streamflow_fee: BigNumber; + partner_fee: BigNumber; +} + export class EvmContract implements Stream { magic: number; diff --git a/packages/stream/package.json b/packages/stream/package.json index 7c614d1f..1a6be72f 100644 --- a/packages/stream/package.json +++ b/packages/stream/package.json @@ -1,6 +1,6 @@ { "name": "@streamflow/stream", - "version": "5.7.1", + "version": "5.8.0", "description": "JavaScript SDK to interact with Streamflow protocol.", "main": "dist/index.js", "homepage": "https://github.com/streamflow-finance/js-sdk/", @@ -38,6 +38,7 @@ "@suiet/wallet-kit": "0.2.18", "aptos": "1.4.0", "bn.js": "5.2.1", + "borsh": "^2.0.0", "bs58": "5.0.0", "ethereum-checksum-address": "0.0.8", "ethers": "5.7.2", diff --git a/packages/stream/solana/StreamClient.ts b/packages/stream/solana/StreamClient.ts index 0c76bda7..ebeaf0be 100644 --- a/packages/stream/solana/StreamClient.ts +++ b/packages/stream/solana/StreamClient.ts @@ -17,6 +17,7 @@ import { sendAndConfirmRawTransaction, BlockheightBasedTransactionConfirmationStrategy, } from "@solana/web3.js"; +import * as borsh from "borsh"; import { Account, @@ -46,6 +47,10 @@ import { TX_FINALITY_CONFIRMED, WITHDRAWOR_PUBLIC_KEY, FEE_ORACLE_PUBLIC_KEY, + DEFAULT_STREAMFLOW_FEE, + PARTNER_ORACLE_PROGRAM_ID, + FEES_METADATA_SEED, + PARTNERS_SCHEMA, } from "./constants"; import { withdrawStreamInstruction, @@ -64,7 +69,9 @@ import { ICreateResult, ICreateStreamData, IGetAllData, + IGetFeesData, IGetOneData, + IFees, IMultiTransactionResult, IRecipient, IStreamConfig, @@ -79,6 +86,7 @@ import { ICreateMultiError, } from "../common/types"; import { BaseStreamClient } from "../common/BaseStreamClient"; +import { IPartnerLayout } from "./instructionTypes"; const METADATA_ACC_SIZE = 1104; @@ -801,6 +809,32 @@ export default class SolanaStreamClient extends BaseStreamClient { }; } + public async getFees({ address }: IGetFeesData): Promise { + const [metadataPubKey] = PublicKey.findProgramAddressSync( + [Buffer.from(FEES_METADATA_SEED)], + new PublicKey(PARTNER_ORACLE_PROGRAM_ID) + ); + const data = await this.connection.getAccountInfo(metadataPubKey); + if (!data) { + return null; + } + const partners = borsh.deserialize(PARTNERS_SCHEMA, data!.data) as unknown as IPartnerLayout[]; + const filteredPartners = partners.filter( + (item) => new PublicKey(item.pubkey).toString() === address + ); + if (filteredPartners.length === 0) { + return null; + } + return { + streamflowFee: Number(filteredPartners[0].strm_fee.toFixed(4)), + partnerFee: Number(filteredPartners[0].partner_fee.toFixed(4)), + }; + } + + public async getDefaultStreamflowFee(): Promise { + return DEFAULT_STREAMFLOW_FEE; + } + public extractErrorCode(err: Error): string | null { return extractSolanaErrorCode(err.toString() ?? "Unknown error!"); } diff --git a/packages/stream/solana/constants.ts b/packages/stream/solana/constants.ts index 43014766..192f4cb8 100644 --- a/packages/stream/solana/constants.ts +++ b/packages/stream/solana/constants.ts @@ -21,6 +21,8 @@ export const STREAMFLOW_PROGRAM_ID = "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5 export const STREAMFLOW_DEVNET_PROGRAM_ID = "FGjLaVo5zLGdzCxMo9gu9tXr1kzTToKd8C8K7YS5hNM1"; +export const PARTNER_ORACLE_PROGRAM_ID = "pardpVtPjC8nLj1Dwncew62mUzfChdCX1EaoZe8oCAa"; + export const STREAMFLOW_TREASURY_PUBLIC_KEY = new PublicKey( "5SEpbdjFK5FxwTvfsGMXVQTD2v4M2c5tyRTxhdsPkgDw" ); @@ -31,8 +33,17 @@ export const FEE_ORACLE_PUBLIC_KEY = new PublicKey("B743wFVk2pCYhV91cn287e1xY7f1 export const AIRDROP_TEST_TOKEN = "Gssm3vfi8s65R31SBdmQRq6cKeYojGgup7whkw4VCiQj"; +export const FEES_METADATA_SEED = Buffer.from("strm_fees"); + +export const DEFAULT_STREAMFLOW_FEE = 0.99; + export const AIRDROP_AMOUNT = 1; // 1 SOL is the cap on the testnet +export const PARTNER_SCHEMA = { + struct: { pubkey: { array: { type: "u8", len: 32 } }, partner_fee: "f32", strm_fee: "f32" }, +}; +export const PARTNERS_SCHEMA = { array: { type: PARTNER_SCHEMA } }; + export const SOLANA_ERROR_MATCH_REGEX = /custom program error: (0x\d{2})/; export const SOLANA_ERROR_MAP: { [key: number]: string } = { diff --git a/packages/stream/solana/instructionTypes.ts b/packages/stream/solana/instructionTypes.ts index d1b796be..61da3c30 100644 --- a/packages/stream/solana/instructionTypes.ts +++ b/packages/stream/solana/instructionTypes.ts @@ -99,3 +99,9 @@ export interface IUpdateStreamLayout { export interface ITopupStreamLayout { amount: Uint8Array; } + +export interface IPartnerLayout { + pubkey: Uint8Array; + partner_fee: number; + strm_fee: number; +} diff --git a/packages/stream/solana/layout.ts b/packages/stream/solana/layout.ts index 2b7b362e..0035a67d 100644 --- a/packages/stream/solana/layout.ts +++ b/packages/stream/solana/layout.ts @@ -4,6 +4,7 @@ import { CREATE_PARAMS_PADDING } from "./constants"; import { ICreateStreamLayout, ICreateUncheckedStreamLayout, + IPartnerLayout, IStreamLayout, ITopupStreamLayout, IUpdateStreamLayout, @@ -66,6 +67,12 @@ export const streamLayout: BufferLayout.Structure = BufferLayout. BufferLayout.blob(8, "funds_unlocked_at_last_rate_change"), ]); +export const partnerLayout: BufferLayout.Structure = BufferLayout.struct([ + BufferLayout.blob(32, "pubkey"), + BufferLayout.f32("partner_fee"), + BufferLayout.f32("strm_fee"), +]); + /** * Create stream instruction layout */ diff --git a/packages/stream/sui/StreamClient.ts b/packages/stream/sui/StreamClient.ts index 7563aa2a..26b5b99e 100644 --- a/packages/stream/sui/StreamClient.ts +++ b/packages/stream/sui/StreamClient.ts @@ -11,7 +11,9 @@ import { ICreateMultipleStreamData, ICreateResult, ICreateStreamData, + IGetFeesData, IGetOneData, + IFees, IMultiTransactionResult, IRecipient, ITopUpData, @@ -29,6 +31,9 @@ import { ITransactionSuiExt, ISuiIdParameters, StreamResource, + ClassResource, + FeeTableResource, + FeeValueResource, } from "./types"; import { extractSuiErrorInfo } from "./utils"; import { SuiWalletWrapper } from "./wallet"; @@ -338,6 +343,65 @@ export default class SuiStreamClient extends BaseStreamClient { return errorInfo?.parsed?.name || null; } + public async getFees({ address }: IGetFeesData): Promise { + const response = await this.client.getObject({ + id: this.feeTableId, + options: { + showContent: true, + }, + }); + + const content = response.data!.content!; + if (content.dataType !== "moveObject") { + throw new Error(`Not a Move Object!`); + } + const fields = content.fields as unknown as FeeTableResource; + + const fieldsResponse = await this.client.getDynamicFields({ + parentId: fields.values.fields.id.id, + }); + const partnerDynamicField = fieldsResponse.data.filter( + (item) => item.name.value === address + )[0]; + + if (!partnerDynamicField) { + return null; + } + + const valueResponse = await this.client.getObject({ + id: partnerDynamicField.objectId, + options: { + showContent: true, + }, + }); + const valueContent = valueResponse.data!.content!; + if (valueContent.dataType !== "moveObject") { + throw new Error(`Not a Move Object!`); + } + const valueFields = (valueContent.fields as unknown as FeeValueResource).value.fields; + + return { + streamflowFee: Number(valueFields.streamflow_fee) / 100, + partnerFee: Number(valueFields.partner_fee) / 100, + }; + } + + public async getDefaultStreamflowFee(): Promise { + const response = await this.client.getObject({ + id: this.configId, + options: { + showContent: true, + }, + }); + + const content = response.data!.content!; + if (content.dataType !== "moveObject") { + throw new Error(`Not a Move Object!`); + } + const fields = content.fields as unknown as ClassResource; + return Number(fields.streamflow_fee) / 100; + } + /** * Returns StreamClient protocol program ID. */ diff --git a/packages/stream/sui/types.ts b/packages/stream/sui/types.ts index fa16806f..906ab1e3 100644 --- a/packages/stream/sui/types.ts +++ b/packages/stream/sui/types.ts @@ -87,6 +87,35 @@ export interface StreamResource { withdrawn: string; } +export interface ClassResource { + streamflow_fee: string; + treasury: string; + tx_fee: string; + withdrawor: string; +} + +export interface FeeTableResource { + values: { + type: string; + fields: { + id: { + id: string; + }; + }; + }; +} + +export interface FeeValueResource { + name: string; + value: { + type: string; + fields: { + partner_fee: string; + streamflow_fee: string; + }; + }; +} + export class Contract implements Stream { magic: number;