From 1fe1f2d828e28cfd9e842bb4dd47c9756a39404f Mon Sep 17 00:00:00 2001 From: Tatomir Date: Wed, 16 Oct 2024 18:33:13 +0200 Subject: [PATCH] Adding support for aligned-unlocks to Stream package (#214) * Adding support for aligned-unlocks create and cancel * update get and error handling to supprot aligned unlocks * small refactor + PR comments * address PR comments and get function * strongly typed refactor * minor adjustments * bump version and computLimit fix * fix cancel instruction and end time decoding * remove console.log --- lerna.json | 2 +- packages/common/package.json | 2 +- packages/distributor/package.json | 2 +- packages/eslint-config/package.json | 2 +- packages/staking/package.json | 2 +- packages/stream/aptos/types.ts | 4 +- packages/stream/common/constants.ts | 2 + packages/stream/common/contractUtils.ts | 18 +- packages/stream/common/types.ts | 178 +- packages/stream/evm/types.ts | 4 +- packages/stream/package.json | 3 +- packages/stream/solana/StreamClient.ts | 494 ++++-- packages/stream/solana/constants.ts | 35 +- .../idl/streamflow_aligned_unlocks.json | 1471 +++++++++++++++++ .../descriptor/streamflow_aligned_unlocks.ts | 1272 ++++++++++++++ packages/stream/solana/index.ts | 3 +- packages/stream/solana/lib/derive-accounts.ts | 15 + packages/stream/solana/{ => lib}/utils.ts | 6 +- packages/stream/solana/types.ts | 68 +- packages/stream/sui/types.ts | 4 +- pnpm-lock.yaml | 5 +- 21 files changed, 3369 insertions(+), 223 deletions(-) create mode 100644 packages/stream/solana/descriptor/idl/streamflow_aligned_unlocks.json create mode 100644 packages/stream/solana/descriptor/streamflow_aligned_unlocks.ts create mode 100644 packages/stream/solana/lib/derive-accounts.ts rename packages/stream/solana/{ => lib}/utils.ts (96%) diff --git a/lerna.json b/lerna.json index 63008c7f..5f2eccb6 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "packages": ["packages/*"], - "version": "7.0.0-alpha.12", + "version": "7.0.0-alpha.13", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/packages/common/package.json b/packages/common/package.json index 3020414e..5e9cfcf1 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@streamflow/common", - "version": "7.0.0-alpha.12", + "version": "7.0.0-alpha.13", "description": "Common utilities and types used by streamflow packages.", "homepage": "https://github.com/streamflow-finance/js-sdk/", "main": "./dist/esm/index.js", diff --git a/packages/distributor/package.json b/packages/distributor/package.json index e781ae6a..7f2aa8e6 100644 --- a/packages/distributor/package.json +++ b/packages/distributor/package.json @@ -1,6 +1,6 @@ { "name": "@streamflow/distributor", - "version": "7.0.0-alpha.12", + "version": "7.0.0-alpha.13", "description": "JavaScript SDK to interact with Streamflow Airdrop protocol.", "homepage": "https://github.com/streamflow-finance/js-sdk/", "main": "dist/esm/index.js", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 93a0b723..51a9aa5e 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@streamflow/eslint-config", - "version": "7.0.0-alpha.12", + "version": "7.0.0-alpha.13", "license": "ISC", "main": "index.js", "files": [ diff --git a/packages/staking/package.json b/packages/staking/package.json index 04024af5..979c86df 100644 --- a/packages/staking/package.json +++ b/packages/staking/package.json @@ -1,6 +1,6 @@ { "name": "@streamflow/staking", - "version": "7.0.0-alpha.11", + "version": "7.0.0-alpha.13", "description": "JavaScript SDK to interact with Streamflow Staking protocol.", "homepage": "https://github.com/streamflow-finance/js-sdk/", "main": "dist/esm/index.js", diff --git a/packages/stream/aptos/types.ts b/packages/stream/aptos/types.ts index 08d43320..e6171bbf 100644 --- a/packages/stream/aptos/types.ts +++ b/packages/stream/aptos/types.ts @@ -4,7 +4,7 @@ import BN from "bn.js"; import { Buffer } from "buffer"; import { buildStreamType, calculateUnlockedAmount } from "../common/contractUtils.js"; -import { Stream, StreamType } from "../common/types.js"; +import { LinearStream, StreamType } from "../common/types.js"; import { normalizeAptosAddress } from "./utils.js"; import { getNumberFromBN } from "../common/utils.js"; @@ -70,7 +70,7 @@ export interface ConfigResource { withdrawor: string; } -export class Contract implements Stream { +export class Contract implements LinearStream { magic: number; version: number; diff --git a/packages/stream/common/constants.ts b/packages/stream/common/constants.ts index 57781769..c7337797 100644 --- a/packages/stream/common/constants.ts +++ b/packages/stream/common/constants.ts @@ -1,4 +1,6 @@ import BN from "bn.js"; +export const MAX_SAFE_UNIX_TIME_VALUE = 8640000000000; + export const BASE_FEE = 1009900; // Buffer to include usual fees when calculating stream amount export const WITHDRAW_AVAILABLE_AMOUNT = new BN("18446744073709551615"); // Magical number to withdraw all available amount from a Contract diff --git a/packages/stream/common/contractUtils.ts b/packages/stream/common/contractUtils.ts index 2821e198..057e9988 100644 --- a/packages/stream/common/contractUtils.ts +++ b/packages/stream/common/contractUtils.ts @@ -1,6 +1,7 @@ import BN from "bn.js"; -import { StreamType } from "./types.js"; +import { AlignedStream, ICreateAlignedStreamData, ICreateStreamData, Stream, StreamType } from "./types.js"; +import { MAX_SAFE_UNIX_TIME_VALUE } from "./constants.js"; interface ICalculateUnlockedAmount { depositedAmount: BN; @@ -52,6 +53,14 @@ export const isVesting = (streamData: { canTopup: boolean; depositedAmount: BN; return !streamData.canTopup && !isCliffCloseToDepositedAmount(streamData); }; +export const isAligned = (stream: Stream): stream is AlignedStream => { + return "minPrice" in stream && "maxPrice" in stream && "minPercentage" in stream && "maxPercentage" in stream; +}; + +export const isCreateAlignedStreamData = (obj: ICreateStreamData): obj is ICreateAlignedStreamData => { + return "minPrice" in obj && "maxPrice" in obj && "minPercentage" in obj && "maxPercentage" in obj; +}; + export const isTokenLock = (streamData: { canTopup: boolean; automaticWithdrawal: boolean; @@ -91,3 +100,10 @@ export const buildStreamType = (streamData: { } return StreamType.Payment; }; + +export const decodeEndTime = (endTime: BN): number => { + if (endTime.gt(new BN(MAX_SAFE_UNIX_TIME_VALUE))) { + return MAX_SAFE_UNIX_TIME_VALUE; + } + return endTime.toNumber(); +}; diff --git a/packages/stream/common/types.ts b/packages/stream/common/types.ts index 9c6cf9f4..19d44902 100644 --- a/packages/stream/common/types.ts +++ b/packages/stream/common/types.ts @@ -1,10 +1,10 @@ +import { Address } from "@coral-xyz/anchor"; import { TransactionInstruction } from "@solana/web3.js"; import { Types } from "aptos"; import BN from "bn.js"; export { IChain, ICluster, ContractError } from "@streamflow/common"; -// Stream Client Types export interface IRecipient { recipient: string; amount: BN; @@ -13,7 +13,7 @@ export interface IRecipient { amountPerPeriod: BN; } -export interface IStreamConfig { +export interface IBaseStreamConfig { period: number; start: number; cliff: number; @@ -30,12 +30,31 @@ export interface IStreamConfig { partner?: string; } -export type ICreateStreamData = IStreamConfig & IRecipient; +export type IAlignedStreamConfig = { + minPrice: number; + maxPrice: number; + minPercentage: number; + maxPercentage: number; + oracleType?: OracleTypeName; + priceOracle?: Address; + skipInitial?: boolean; + tickSize?: number; +}; + +export type ICreateLinearStreamData = IBaseStreamConfig & IRecipient; + +export type ICreateAlignedStreamData = ICreateLinearStreamData & IAlignedStreamConfig; + +export type ICreateStreamData = ICreateLinearStreamData | ICreateAlignedStreamData; -export type ICreateMultipleStreamData = IStreamConfig & { +export type ICreateMultipleLinearStreamData = IBaseStreamConfig & { recipients: IRecipient[]; }; +export type ICreateMultipleAlignedStreamData = ICreateMultipleLinearStreamData & IAlignedStreamConfig; + +export type ICreateMultipleStreamData = ICreateMultipleLinearStreamData | ICreateMultipleAlignedStreamData; + export interface IInteractData { id: string; } @@ -101,6 +120,8 @@ export interface IMultiTransactionResult { errors: ICreateMultiError[]; } +export type OracleTypeName = "none" | "pyth" | "switchboard" | "test"; + export enum StreamDirection { Outgoing = "outgoing", Incoming = "incoming", @@ -154,6 +175,72 @@ export enum ContractErrorCode { ENO_RECIPIENT_COIN_ADDRESS = "ENO_RECIPIENT_COIN_ADDRESS", } +// Base types, implemented by each chain package +export interface LinearStream { + magic: number; + version: number; + createdAt: number; + withdrawnAmount: BN; + canceledAt: number; + end: number; + lastWithdrawnAt: number; + sender: string; + senderTokens: string; + recipient: string; + recipientTokens: string; + mint: string; + escrowTokens: string; + streamflowTreasury: string; + streamflowTreasuryTokens: string; + streamflowFeeTotal: BN; + streamflowFeeWithdrawn: BN; + streamflowFeePercent: number; + partnerFeeTotal: BN; + partnerFeeWithdrawn: BN; + partnerFeePercent: number; + partner: string; + partnerTokens: string; + start: number; + depositedAmount: BN; + period: number; + amountPerPeriod: BN; + cliff: number; + cliffAmount: BN; + cancelableBySender: boolean; + cancelableByRecipient: boolean; + automaticWithdrawal: boolean; + transferableBySender: boolean; + transferableByRecipient: boolean; + canTopup: boolean; + name: string; + withdrawalFrequency: number; + closed: boolean; + currentPauseStart: number; + pauseCumulative: BN; + lastRateChangeTime: number; + fundsUnlockedAtLastRateChange: BN; + + type: StreamType; + + unlocked(currentTimestamp: number): BN; + + remaining(decimals: number): number; +} + +export type AlignedStreamData = { + minPrice: number; + maxPrice: number; + minPercentage: number; + maxPercentage: number; + oracleType: OracleTypeName; + priceOracle: string | undefined; + tickSize: number; +}; + +export type AlignedStream = LinearStream & AlignedStreamData; + +export type Stream = LinearStream | AlignedStream; + /** * Error codes raised by Solana protocol specifically */ @@ -206,54 +293,43 @@ export enum SolanaContractErrorCode { MetadataNotRentExempt = "MetadataNotRentExempt", } -// Base types, implemented by each chain package -export interface Stream { - magic: number; - version: number; - createdAt: number; - withdrawnAmount: BN; - canceledAt: number; - end: number; - lastWithdrawnAt: number; - sender: string; - senderTokens: string; - recipient: string; - recipientTokens: string; - mint: string; - escrowTokens: string; - streamflowTreasury: string; - streamflowTreasuryTokens: string; - streamflowFeeTotal: BN; - streamflowFeeWithdrawn: BN; - streamflowFeePercent: number; - partnerFeeTotal: BN; - partnerFeeWithdrawn: BN; - partnerFeePercent: number; - partner: string; - partnerTokens: string; - start: number; - depositedAmount: BN; - period: number; - amountPerPeriod: BN; - cliff: number; - cliffAmount: BN; - cancelableBySender: boolean; - cancelableByRecipient: boolean; - automaticWithdrawal: boolean; - transferableBySender: boolean; - transferableByRecipient: boolean; - canTopup: boolean; - name: string; - withdrawalFrequency: number; - closed: boolean; - currentPauseStart: number; - pauseCumulative: BN; - lastRateChangeTime: number; - fundsUnlockedAtLastRateChange: BN; +export enum SolanaAlignedProxyErrorCode { + /** Authority does not have permission for this action */ + Unauthorized = "Unauthorized", - type: StreamType; + /** Arithmetic error */ + ArithmeticError = "ArithmeticError", - unlocked(currentTimestamp: number): BN; + /** Mint has unsupported Token Extensions */ + UnsupportedTokenExtensions = "UnsupportedTokenExtensions", - remaining(decimals: number): number; + /** Provided period is too short, should be equal or more than 30 seconds */ + PeriodTooShort = "PeriodTooShort", + + /** Provided percentage tick size is invalid */ + InvalidTickSize = "InvalidTickSize", + + /** Provided percentage bounds are invalid */ + InvalidPercentageBoundaries = "InvalidPercentageBoundaries", + + /** Provided price bounds are invalid */ + InvalidPriceBoundaries = "InvalidPriceBoundaries", + + /** Unsupported price oracle */ + UnsupportedOracle = "UnsupportedOracle", + + /** Invalid oracle account */ + InvalidOracleAccount = "InvalidOracleAccount", + + /** Invalid oracle price */ + InvalidOraclePrice = "InvalidOraclePrice", + + /** Invalid Stream Metadata */ + InvalidStreamMetadata = "InvalidStreamMetadata", + + /** Release amount has already been updated in this period */ + AmountAlreadyUpdated = "AmountAlreadyUpdated", + + /** All funds are already unlocked */ + AllFundsUnlocked = "AllFundsUnlocked", } diff --git a/packages/stream/evm/types.ts b/packages/stream/evm/types.ts index 6624d865..c7950098 100644 --- a/packages/stream/evm/types.ts +++ b/packages/stream/evm/types.ts @@ -2,7 +2,7 @@ import BN from "bn.js"; import { BigNumber as BigNumberEvm } from "ethers"; import { buildStreamType, calculateUnlockedAmount } from "../common/contractUtils.js"; -import { Stream, StreamType } from "../common/types.js"; +import { LinearStream, StreamType } from "../common/types.js"; import { getNumberFromBN } from "../common/utils.js"; export interface StreamAbiResult { @@ -54,7 +54,7 @@ export interface FeesAbiResult { partner_fee: BigNumberEvm; } -export class EvmContract implements Stream { +export class EvmContract implements LinearStream { magic: number; version: number; diff --git a/packages/stream/package.json b/packages/stream/package.json index fbcf3d35..a48647f1 100644 --- a/packages/stream/package.json +++ b/packages/stream/package.json @@ -1,6 +1,6 @@ { "name": "@streamflow/stream", - "version": "7.0.0-alpha.12", + "version": "7.0.0-alpha.13", "description": "JavaScript SDK to interact with Streamflow protocol.", "homepage": "https://github.com/streamflow-finance/js-sdk/", "main": "./dist/esm/index.js", @@ -51,6 +51,7 @@ "@types/bn.js": "5.1.1" }, "dependencies": { + "@coral-xyz/anchor": "^0.30.0", "@coral-xyz/borsh": "0.30.1", "@manahippo/aptos-wallet-adapter": "1.0.10", "@mysten/sui.js": "0.54.1", diff --git a/packages/stream/solana/StreamClient.ts b/packages/stream/solana/StreamClient.ts index 21d4247f..94d7ab21 100644 --- a/packages/stream/solana/StreamClient.ts +++ b/packages/stream/solana/StreamClient.ts @@ -33,9 +33,10 @@ import { IProgramAccount, } from "@streamflow/common/solana"; import * as borsh from "borsh"; +import { Program } from "@coral-xyz/anchor"; +import { getBN } from "@streamflow/common"; import { - Account, MetadataRecipientHashMap, Contract, BatchItem, @@ -43,13 +44,17 @@ import { IInteractStreamSolanaExt, ITopUpStreamSolanaExt, ISearchStreams, + ICreateStreamInstructions, + AlignedContract, + DecodedStream, + OracleType, } from "./types.js"; import { decodeStream, extractSolanaErrorCode, sendAndConfirmStreamRawTransaction, signAllTransactionWithRecipients, -} from "./utils.js"; +} from "./lib/utils.js"; import { PROGRAM_ID, STREAMFLOW_TREASURY_PUBLIC_KEY, @@ -63,6 +68,9 @@ import { FEES_METADATA_SEED, PARTNERS_SCHEMA, STREAM_STRUCT_OFFSETS, + ORIGINAL_CONTRACT_SENDER_OFFSET, + ALIGNED_PRECISION_FACTOR_POW, + ALIGNED_COMPUTE_LIMIT, } from "./constants.js"; import { withdrawStreamInstruction, @@ -84,8 +92,6 @@ import { IGetOneData, IFees, IMultiTransactionResult, - IRecipient, - IStreamConfig, ITopUpData, ITransactionResult, ITransferData, @@ -95,11 +101,16 @@ import { StreamType, Stream, ICreateMultiError, + ICreateAlignedStreamData, } from "../common/types.js"; import { BaseStreamClient } from "../common/BaseStreamClient.js"; import { IPartnerLayout } from "./instructionTypes.js"; import { calculateTotalAmountToDeposit } from "../common/utils.js"; import { WITHDRAW_AVAILABLE_AMOUNT } from "../common/constants.js"; +import { StreamflowAlignedUnlocks as AlignedUnlocksProgramType } from "./descriptor/streamflow_aligned_unlocks"; +import StreamflowAlignedUnlocksIDL from "./descriptor/idl/streamflow_aligned_unlocks.json"; +import { deriveContractPDA, deriveEscrowPDA, deriveTestOraclePDA } from "./lib/derive-accounts.js"; +import { isCreateAlignedStreamData } from "../common/contractUtils.js"; const METADATA_ACC_SIZE = 1104; @@ -112,6 +123,8 @@ export class SolanaStreamClient extends BaseStreamClient { private sendThrottler: PQueue; + private alignedProxyProgram: Program; + /** * Create Stream instance */ @@ -128,6 +141,11 @@ export class SolanaStreamClient extends BaseStreamClient { this.connection = new Connection(clusterUrl, this.commitment); this.programId = programId !== "" ? new PublicKey(programId) : new PublicKey(PROGRAM_ID[cluster]); this.sendThrottler = sendThrottler ?? buildSendThrottler(sendRate); + const alignedUnlocksProgram = { + ...StreamflowAlignedUnlocksIDL, + address: StreamflowAlignedUnlocksIDL.address, + } as AlignedUnlocksProgramType; + this.alignedProxyProgram = new Program(alignedUnlocksProgram, { connection: this.connection }); } public getConnection(): Connection { @@ -147,7 +165,25 @@ export class SolanaStreamClient extends BaseStreamClient { * All fees are paid by sender (escrow metadata account rent, escrow token account rent, recipient's associated token account rent, Streamflow's service fee). */ public async create(data: ICreateStreamData, extParams: ICreateStreamSolanaExt): Promise { + const { partner, amount } = data; + const { isNative, sender } = extParams; + + const partnerPublicKey = partner ? new PublicKey(partner) : WITHDRAWOR_PUBLIC_KEY; + + if (!sender.publicKey) { + throw new Error("Sender's PublicKey is not available, check passed wallet adapter!"); + } + const { ixs, metadata, metadataPubKey } = await this.prepareCreateInstructions(data, extParams); + + if (isNative) { + const totalFee = await this.getTotalFee({ + address: partnerPublicKey.toString(), + }); + const totalAmount = calculateTotalAmountToDeposit(amount, totalFee); + ixs.push(...(await prepareWrappedAccount(this.connection, sender.publicKey, totalAmount))); + } + const { tx, hash, context } = await prepareTransaction( this.connection, ixs, @@ -170,11 +206,127 @@ export class SolanaStreamClient extends BaseStreamClient { return { ixs, txId: signature, metadataId: metadataPubKey.toBase58() }; } + async prepareCreateInstructions( + streamParams: ICreateStreamData, + extParams: ICreateStreamSolanaExt, + ): Promise { + const { ixs, metadata, metadataPubKey } = isCreateAlignedStreamData(streamParams) + ? await this.prepareCreateAlignedUnlockInstructions(streamParams, extParams) + : await this.prepareCreateStreamInstructions(streamParams, extParams); + + return { ixs, metadata, metadataPubKey }; + } + + async prepareCreateAlignedUnlockInstructions( + streamParams: ICreateAlignedStreamData, + extParams: ICreateStreamSolanaExt, + ): Promise { + const { + tokenId: mint, + start, + period, + cliff, + canTopup, + cancelableBySender, + cancelableByRecipient, + transferableBySender, + transferableByRecipient, + partner, + recipient, + cliffAmount, + amountPerPeriod, + amount: depositedAmount, + name: streamName, + minPrice, + maxPercentage, + minPercentage, + maxPrice, + skipInitial, + tickSize, + priceOracle, + oracleType, + } = streamParams; + const { isNative, sender, computeLimit, computePrice, metadataPubKeys } = extParams; + + if (!sender.publicKey) { + throw new Error("Sender's PublicKey is not available, check passed wallet adapter!"); + } + + if (!priceOracle && oracleType && oracleType !== "none") { + throw new Error("Price oracle is required for the specified oracle type"); + } + + const recipientPublicKey = new PublicKey(recipient); + const mintPublicKey = isNative ? NATIVE_MINT : new PublicKey(mint); + + const metadata = !metadataPubKeys ? Keypair.generate() : undefined; + const metadataPubKey = metadata ? metadata.publicKey : metadataPubKeys![0]; + + const { tokenProgramId } = await getMintAndProgram(this.connection, mintPublicKey); + const partnerPublicKey = partner ? new PublicKey(partner) : WITHDRAWOR_PUBLIC_KEY; + + const streamflowProgramPublicKey = new PublicKey(this.programId); + + const escrowPDA = deriveEscrowPDA(streamflowProgramPublicKey, metadataPubKey); + + const oracle = + priceOracle ?? deriveTestOraclePDA(this.alignedProxyProgram.programId, mintPublicKey, sender.publicKey); + + const ixs: TransactionInstruction[] = prepareBaseInstructions(this.connection, { + computePrice, + computeLimit: computeLimit ?? ALIGNED_COMPUTE_LIMIT, + }); + + const encodedUIntArray = new TextEncoder().encode(streamName); + const streamNameArray = Array.from(encodedUIntArray); + + const createIx = await this.alignedProxyProgram.methods + .create({ + startTime: new BN(start), + netAmountDeposited: depositedAmount, + period: new BN(period), + amountPerPeriod: amountPerPeriod, + cliff: new BN(cliff), + cliffAmount: cliffAmount, + transferableBySender, + transferableByRecipient, + cancelableByRecipient, + cancelableBySender, + canTopup, + oracleType: (!!oracleType ? { [oracleType]: {} } : { none: {} }) as OracleType, + streamName: streamNameArray, + minPrice: getBN(minPrice, ALIGNED_PRECISION_FACTOR_POW), + maxPrice: getBN(maxPrice, ALIGNED_PRECISION_FACTOR_POW), + minPercentage: getBN(minPercentage, ALIGNED_PRECISION_FACTOR_POW), + maxPercentage: getBN(maxPercentage, ALIGNED_PRECISION_FACTOR_POW), + tickSize: new BN(tickSize || 1), + skipInitial: skipInitial ?? false, + }) + .accountsPartial({ + sender: sender.publicKey, + streamMetadata: metadataPubKey, + escrowTokens: escrowPDA, + mint: mintPublicKey, + partner: partnerPublicKey, + recipient: recipientPublicKey, + withdrawor: WITHDRAWOR_PUBLIC_KEY, + feeOracle: FEE_ORACLE_PUBLIC_KEY, + priceOracle: oracle, + tokenProgram: tokenProgramId, + streamflowProgram: this.programId, + }) + .instruction(); + + ixs.push(createIx); + + return { ixs, metadata, metadataPubKey }; + } + /** * Creates a new stream/vesting contract. * All fees are paid by sender (escrow metadata account rent, escrow token account rent, recipient's associated token account rent, Streamflow's service fee). */ - public async prepareCreateInstructions( + public async prepareCreateStreamInstructions( { recipient, tokenId: mint, @@ -197,11 +349,7 @@ export class SolanaStreamClient extends BaseStreamClient { partner, }: ICreateStreamData, { sender, metadataPubKeys, isNative = false, computePrice, computeLimit }: ICreateStreamSolanaExt, - ): Promise<{ - ixs: TransactionInstruction[]; - metadata: Keypair | undefined; - metadataPubKey: PublicKey; - }> { + ): Promise { if (!sender.publicKey) { throw new Error("Sender's PublicKey is not available, check passed wallet adapter!"); } @@ -228,14 +376,6 @@ export class SolanaStreamClient extends BaseStreamClient { const partnerTokens = await ata(mintPublicKey, partnerPublicKey, tokenProgramId); - if (isNative) { - const totalFee = await this.getTotalFee({ - address: partnerPublicKey.toString(), - }); - const totalAmount = calculateTotalAmountToDeposit(depositedAmount, totalFee); - ixs.push(...(await prepareWrappedAccount(this.connection, sender.publicKey, totalAmount))); - } - ixs.push( createStreamInstruction( { @@ -432,13 +572,11 @@ export class SolanaStreamClient extends BaseStreamClient { */ public async createMultiple( data: ICreateMultipleStreamData, - { sender, metadataPubKeys, isNative = false, computePrice, computeLimit }: ICreateStreamSolanaExt, + extParams: ICreateStreamSolanaExt, ): Promise { - const { recipients } = data; + const { recipients, ...streamParams } = data; - if (!sender.publicKey) { - throw new Error("Sender's PublicKey is not available, check passed wallet adapter!"); - } + const { sender, metadataPubKeys: metadataPubKeysExt, isNative, computePrice, computeLimit } = extParams; const metadatas: string[] = []; const metadataToRecipient: MetadataRecipientHashMap = {}; @@ -450,16 +588,30 @@ export class SolanaStreamClient extends BaseStreamClient { metadata: Keypair | undefined; recipient: string; }[] = []; - metadataPubKeys = metadataPubKeys || []; + const metadataPubKeys = metadataPubKeysExt || []; + + if (recipients.length === 0) { + throw new Error("Recipients array is empty!"); + } + + if (!sender.publicKey) { + throw new Error("Sender's PublicKey is not available, check passed wallet adapter!"); + } for (let i = 0; i < recipients.length; i++) { const recipientData = recipients[i]; - const { ixs, metadata, metadataPubKey } = await this.prepareStreamInstructions(recipientData, data, { + const createStreamData = { ...streamParams, ...recipientData }; + const createStreamExtParams = { sender, metadataPubKeys: metadataPubKeys[i] ? [metadataPubKeys[i]] : undefined, computePrice, computeLimit, - }); + }; + + const { ixs, metadata, metadataPubKey } = await this.prepareCreateInstructions( + createStreamData, + createStreamExtParams, + ); metadataToRecipient[metadataPubKey.toBase58()] = recipientData; @@ -629,8 +781,8 @@ export class SolanaStreamClient extends BaseStreamClient { /** * Attempts canceling the specified stream. */ - public async cancel({ id }: ICancelData, extParams: IInteractStreamSolanaExt): Promise { - const ixs = await this.prepareCancelInstructions({ id }, extParams); + public async cancel(cancelData: ICancelData, extParams: IInteractStreamSolanaExt): Promise { + const ixs = await this.prepareCancelInstructions(cancelData, extParams); const { tx, hash, context } = await prepareTransaction(this.connection, ixs, extParams.invoker.publicKey); const signature = await signAndExecuteTransaction( this.connection, @@ -647,10 +799,74 @@ export class SolanaStreamClient extends BaseStreamClient { return { ixs, txId: signature }; } + public async prepareCancelInstructions( + cancelData: ICancelData, + extParams: IInteractStreamSolanaExt, + ): Promise { + const streamPublicKey = new PublicKey(cancelData.id); + const account = await this.connection.getAccountInfo(streamPublicKey); + if (!account) { + throw new Error("Impossible to cancel a stream contract that does not exist"); + } + const { sender: senderPublicKey } = decodeStream(account.data); + const isAlignedUnlock = this.isAlignedUnlock(streamPublicKey, senderPublicKey); + + const ixs = isAlignedUnlock + ? await this.prepareCancelAlignedUnlockInstructions(cancelData, extParams) + : await this.prepareCancelStreamInstructions(cancelData, extParams); + + return ixs; + } + + public async prepareCancelAlignedUnlockInstructions( + { id }: ICancelData, + { invoker, checkTokenAccounts, computePrice, computeLimit }: IInteractStreamSolanaExt, + ): Promise { + if (!invoker.publicKey) { + throw new Error("Invoker's PublicKey is not available, check passed wallet adapter!"); + } + const streamPublicKey = new PublicKey(id); + const escrowAcc = await this.connection.getAccountInfo(streamPublicKey); + if (!escrowAcc?.data) { + throw new Error("Couldn't get account info"); + } + + const streamflowProgramPublicKey = new PublicKey(this.programId); + const streamData = decodeStream(escrowAcc?.data); + const escrowPDA = deriveEscrowPDA(streamflowProgramPublicKey, streamPublicKey); + const { tokenProgramId } = await getMintAndProgram(this.connection, streamData.mint); + const partnerPublicKey = streamData.partner; + + const ixs: TransactionInstruction[] = prepareBaseInstructions(this.connection, { + computePrice, + computeLimit: computeLimit ?? ALIGNED_COMPUTE_LIMIT, + }); + await this.checkAssociatedTokenAccounts(streamData, { invoker, checkTokenAccounts }, ixs, tokenProgramId); + + const cancelIx = await this.alignedProxyProgram.methods + .cancel() + .accountsPartial({ + sender: invoker.publicKey, + streamMetadata: streamPublicKey, + escrowTokens: escrowPDA, + recipient: streamData.recipient, + partner: partnerPublicKey, + streamflowTreasury: STREAMFLOW_TREASURY_PUBLIC_KEY, + mint: streamData.mint, + tokenProgram: tokenProgramId, + streamflowProgram: this.programId, + }) + .instruction(); + + ixs.push(cancelIx); + + return ixs; + } + /** * Creates Transaction Instructions for cancel */ - public async prepareCancelInstructions( + public async prepareCancelStreamInstructions( { id }: ICancelData, { invoker, checkTokenAccounts, computePrice, computeLimit }: IInteractStreamSolanaExt, ): Promise { @@ -841,12 +1057,67 @@ export class SolanaStreamClient extends BaseStreamClient { * Fetch stream data by its id (address). */ public async getOne({ id }: IGetOneData): Promise { - const escrow = await this.connection.getAccountInfo(new PublicKey(id), TX_FINALITY_CONFIRMED); + const streamPublicKey = new PublicKey(id); + const escrow = await this.connection.getAccountInfo(streamPublicKey, TX_FINALITY_CONFIRMED); if (!escrow?.data) { throw new Error("Couldn't get account info."); } + const stream = decodeStream(escrow.data); + + if (this.isAlignedUnlock(streamPublicKey, stream.sender)) { + const alignedProxy = await this.alignedProxyProgram.account.contract.fetch( + deriveContractPDA(this.alignedProxyProgram.programId, streamPublicKey), + ); + if (!alignedProxy) { + throw new Error("Couldn't get proxy account info."); + } + return new AlignedContract(stream, alignedProxy); + } + return new Contract(stream); + } + + /** + * Fetch all aligned outgoing streams/contracts by the provided public key. + */ + private async getOutgoingAlignedStreams(sender: string): Promise> { + const streams: Record = {}; + + const alignedOutgoingProgramAccounts = await this.alignedProxyProgram.account.contract.all([ + { + memcmp: { + offset: ORIGINAL_CONTRACT_SENDER_OFFSET, + bytes: sender, + }, + }, + ]); + const streamPubKeys = alignedOutgoingProgramAccounts.map((account) => account.account.stream); + const streamAccounts = await this.connection.getMultipleAccountsInfo(streamPubKeys, TX_FINALITY_CONFIRMED); + streamAccounts.forEach((account, index) => { + if (account) { + const alignedData = alignedOutgoingProgramAccounts[index].account; + streams[alignedData.sender.toBase58()] = new AlignedContract(decodeStream(account.data), alignedData); + } + }); - return new Contract(decodeStream(escrow?.data)); + return streams; + } + + private async getIncomingAlignedStreams( + streamRecord: Record, + ): Promise> { + const streams: Record = {}; + const alignedStreamsPubKeys = Object.keys(streamRecord); + const alignedProxyPDAs = alignedStreamsPubKeys.map((streamPubKey) => + deriveContractPDA(this.alignedProxyProgram.programId, new PublicKey(streamPubKey)), + ); + const alignedProxyAccounts = await this.alignedProxyProgram.account.contract.fetchMultiple(alignedProxyPDAs); + alignedProxyAccounts.forEach((account, index) => { + if (account) { + const alignedData = streamRecord[alignedStreamsPubKeys[index]]; + streams[alignedStreamsPubKeys[index]] = new AlignedContract(alignedData, account); + } + }); + return streams; } /** @@ -859,37 +1130,56 @@ export class SolanaStreamClient extends BaseStreamClient { direction = StreamDirection.All, }: IGetAllData): Promise<[string, Stream][]> { const publicKey = new PublicKey(address); - let accounts: Account[] = []; - //todo: we need to be smart with our layout so we minimize rpc calls to the chain - if (direction === "all") { - const outgoingAccounts = await getProgramAccounts( + let streams: Record = {}; + // don't do unnecessary rpc calls if we are not querying for vesting streams + const shouldFetchAligned = type === StreamType.All || type === StreamType.Vesting; + + if (direction !== StreamDirection.Incoming) { + const outgoingStreamAccounts = await getProgramAccounts( this.connection, publicKey, STREAM_STRUCT_OFFSET_SENDER, this.programId, ); - const incomingAccounts = await getProgramAccounts( + outgoingStreamAccounts.forEach((account) => { + streams[account.pubkey.toBase58()] = new Contract(decodeStream(account.account.data)); + }); + + if (shouldFetchAligned) { + const alignedStreams = await this.getOutgoingAlignedStreams(address); + streams = { ...streams, ...alignedStreams }; + } + } + if (direction !== StreamDirection.Outgoing) { + const allIncomingAccounts = await getProgramAccounts( this.connection, publicKey, STREAM_STRUCT_OFFSET_RECIPIENT, this.programId, ); - accounts = [...outgoingAccounts, ...incomingAccounts]; - } else { - const offset = direction === "outgoing" ? STREAM_STRUCT_OFFSET_SENDER : STREAM_STRUCT_OFFSET_RECIPIENT; - accounts = await getProgramAccounts(this.connection, publicKey, offset, this.programId); - } - let streams: Record = {}; + const allIncomingStreams = allIncomingAccounts.map((account) => decodeStream(account.account.data)); - accounts.forEach((account) => { - const decoded = new Contract(decodeStream(account.account.data)); - streams = { ...streams, [account.pubkey.toBase58()]: decoded }; - }); + const alignedDecoded: Record = {}; + + // filter out aligned unlocks and store them in a separate object + allIncomingAccounts.forEach((account, index) => { + if (this.isAlignedUnlock(account.pubkey, allIncomingStreams[index].sender)) { + alignedDecoded[account.pubkey.toBase58()] = allIncomingStreams[index]; + } else { + streams[account.pubkey.toBase58()] = new Contract(allIncomingStreams[index]); + } + }); + + if (shouldFetchAligned) { + const incomingAlignedStreams = await this.getIncomingAlignedStreams(alignedDecoded); + streams = { ...streams, ...incomingAlignedStreams }; + } + } const sortedStreams = Object.entries(streams).sort(([, stream1], [, stream2]) => stream2.start - stream1.start); - if (type === "all") return sortedStreams; + if (type === StreamType.All) return sortedStreams; return sortedStreams.filter((stream) => stream[1].type === type); } @@ -978,7 +1268,7 @@ export class SolanaStreamClient extends BaseStreamClient { if (!data) { return null; } - const partners = borsh.deserialize(PARTNERS_SCHEMA, data!.data) as unknown as IPartnerLayout[]; + 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; @@ -998,106 +1288,6 @@ export class SolanaStreamClient extends BaseStreamClient { return extractSolanaErrorCode(err.toString() ?? "Unknown error!", logs); } - /** - * Forms instructions from params, creates a raw transaction and fetch recent blockhash. - */ - private async prepareStreamInstructions( - recipient: IRecipient, - streamParams: IStreamConfig, - extParams: ICreateStreamSolanaExt, - ): Promise<{ - ixs: TransactionInstruction[]; - metadata: Keypair | undefined; - metadataPubKey: PublicKey; - }> { - const { - tokenId: mint, - start, - period, - cliff, - canTopup, - canUpdateRate, - canPause, - cancelableBySender, - cancelableByRecipient, - transferableBySender, - transferableByRecipient, - automaticWithdrawal = false, - withdrawalFrequency = 0, - partner, - } = streamParams; - - const { sender, metadataPubKeys, computeLimit, computePrice } = extParams; - - if (!sender.publicKey) { - throw new Error("Sender's PublicKey is not available, check passed wallet adapter!"); - } - - const ixs: TransactionInstruction[] = prepareBaseInstructions(this.connection, { - computePrice, - computeLimit, - }); - const recipientPublicKey = new PublicKey(recipient.recipient); - const mintPublicKey = new PublicKey(mint); - const { metadata, metadataPubKey } = this.getOrCreateStreamMetadata(metadataPubKeys); - const [escrowTokens] = PublicKey.findProgramAddressSync( - [Buffer.from("strm"), metadataPubKey.toBuffer()], - this.programId, - ); - - const { tokenProgramId } = await getMintAndProgram(this.connection, mintPublicKey); - const senderTokens = await ata(mintPublicKey, sender.publicKey, tokenProgramId); - const recipientTokens = await ata(mintPublicKey, recipientPublicKey, tokenProgramId); - const streamflowTreasuryTokens = await ata(mintPublicKey, STREAMFLOW_TREASURY_PUBLIC_KEY, tokenProgramId); - const partnerPublicKey = partner ? new PublicKey(partner) : WITHDRAWOR_PUBLIC_KEY; - const partnerTokens = await ata(mintPublicKey, partnerPublicKey, tokenProgramId); - - ixs.push( - createStreamInstruction( - { - start: new BN(start), - depositedAmount: recipient.amount, - period: new BN(period), - amountPerPeriod: recipient.amountPerPeriod, - cliff: new BN(cliff), - cliffAmount: new BN(recipient.cliffAmount), - cancelableBySender, - cancelableByRecipient, - automaticWithdrawal, - transferableBySender, - transferableByRecipient, - canTopup, - canUpdateRate: !!canUpdateRate, - canPause: !!canPause, - name: recipient.name, - withdrawFrequency: new BN(automaticWithdrawal ? withdrawalFrequency : period), - }, - this.programId, - { - sender: sender.publicKey, - senderTokens, - recipient: new PublicKey(recipient.recipient), - metadata: metadataPubKey, - escrowTokens, - recipientTokens, - streamflowTreasury: STREAMFLOW_TREASURY_PUBLIC_KEY, - streamflowTreasuryTokens: streamflowTreasuryTokens, - partner: partnerPublicKey, - partnerTokens: partnerTokens, - mint: new PublicKey(mint), - feeOracle: FEE_ORACLE_PUBLIC_KEY, - rent: SYSVAR_RENT_PUBKEY, - timelockProgram: this.programId, - tokenProgram: tokenProgramId, - associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, - withdrawor: WITHDRAWOR_PUBLIC_KEY, - systemProgram: SystemProgram.programId, - }, - ), - ); - return { ixs, metadata, metadataPubKey }; - } - /** * Utility function to generate metadata for a Contract or return existing Pubkey */ @@ -1115,6 +1305,14 @@ export class SolanaStreamClient extends BaseStreamClient { return { metadata, metadataPubKey }; } + /** + * Utility function that checks whether the associated stream address is an aligned unlock contract, indicated by whether the sender/creator is a PDA + */ + private isAlignedUnlock(streamPublicKey: PublicKey, senderPublicKey: PublicKey) { + const pda = deriveContractPDA(this.alignedProxyProgram.programId, streamPublicKey); + return senderPublicKey.equals(pda); + } + /** * Utility function that checks whether associated token accounts still exist and adds instructions to add them if not */ diff --git a/packages/stream/solana/constants.ts b/packages/stream/solana/constants.ts index 117a57a5..ba534799 100644 --- a/packages/stream/solana/constants.ts +++ b/packages/stream/solana/constants.ts @@ -1,15 +1,19 @@ import { PublicKey } from "@solana/web3.js"; import { Buffer } from "buffer"; -import { ICluster, SolanaContractErrorCode } from "../common/types.js"; +import { ICluster, SolanaContractErrorCode, SolanaAlignedProxyErrorCode } from "../common/types.js"; import { ISearchStreams } from "./types.js"; export const TX_FINALITY_CONFIRMED = "confirmed"; +export const ORIGINAL_CONTRACT_SENDER_OFFSET = 16; + export const STREAM_STRUCT_OFFSET_SENDER = 49; export const STREAM_STRUCT_OFFSET_RECIPIENT = 113; export const STREAM_STRUCT_OFFSET_MINT = 177; +export const ALIGNED_PRECISION_FACTOR_POW = 9; + export const STREAM_STRUCT_OFFSETS: Record = { mint: STREAM_STRUCT_OFFSET_MINT, recipient: STREAM_STRUCT_OFFSET_RECIPIENT, @@ -26,6 +30,22 @@ export const PROGRAM_ID = { [ICluster.Local]: "HqDGZjaVRXJ9MGRQEw7qDc2rAr6iH1n1kAQdCZaCMfMZ", }; +export const ALIGNED_UNLOCKS_PROGRAM_ID = { + [ICluster.Devnet]: "aSTRM2NKoKxNnkmLWk9sz3k74gKBk9t7bpPrTGxMszH", + [ICluster.Mainnet]: "aSTRM2NKoKxNnkmLWk9sz3k74gKBk9t7bpPrTGxMszH", + [ICluster.Testnet]: "aSTRM2NKoKxNnkmLWk9sz3k74gKBk9t7bpPrTGxMszH", + [ICluster.Local]: "aSTRM2NKoKxNnkmLWk9sz3k74gKBk9t7bpPrTGxMszH", +}; + +// Aligned Unlocks Program transactions require a higher comput limit +export const ALIGNED_COMPUTE_LIMIT = 300000; + +export const CONTRACT_DISCRIMINATOR = [172, 138, 115, 242, 121, 67, 183, 26]; +export const TEST_ORACLE_DISCRIMINATOR = [198, 49, 63, 134, 232, 251, 168, 28]; +export const CONTRACT_SEED = Buffer.from("contract", "utf-8"); +export const ESCROW_SEED = Buffer.from("strm", "utf-8"); +export const TEST_ORACLE_SEED = Buffer.from("test-oracle", "utf-8"); + export const PARTNER_ORACLE_PROGRAM_ID = "pardpVtPjC8nLj1Dwncew62mUzfChdCX1EaoZe8oCAa"; export const STREAMFLOW_TREASURY_PUBLIC_KEY = new PublicKey("5SEpbdjFK5FxwTvfsGMXVQTD2v4M2c5tyRTxhdsPkgDw"); @@ -77,4 +97,17 @@ export const SOLANA_ERROR_MAP: { [key: number]: string } = { 0x85: SolanaContractErrorCode.AlreadyPaused, 0x86: SolanaContractErrorCode.NotPaused, 0x87: SolanaContractErrorCode.MetadataNotRentExempt, + 0x1770: SolanaAlignedProxyErrorCode.Unauthorized, + 0x1771: SolanaAlignedProxyErrorCode.ArithmeticError, + 0x1772: SolanaAlignedProxyErrorCode.UnsupportedTokenExtensions, + 0x1773: SolanaAlignedProxyErrorCode.PeriodTooShort, + 0x1774: SolanaAlignedProxyErrorCode.InvalidTickSize, + 0x1775: SolanaAlignedProxyErrorCode.InvalidPercentageBoundaries, + 0x1776: SolanaAlignedProxyErrorCode.InvalidPriceBoundaries, + 0x1777: SolanaAlignedProxyErrorCode.UnsupportedOracle, + 0x1778: SolanaAlignedProxyErrorCode.InvalidOracleAccount, + 0x1779: SolanaAlignedProxyErrorCode.InvalidOraclePrice, + 0x177a: SolanaAlignedProxyErrorCode.InvalidStreamMetadata, + 0x177b: SolanaAlignedProxyErrorCode.AmountAlreadyUpdated, + 0x177c: SolanaAlignedProxyErrorCode.AllFundsUnlocked, }; diff --git a/packages/stream/solana/descriptor/idl/streamflow_aligned_unlocks.json b/packages/stream/solana/descriptor/idl/streamflow_aligned_unlocks.json new file mode 100644 index 00000000..ea7162fa --- /dev/null +++ b/packages/stream/solana/descriptor/idl/streamflow_aligned_unlocks.json @@ -0,0 +1,1471 @@ +{ + "address": "aSTRM2NKoKxNnkmLWk9sz3k74gKBk9t7bpPrTGxMszH", + "metadata": { + "name": "streamflow_aligned_unlocks", + "version": "1.0.0", + "spec": "0.1.0", + "description": "Proxy to update unlock amount within Streamflow vesting protocol according to Token performance and other metrics" + }, + "instructions": [ + { + "name": "cancel", + "discriminator": [ + 232, + 219, + 223, + 41, + 219, + 236, + 220, + 190 + ], + "accounts": [ + { + "name": "sender", + "writable": true, + "signer": true + }, + { + "name": "sender_tokens", + "docs": [ + "Associated token account address of `sender`." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "sender" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "recipient", + "writable": true + }, + { + "name": "recipient_tokens", + "docs": [ + "CHECK; Associated token account address of `recipient`." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "recipient" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "proxy_metadata", + "docs": [ + "Proxy Contract" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 116, + 114, + 97, + 99, + 116 + ] + }, + { + "kind": "account", + "path": "stream_metadata" + } + ] + } + }, + { + "name": "proxy_tokens", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "proxy_metadata" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "stream_metadata", + "writable": true + }, + { + "name": "escrow_tokens", + "writable": true + }, + { + "name": "streamflow_treasury", + "writable": true + }, + { + "name": "streamflow_treasury_tokens", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "streamflow_treasury" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "partner", + "writable": true + }, + { + "name": "partner_tokens", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "partner" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "mint", + "docs": [ + "The SPL token mint account." + ], + "writable": true + }, + { + "name": "streamflow_program", + "address": "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m" + }, + { + "name": "token_program", + "docs": [ + "The SPL program needed in case an associated account", + "for the new recipient is being created." + ] + } + ], + "args": [] + }, + { + "name": "change_oracle", + "discriminator": [ + 177, + 227, + 230, + 103, + 13, + 72, + 141, + 248 + ], + "accounts": [ + { + "name": "sender", + "writable": true, + "signer": true + }, + { + "name": "proxy_metadata", + "docs": [ + "Proxy Contract" + ], + "writable": true + }, + { + "name": "new_price_oracle" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ChangeOracleParams" + } + } + } + ] + }, + { + "name": "create", + "discriminator": [ + 24, + 30, + 200, + 40, + 5, + 28, + 7, + 119 + ], + "accounts": [ + { + "name": "sender", + "writable": true, + "signer": true + }, + { + "name": "sender_tokens", + "docs": [ + "Associated token account address of `sender`." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "sender" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "recipient", + "writable": true + }, + { + "name": "recipient_tokens", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "recipient" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "proxy_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 116, + 114, + 97, + 99, + 116 + ] + }, + { + "kind": "account", + "path": "stream_metadata" + } + ] + } + }, + { + "name": "proxy_tokens", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "proxy_metadata" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "stream_metadata", + "writable": true, + "signer": true + }, + { + "name": "escrow_tokens", + "writable": true + }, + { + "name": "withdrawor", + "writable": true + }, + { + "name": "partner", + "writable": true + }, + { + "name": "partner_tokens", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "partner" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "mint" + }, + { + "name": "fee_oracle" + }, + { + "name": "price_oracle" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "streamflow_program", + "address": "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m" + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "ix", + "type": { + "defined": { + "name": "CreateParams" + } + } + } + ] + }, + { + "name": "create_test_oracle", + "discriminator": [ + 183, + 110, + 4, + 11, + 131, + 220, + 84, + 12 + ], + "accounts": [ + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "mint" + }, + { + "name": "test_oracle", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 101, + 115, + 116, + 45, + 111, + 114, + 97, + 99, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "mint" + }, + { + "kind": "account", + "path": "creator" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateTestOracleParams" + } + } + } + ] + }, + { + "name": "set_test_oracle_authority", + "discriminator": [ + 26, + 66, + 233, + 99, + 38, + 118, + 181, + 247 + ], + "accounts": [ + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "test_oracle", + "writable": true + }, + { + "name": "new_authority" + } + ], + "args": [] + }, + { + "name": "update_amount", + "discriminator": [ + 212, + 178, + 69, + 133, + 251, + 180, + 212, + 71 + ], + "accounts": [ + { + "name": "authority", + "docs": [ + "Wallet authorised to call this method" + ], + "writable": true, + "signer": true + }, + { + "name": "proxy_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 116, + 114, + 97, + 99, + 116 + ] + }, + { + "kind": "account", + "path": "stream_metadata" + } + ] + } + }, + { + "name": "stream_metadata", + "writable": true + }, + { + "name": "withdrawor", + "writable": true + }, + { + "name": "price_oracle", + "relations": [ + "proxy_metadata" + ] + }, + { + "name": "streamflow_program", + "address": "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "update_test_oracle", + "discriminator": [ + 158, + 147, + 215, + 74, + 34, + 123, + 80, + 76 + ], + "accounts": [ + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "test_oracle", + "writable": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateTestOracleParams" + } + } + } + ] + } + ], + "accounts": [ + { + "name": "Contract", + "discriminator": [ + 172, + 138, + 115, + 242, + 121, + 67, + 183, + 26 + ] + }, + { + "name": "TestOracle", + "discriminator": [ + 198, + 49, + 63, + 134, + 232, + 251, + 168, + 28 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "Unauthorized", + "msg": "Authority does not have permission for this action" + }, + { + "code": 6001, + "name": "ArithmeticError", + "msg": "Arithmetic error" + }, + { + "code": 6002, + "name": "UnsupportedTokenExtensions", + "msg": "Mint has unsupported Token Extensions" + }, + { + "code": 6003, + "name": "PeriodTooShort", + "msg": "Provided period is too short, should be equal or more than 30 seconds" + }, + { + "code": 6004, + "name": "InvalidTickSize", + "msg": "Provided percentage tick size is invalid" + }, + { + "code": 6005, + "name": "InvalidPercentageBoundaries", + "msg": "Provided percentage bounds are invalid" + }, + { + "code": 6006, + "name": "InvalidPriceBoundaries", + "msg": "Provided price bounds are invalid" + }, + { + "code": 6007, + "name": "UnsupportedOracle", + "msg": "Unsupported price oracle" + }, + { + "code": 6008, + "name": "InvalidOracleAccount", + "msg": "Invalid oracle account" + }, + { + "code": 6009, + "name": "InvalidOraclePrice", + "msg": "Invalid oracle price" + }, + { + "code": 6010, + "name": "InvalidStreamMetadata", + "msg": "Invalid Stream Metadata" + }, + { + "code": 6011, + "name": "AmountAlreadyUpdated", + "msg": "Release amount has already been updated in this period" + }, + { + "code": 6012, + "name": "AllFundsUnlocked", + "msg": "All funds are already unlocked" + } + ], + "types": [ + { + "name": "ChangeOracleParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracle_type", + "type": { + "defined": { + "name": "OracleType" + } + } + } + ] + } + }, + { + "name": "Contract", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "docs": [ + "Bump Seed used to sign transactions" + ], + "type": "u8" + }, + { + "name": "sender", + "docs": [ + "Original Contract sender" + ], + "type": "pubkey" + }, + { + "name": "sender_tokens", + "docs": [ + "Original Contract sender tokens address" + ], + "type": "pubkey" + }, + { + "name": "stream", + "docs": [ + "Vesting Stream address" + ], + "type": "pubkey" + }, + { + "name": "price_oracle_type", + "docs": [ + "Type of the Oracle used to derive Token Price" + ], + "type": { + "defined": { + "name": "OracleType" + } + } + }, + { + "name": "price_oracle", + "docs": [ + "Address of the Price Oracle" + ], + "type": "pubkey" + }, + { + "name": "min_price", + "docs": [ + "Min price boundary" + ], + "type": "u64" + }, + { + "name": "max_price", + "docs": [ + "Max price boundary" + ], + "type": "u64" + }, + { + "name": "min_percentage", + "docs": [ + "Min percentage boundary, can be 0 that equals 1 Raw Token" + ], + "type": "u64" + }, + { + "name": "max_percentage", + "docs": [ + "Max percentage boundary" + ], + "type": "u64" + }, + { + "name": "tick_size", + "docs": [ + "Ticket size for percentage boundaries" + ], + "type": "u64" + }, + { + "name": "start_time", + "docs": [ + "unlock_start from Stream contract for our worker to be able to fetch it in one call with the proxy contract" + ], + "type": "u64" + }, + { + "name": "end_time", + "docs": [ + "Copy end_time from Stream contract for our worker to be able to fetch it in one call with the proxy contract" + ], + "type": "u64" + }, + { + "name": "period", + "docs": [ + "Copy period from Stream contract for our worker to be able to fetch it in one call with the proxy contract" + ], + "type": "u64" + }, + { + "name": "last_amount_update_time", + "docs": [ + "Copy last_rate_change_time from Stream contract for our worker to be able to fetch it in one call with the proxy contract" + ], + "type": "u64" + }, + { + "name": "last_price", + "docs": [ + "Price used on last amount calculation" + ], + "type": "u64" + }, + { + "name": "stream_canceled_time", + "docs": [ + "Timestamp when stream was cancelled" + ], + "type": "u64" + }, + { + "name": "initial_amount_per_period", + "docs": [ + "Amount per period to use as base for calculations" + ], + "type": "u64" + }, + { + "name": "initial_price", + "docs": [ + "Initial token price at the time of Contract creation" + ], + "type": "u64" + }, + { + "name": "buffer", + "docs": [ + "Buffer for additional fields" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "CreateParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "start_time", + "docs": [ + "Timestamp when the tokens start vesting" + ], + "type": "u64" + }, + { + "name": "net_amount_deposited", + "docs": [ + "Deposited amount of tokens" + ], + "type": "u64" + }, + { + "name": "period", + "docs": [ + "Time step (period) in seconds per which the vesting/release occurs" + ], + "type": "u64" + }, + { + "name": "amount_per_period", + "docs": [ + "Base Amount released per period. Combined with `period`, we get a release rate." + ], + "type": "u64" + }, + { + "name": "cliff", + "docs": [ + "Vesting contract \"cliff\" timestamp" + ], + "type": "u64" + }, + { + "name": "cliff_amount", + "docs": [ + "Amount unlocked at the \"cliff\" timestamp" + ], + "type": "u64" + }, + { + "name": "cancelable_by_sender", + "docs": [ + "Whether a stream can be canceled by a sender" + ], + "type": "bool" + }, + { + "name": "cancelable_by_recipient", + "docs": [ + "Whether a stream can be canceled by a recipient" + ], + "type": "bool" + }, + { + "name": "transferable_by_sender", + "docs": [ + "Whether the sender can transfer the stream" + ], + "type": "bool" + }, + { + "name": "transferable_by_recipient", + "docs": [ + "Whether the recipient can transfer the stream" + ], + "type": "bool" + }, + { + "name": "can_topup", + "docs": [ + "Whether topup is enabled" + ], + "type": "bool" + }, + { + "name": "stream_name", + "docs": [ + "The name of this stream" + ], + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "min_price", + "type": "u64" + }, + { + "name": "max_price", + "type": "u64" + }, + { + "name": "min_percentage", + "type": "u64" + }, + { + "name": "max_percentage", + "type": "u64" + }, + { + "name": "tick_size", + "type": "u64" + }, + { + "name": "skip_initial", + "docs": [ + "Whether to skip initial calculation of amount per period" + ], + "type": "bool" + }, + { + "name": "oracle_type", + "docs": [ + "Type of Oracle to use to derive Token Price" + ], + "type": { + "defined": { + "name": "OracleType" + } + } + } + ] + } + }, + { + "name": "CreateTestOracleParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "price", + "type": "u64" + }, + { + "name": "expo", + "type": "i32" + }, + { + "name": "conf", + "type": "u64" + }, + { + "name": "publish_time", + "type": "i64" + } + ] + } + }, + { + "name": "OracleType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "Test" + }, + { + "name": "Pyth" + }, + { + "name": "Switchboard" + } + ] + } + }, + { + "name": "TestOracle", + "type": { + "kind": "struct", + "fields": [ + { + "name": "price", + "type": "u64" + }, + { + "name": "expo", + "type": "i32" + }, + { + "name": "conf", + "type": "u64" + }, + { + "name": "publish_time", + "type": "i64" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + } + ] + } + }, + { + "name": "UpdateTestOracleParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "price", + "type": "u64" + }, + { + "name": "expo", + "type": "i32" + }, + { + "name": "conf", + "type": "u64" + }, + { + "name": "publish_time", + "type": "i64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/packages/stream/solana/descriptor/streamflow_aligned_unlocks.ts b/packages/stream/solana/descriptor/streamflow_aligned_unlocks.ts new file mode 100644 index 00000000..a3346ffa --- /dev/null +++ b/packages/stream/solana/descriptor/streamflow_aligned_unlocks.ts @@ -0,0 +1,1272 @@ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/streamflow_aligned_unlocks.json`. + */ +export type StreamflowAlignedUnlocks = { + address: "aSTRM2NKoKxNnkmLWk9sz3k74gKBk9t7bpPrTGxMszH"; + metadata: { + name: "streamflowAlignedUnlocks"; + version: "1.0.0"; + spec: "0.1.0"; + description: "Proxy to update unlock amount within Streamflow vesting protocol according to Token performance and other metrics"; + }; + instructions: [ + { + name: "cancel"; + discriminator: [232, 219, 223, 41, 219, 236, 220, 190]; + accounts: [ + { + name: "sender"; + writable: true; + signer: true; + }, + { + name: "senderTokens"; + docs: ["Associated token account address of `sender`."]; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "sender"; + }, + { + kind: "account"; + path: "tokenProgram"; + }, + { + kind: "account"; + path: "mint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "recipient"; + writable: true; + }, + { + name: "recipientTokens"; + docs: ["CHECK; Associated token account address of `recipient`."]; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "recipient"; + }, + { + kind: "account"; + path: "tokenProgram"; + }, + { + kind: "account"; + path: "mint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "proxyMetadata"; + docs: ["Proxy Contract"]; + writable: true; + pda: { + seeds: [ + { + kind: "const"; + value: [99, 111, 110, 116, 114, 97, 99, 116]; + }, + { + kind: "account"; + path: "streamMetadata"; + }, + ]; + }; + }, + { + name: "proxyTokens"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "proxyMetadata"; + }, + { + kind: "account"; + path: "tokenProgram"; + }, + { + kind: "account"; + path: "mint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "streamMetadata"; + writable: true; + }, + { + name: "escrowTokens"; + writable: true; + }, + { + name: "streamflowTreasury"; + writable: true; + }, + { + name: "streamflowTreasuryTokens"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "streamflowTreasury"; + }, + { + kind: "account"; + path: "tokenProgram"; + }, + { + kind: "account"; + path: "mint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "partner"; + writable: true; + }, + { + name: "partnerTokens"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "partner"; + }, + { + kind: "account"; + path: "tokenProgram"; + }, + { + kind: "account"; + path: "mint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "mint"; + docs: ["The SPL token mint account."]; + writable: true; + }, + { + name: "streamflowProgram"; + address: "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m"; + }, + { + name: "tokenProgram"; + docs: ["The SPL program needed in case an associated account", "for the new recipient is being created."]; + }, + ]; + args: []; + }, + { + name: "changeOracle"; + discriminator: [177, 227, 230, 103, 13, 72, 141, 248]; + accounts: [ + { + name: "sender"; + writable: true; + signer: true; + }, + { + name: "proxyMetadata"; + docs: ["Proxy Contract"]; + writable: true; + }, + { + name: "newPriceOracle"; + }, + ]; + args: [ + { + name: "params"; + type: { + defined: { + name: "changeOracleParams"; + }; + }; + }, + ]; + }, + { + name: "create"; + discriminator: [24, 30, 200, 40, 5, 28, 7, 119]; + accounts: [ + { + name: "sender"; + writable: true; + signer: true; + }, + { + name: "senderTokens"; + docs: ["Associated token account address of `sender`."]; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "sender"; + }, + { + kind: "account"; + path: "tokenProgram"; + }, + { + kind: "account"; + path: "mint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "recipient"; + writable: true; + }, + { + name: "recipientTokens"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "recipient"; + }, + { + kind: "account"; + path: "tokenProgram"; + }, + { + kind: "account"; + path: "mint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "proxyMetadata"; + writable: true; + pda: { + seeds: [ + { + kind: "const"; + value: [99, 111, 110, 116, 114, 97, 99, 116]; + }, + { + kind: "account"; + path: "streamMetadata"; + }, + ]; + }; + }, + { + name: "proxyTokens"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "proxyMetadata"; + }, + { + kind: "account"; + path: "tokenProgram"; + }, + { + kind: "account"; + path: "mint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "streamMetadata"; + writable: true; + signer: true; + }, + { + name: "escrowTokens"; + writable: true; + }, + { + name: "withdrawor"; + writable: true; + }, + { + name: "partner"; + writable: true; + }, + { + name: "partnerTokens"; + writable: true; + pda: { + seeds: [ + { + kind: "account"; + path: "partner"; + }, + { + kind: "account"; + path: "tokenProgram"; + }, + { + kind: "account"; + path: "mint"; + }, + ]; + program: { + kind: "const"; + value: [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89, + ]; + }; + }; + }, + { + name: "mint"; + }, + { + name: "feeOracle"; + }, + { + name: "priceOracle"; + }, + { + name: "rent"; + address: "SysvarRent111111111111111111111111111111111"; + }, + { + name: "streamflowProgram"; + address: "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m"; + }, + { + name: "tokenProgram"; + }, + { + name: "associatedTokenProgram"; + address: "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; + }, + { + name: "systemProgram"; + address: "11111111111111111111111111111111"; + }, + ]; + args: [ + { + name: "ix"; + type: { + defined: { + name: "createParams"; + }; + }; + }, + ]; + }, + { + name: "createTestOracle"; + discriminator: [183, 110, 4, 11, 131, 220, 84, 12]; + accounts: [ + { + name: "creator"; + writable: true; + signer: true; + }, + { + name: "mint"; + }, + { + name: "testOracle"; + writable: true; + pda: { + seeds: [ + { + kind: "const"; + value: [116, 101, 115, 116, 45, 111, 114, 97, 99, 108, 101]; + }, + { + kind: "account"; + path: "mint"; + }, + { + kind: "account"; + path: "creator"; + }, + ]; + }; + }, + { + name: "systemProgram"; + address: "11111111111111111111111111111111"; + }, + ]; + args: [ + { + name: "params"; + type: { + defined: { + name: "createTestOracleParams"; + }; + }; + }, + ]; + }, + { + name: "setTestOracleAuthority"; + discriminator: [26, 66, 233, 99, 38, 118, 181, 247]; + accounts: [ + { + name: "authority"; + writable: true; + signer: true; + }, + { + name: "testOracle"; + writable: true; + }, + { + name: "newAuthority"; + }, + ]; + args: []; + }, + { + name: "updateAmount"; + discriminator: [212, 178, 69, 133, 251, 180, 212, 71]; + accounts: [ + { + name: "authority"; + docs: ["Wallet authorised to call this method"]; + writable: true; + signer: true; + }, + { + name: "proxyMetadata"; + writable: true; + pda: { + seeds: [ + { + kind: "const"; + value: [99, 111, 110, 116, 114, 97, 99, 116]; + }, + { + kind: "account"; + path: "streamMetadata"; + }, + ]; + }; + }, + { + name: "streamMetadata"; + writable: true; + }, + { + name: "withdrawor"; + writable: true; + }, + { + name: "priceOracle"; + relations: ["proxyMetadata"]; + }, + { + name: "streamflowProgram"; + address: "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m"; + }, + { + name: "systemProgram"; + address: "11111111111111111111111111111111"; + }, + ]; + args: []; + }, + { + name: "updateTestOracle"; + discriminator: [158, 147, 215, 74, 34, 123, 80, 76]; + accounts: [ + { + name: "authority"; + writable: true; + signer: true; + }, + { + name: "testOracle"; + writable: true; + }, + ]; + args: [ + { + name: "params"; + type: { + defined: { + name: "updateTestOracleParams"; + }; + }; + }, + ]; + }, + ]; + accounts: [ + { + name: "contract"; + discriminator: [172, 138, 115, 242, 121, 67, 183, 26]; + }, + { + name: "testOracle"; + discriminator: [198, 49, 63, 134, 232, 251, 168, 28]; + }, + ]; + errors: [ + { + code: 6000; + name: "unauthorized"; + msg: "Authority does not have permission for this action"; + }, + { + code: 6001; + name: "arithmeticError"; + msg: "Arithmetic error"; + }, + { + code: 6002; + name: "unsupportedTokenExtensions"; + msg: "Mint has unsupported Token Extensions"; + }, + { + code: 6003; + name: "periodTooShort"; + msg: "Provided period is too short, should be equal or more than 30 seconds"; + }, + { + code: 6004; + name: "invalidTickSize"; + msg: "Provided percentage tick size is invalid"; + }, + { + code: 6005; + name: "invalidPercentageBoundaries"; + msg: "Provided percentage bounds are invalid"; + }, + { + code: 6006; + name: "invalidPriceBoundaries"; + msg: "Provided price bounds are invalid"; + }, + { + code: 6007; + name: "unsupportedOracle"; + msg: "Unsupported price oracle"; + }, + { + code: 6008; + name: "invalidOracleAccount"; + msg: "Invalid oracle account"; + }, + { + code: 6009; + name: "invalidOraclePrice"; + msg: "Invalid oracle price"; + }, + { + code: 6010; + name: "invalidStreamMetadata"; + msg: "Invalid Stream Metadata"; + }, + { + code: 6011; + name: "amountAlreadyUpdated"; + msg: "Release amount has already been updated in this period"; + }, + { + code: 6012; + name: "allFundsUnlocked"; + msg: "All funds are already unlocked"; + }, + ]; + types: [ + { + name: "changeOracleParams"; + type: { + kind: "struct"; + fields: [ + { + name: "oracleType"; + type: { + defined: { + name: "oracleType"; + }; + }; + }, + ]; + }; + }, + { + name: "contract"; + type: { + kind: "struct"; + fields: [ + { + name: "bump"; + docs: ["Bump Seed used to sign transactions"]; + type: "u8"; + }, + { + name: "sender"; + docs: ["Original Contract sender"]; + type: "pubkey"; + }, + { + name: "senderTokens"; + docs: ["Original Contract sender tokens address"]; + type: "pubkey"; + }, + { + name: "stream"; + docs: ["Vesting Stream address"]; + type: "pubkey"; + }, + { + name: "priceOracleType"; + docs: ["Type of the Oracle used to derive Token Price"]; + type: { + defined: { + name: "oracleType"; + }; + }; + }, + { + name: "priceOracle"; + docs: ["Address of the Price Oracle"]; + type: "pubkey"; + }, + { + name: "minPrice"; + docs: ["Min price boundary"]; + type: "u64"; + }, + { + name: "maxPrice"; + docs: ["Max price boundary"]; + type: "u64"; + }, + { + name: "minPercentage"; + docs: ["Min percentage boundary, can be 0 that equals 1 Raw Token"]; + type: "u64"; + }, + { + name: "maxPercentage"; + docs: ["Max percentage boundary"]; + type: "u64"; + }, + { + name: "tickSize"; + docs: ["Ticket size for percentage boundaries"]; + type: "u64"; + }, + { + name: "startTime"; + docs: [ + "unlock_start from Stream contract for our worker to be able to fetch it in one call with the proxy contract", + ]; + type: "u64"; + }, + { + name: "endTime"; + docs: [ + "Copy end_time from Stream contract for our worker to be able to fetch it in one call with the proxy contract", + ]; + type: "u64"; + }, + { + name: "period"; + docs: [ + "Copy period from Stream contract for our worker to be able to fetch it in one call with the proxy contract", + ]; + type: "u64"; + }, + { + name: "lastAmountUpdateTime"; + docs: [ + "Copy last_rate_change_time from Stream contract for our worker to be able to fetch it in one call with the proxy contract", + ]; + type: "u64"; + }, + { + name: "lastPrice"; + docs: ["Price used on last amount calculation"]; + type: "u64"; + }, + { + name: "streamCanceledTime"; + docs: ["Timestamp when stream was cancelled"]; + type: "u64"; + }, + { + name: "initialAmountPerPeriod"; + docs: ["Amount per period to use as base for calculations"]; + type: "u64"; + }, + { + name: "initialPrice"; + docs: ["Initial token price at the time of Contract creation"]; + type: "u64"; + }, + { + name: "buffer"; + docs: ["Buffer for additional fields"]; + type: { + array: ["u8", 32]; + }; + }, + ]; + }; + }, + { + name: "createParams"; + type: { + kind: "struct"; + fields: [ + { + name: "startTime"; + docs: ["Timestamp when the tokens start vesting"]; + type: "u64"; + }, + { + name: "netAmountDeposited"; + docs: ["Deposited amount of tokens"]; + type: "u64"; + }, + { + name: "period"; + docs: ["Time step (period) in seconds per which the vesting/release occurs"]; + type: "u64"; + }, + { + name: "amountPerPeriod"; + docs: ["Base Amount released per period. Combined with `period`, we get a release rate."]; + type: "u64"; + }, + { + name: "cliff"; + docs: ['Vesting contract "cliff" timestamp']; + type: "u64"; + }, + { + name: "cliffAmount"; + docs: ['Amount unlocked at the "cliff" timestamp']; + type: "u64"; + }, + { + name: "cancelableBySender"; + docs: ["Whether a stream can be canceled by a sender"]; + type: "bool"; + }, + { + name: "cancelableByRecipient"; + docs: ["Whether a stream can be canceled by a recipient"]; + type: "bool"; + }, + { + name: "transferableBySender"; + docs: ["Whether the sender can transfer the stream"]; + type: "bool"; + }, + { + name: "transferableByRecipient"; + docs: ["Whether the recipient can transfer the stream"]; + type: "bool"; + }, + { + name: "canTopup"; + docs: ["Whether topup is enabled"]; + type: "bool"; + }, + { + name: "streamName"; + docs: ["The name of this stream"]; + type: { + array: ["u8", 64]; + }; + }, + { + name: "minPrice"; + type: "u64"; + }, + { + name: "maxPrice"; + type: "u64"; + }, + { + name: "minPercentage"; + type: "u64"; + }, + { + name: "maxPercentage"; + type: "u64"; + }, + { + name: "tickSize"; + type: "u64"; + }, + { + name: "skipInitial"; + docs: ["Whether to skip initial calculation of amount per period"]; + type: "bool"; + }, + { + name: "oracleType"; + docs: ["Type of Oracle to use to derive Token Price"]; + type: { + defined: { + name: "oracleType"; + }; + }; + }, + ]; + }; + }, + { + name: "createTestOracleParams"; + type: { + kind: "struct"; + fields: [ + { + name: "price"; + type: "u64"; + }, + { + name: "expo"; + type: "i32"; + }, + { + name: "conf"; + type: "u64"; + }, + { + name: "publishTime"; + type: "i64"; + }, + ]; + }; + }, + { + name: "oracleType"; + type: { + kind: "enum"; + variants: [ + { + name: "none"; + }, + { + name: "test"; + }, + { + name: "pyth"; + }, + { + name: "switchboard"; + }, + ]; + }; + }, + { + name: "testOracle"; + type: { + kind: "struct"; + fields: [ + { + name: "price"; + type: "u64"; + }, + { + name: "expo"; + type: "i32"; + }, + { + name: "conf"; + type: "u64"; + }, + { + name: "publishTime"; + type: "i64"; + }, + { + name: "creator"; + type: "pubkey"; + }, + { + name: "authority"; + type: "pubkey"; + }, + ]; + }; + }, + { + name: "updateTestOracleParams"; + type: { + kind: "struct"; + fields: [ + { + name: "price"; + type: "u64"; + }, + { + name: "expo"; + type: "i32"; + }, + { + name: "conf"; + type: "u64"; + }, + { + name: "publishTime"; + type: "i64"; + }, + ]; + }; + }, + ]; +}; diff --git a/packages/stream/solana/index.ts b/packages/stream/solana/index.ts index ae5ab5cc..9ecf38b4 100644 --- a/packages/stream/solana/index.ts +++ b/packages/stream/solana/index.ts @@ -1,5 +1,6 @@ export * from "./StreamClient.js"; -export * from "./utils.js"; +export * from "./lib/utils.js"; +export * from "./lib/derive-accounts.js"; export * from "./types.js"; export * from "./instructions.js"; export * as constants from "./constants.js"; diff --git a/packages/stream/solana/lib/derive-accounts.ts b/packages/stream/solana/lib/derive-accounts.ts new file mode 100644 index 00000000..00ed2c12 --- /dev/null +++ b/packages/stream/solana/lib/derive-accounts.ts @@ -0,0 +1,15 @@ +import { PublicKey } from "@solana/web3.js"; + +import { CONTRACT_SEED, ESCROW_SEED, TEST_ORACLE_SEED } from "../constants"; + +export const deriveContractPDA = (programId: PublicKey, streamMetadata: PublicKey): PublicKey => { + return PublicKey.findProgramAddressSync([CONTRACT_SEED, streamMetadata.toBuffer()], programId)[0]; +}; + +export const deriveEscrowPDA = (programId: PublicKey, streamMetadata: PublicKey): PublicKey => { + return PublicKey.findProgramAddressSync([ESCROW_SEED, streamMetadata.toBuffer()], programId)[0]; +}; + +export const deriveTestOraclePDA = (programId: PublicKey, mint: PublicKey, creator: PublicKey): PublicKey => { + return PublicKey.findProgramAddressSync([TEST_ORACLE_SEED, mint.toBuffer(), creator.toBuffer()], programId)[0]; +}; diff --git a/packages/stream/solana/utils.ts b/packages/stream/solana/lib/utils.ts similarity index 96% rename from packages/stream/solana/utils.ts rename to packages/stream/solana/lib/utils.ts index e9e7ff8b..6ebc2817 100644 --- a/packages/stream/solana/utils.ts +++ b/packages/stream/solana/lib/utils.ts @@ -9,9 +9,9 @@ import { } from "@streamflow/common/solana"; import BN from "bn.js"; -import { streamLayout } from "./layout.js"; -import { DecodedStream, BatchItem, BatchItemResult } from "./types.js"; -import { SOLANA_ERROR_MAP, SOLANA_ERROR_MATCH_REGEX } from "./constants.js"; +import { streamLayout } from "../layout.js"; +import { DecodedStream, BatchItem, BatchItemResult } from "../types.js"; +import { SOLANA_ERROR_MAP, SOLANA_ERROR_MATCH_REGEX } from "../constants.js"; const decoder = new TextDecoder("utf-8"); const LE = "le"; //little endian diff --git a/packages/stream/solana/types.ts b/packages/stream/solana/types.ts index c6ffb6e3..44483d54 100644 --- a/packages/stream/solana/types.ts +++ b/packages/stream/solana/types.ts @@ -1,11 +1,27 @@ import { SignerWalletAdapter } from "@solana/wallet-adapter-base"; -import { AccountInfo, PublicKey, Keypair, VersionedTransaction } from "@solana/web3.js"; +import { AccountInfo, PublicKey, Keypair, VersionedTransaction, TransactionInstruction } from "@solana/web3.js"; import { ITransactionSolanaExt } from "@streamflow/common/solana"; import BN from "bn.js"; +import { type IdlTypes } from "@coral-xyz/anchor"; -import { buildStreamType, calculateUnlockedAmount } from "../common/contractUtils.js"; -import { IRecipient, Stream, StreamType } from "../common/types.js"; +import { buildStreamType, calculateUnlockedAmount, decodeEndTime } from "../common/contractUtils.js"; +import { AlignedStream, IRecipient, LinearStream, OracleTypeName, StreamType } from "../common/types.js"; import { getNumberFromBN } from "../common/utils.js"; +import { StreamflowAlignedUnlocks as AlignedUnlocksIDL } from "./descriptor/streamflow_aligned_unlocks.js"; +import { ALIGNED_PRECISION_FACTOR_POW } from "./constants.js"; + +export { IChain, ICluster, ContractError } from "@streamflow/common"; + +type AlignedUnlocksTypes = IdlTypes; + +export type AlignedUnlocksContract = AlignedUnlocksTypes["contract"]; +export type OracleType = AlignedUnlocksTypes["oracleType"]; +export type TestOracle = AlignedUnlocksTypes["testOracle"]; + +export type CreateParams = AlignedUnlocksTypes["createParams"]; +export type ChangeOracleParams = AlignedUnlocksTypes["changeOracleParams"]; +export type CreateTestOracleParams = AlignedUnlocksTypes["createTestOracleParams"]; +export type UpdateTestOracleParams = AlignedUnlocksTypes["updateTestOracleParams"]; export interface ISearchStreams { mint?: string; @@ -39,7 +55,13 @@ export interface ITopUpStreamSolanaExt extends ITransactionSolanaExt { isNative?: boolean; } -export class Contract implements Stream { +export interface ICreateStreamInstructions { + ixs: TransactionInstruction[]; + metadata: Keypair | undefined; + metadataPubKey: PublicKey; +} + +export class Contract implements LinearStream { magic: number; version: number; @@ -126,13 +148,16 @@ export class Contract implements Stream { type: StreamType; + isAligned?: boolean; + constructor(stream: DecodedStream) { this.magic = stream.magic.toNumber(); this.version = stream.version.toNumber(); this.createdAt = stream.createdAt.toNumber(); this.withdrawnAmount = stream.withdrawnAmount; this.canceledAt = stream.canceledAt.toNumber(); - this.end = stream.end.toNumber(); + // for aligned contracts end time can be an invalid timeValue + this.end = decodeEndTime(stream.end); this.lastWithdrawnAt = stream.lastWithdrawnAt.toNumber(); this.sender = stream.sender.toBase58(); this.senderTokens = stream.senderTokens.toBase58(); @@ -170,6 +195,7 @@ export class Contract implements Stream { this.lastRateChangeTime = stream.lastRateChangeTime.toNumber(); this.fundsUnlockedAtLastRateChange = stream.fundsUnlockedAtLastRateChange; this.type = buildStreamType(this); + this.isAligned = false; } unlocked(currentTimestamp: number): BN { @@ -184,6 +210,38 @@ export class Contract implements Stream { } } +export class AlignedContract extends Contract implements AlignedStream { + minPrice: number; + + maxPrice: number; + + minPercentage: number; + + maxPercentage: number; + + tickSize: number; + + proxyAddress: string; + + priceOracle: string | undefined; + + oracleType: OracleTypeName; + + constructor(stream: DecodedStream, alignedProxy: AlignedUnlocksContract) { + super(stream); + this.minPrice = getNumberFromBN(alignedProxy.minPrice, ALIGNED_PRECISION_FACTOR_POW); + this.maxPrice = getNumberFromBN(alignedProxy.maxPrice, ALIGNED_PRECISION_FACTOR_POW); + this.minPercentage = getNumberFromBN(alignedProxy.minPercentage, ALIGNED_PRECISION_FACTOR_POW); + this.maxPercentage = getNumberFromBN(alignedProxy.maxPercentage, ALIGNED_PRECISION_FACTOR_POW); + this.oracleType = (Object.keys(alignedProxy.priceOracleType).find((key) => !!key) || "none") as OracleTypeName; + this.tickSize = alignedProxy.tickSize.toNumber(); + this.priceOracle = this.oracleType === "none" ? undefined : alignedProxy.priceOracle.toBase58(); + this.sender = alignedProxy.sender.toBase58(); + this.canceledAt = alignedProxy.streamCanceledTime.toNumber(); + this.proxyAddress = stream.sender.toBase58(); + } +} + export interface DecodedStream { magic: BN; version: BN; diff --git a/packages/stream/sui/types.ts b/packages/stream/sui/types.ts index cacdfa03..14a0004c 100644 --- a/packages/stream/sui/types.ts +++ b/packages/stream/sui/types.ts @@ -5,7 +5,7 @@ import { ExecuteTransactionRequestType, SuiTransactionBlockResponseOptions } fro import BN from "bn.js"; import { buildStreamType, calculateUnlockedAmount } from "../common/contractUtils.js"; -import { Stream, StreamType } from "../common/types.js"; +import { LinearStream, StreamType } from "../common/types.js"; import { getNumberFromBN } from "../common/utils.js"; export interface ICreateStreamSuiExt { @@ -113,7 +113,7 @@ export interface FeeValueResource { }; } -export class Contract implements Stream { +export class Contract implements LinearStream { magic: number; version: number; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8780d3b..ee2d465c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,7 +193,10 @@ importers: packages/stream: dependencies: - "@coral-xyz/borsh": + '@coral-xyz/anchor': + specifier: ^0.30.0 + version: 0.30.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@coral-xyz/borsh': specifier: 0.30.1 version: 0.30.1(@solana/web3.js@1.90.2(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)) "@manahippo/aptos-wallet-adapter":