From 8f74eb955ac19aa0cddfd4bb93ce00a5136b92a5 Mon Sep 17 00:00:00 2001 From: Yolley Date: Tue, 23 Jan 2024 11:58:22 +0100 Subject: [PATCH] STREAM-1091: expose on-chain call utils, fix exports (#128) * STREAM-1091: expose on-chain call utils, fix exports --- .gitignore | 1 + lerna.json | 2 +- packages/stream/aptos/index.ts | 4 + packages/stream/evm/index.ts | 4 + packages/stream/package.json | 4 +- packages/stream/solana/StreamClient.ts | 57 ++--------- packages/stream/solana/types.ts | 5 + packages/stream/solana/utils.ts | 125 ++++++++++++++++++++++++- packages/stream/sui/index.ts | 4 + 9 files changed, 151 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index a7a2b4cc..edae40fd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ dist/ /local/ .DS_Store packages/.DS_Store +*.tgz diff --git a/lerna.json b/lerna.json index 09ea9283..0725dc0c 100644 --- a/lerna.json +++ b/lerna.json @@ -2,6 +2,6 @@ "packages": [ "packages/*" ], - "version": "5.9.10", + "version": "5.10.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } \ No newline at end of file diff --git a/packages/stream/aptos/index.ts b/packages/stream/aptos/index.ts index a5424d4f..a06781d9 100644 --- a/packages/stream/aptos/index.ts +++ b/packages/stream/aptos/index.ts @@ -1,3 +1,7 @@ export { default, default as AptosStreamClient } from "./StreamClient"; +export * from "./utils"; + export * from "./types"; + +export * as constants from "./constants"; diff --git a/packages/stream/evm/index.ts b/packages/stream/evm/index.ts index 7f28d937..6f81b20d 100644 --- a/packages/stream/evm/index.ts +++ b/packages/stream/evm/index.ts @@ -1,3 +1,7 @@ export { default, default as EvmStreamClient } from "./StreamClient"; +export * from "./utils"; + export * from "./types"; + +export * as constants from "./constants"; diff --git a/packages/stream/package.json b/packages/stream/package.json index 42321ed8..5e080bc6 100644 --- a/packages/stream/package.json +++ b/packages/stream/package.json @@ -1,9 +1,9 @@ { "name": "@streamflow/stream", - "version": "5.9.10", + "version": "5.10.0", "description": "JavaScript SDK to interact with Streamflow protocol.", - "main": "dist/index.js", "homepage": "https://github.com/streamflow-finance/js-sdk/", + "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "rm -rf dist; tsc -p tsconfig.json", diff --git a/packages/stream/solana/StreamClient.ts b/packages/stream/solana/StreamClient.ts index e421de97..3ad8a0fd 100644 --- a/packages/stream/solana/StreamClient.ts +++ b/packages/stream/solana/StreamClient.ts @@ -2,7 +2,6 @@ import BN from "bn.js"; import { Buffer } from "buffer"; -import bs58 from "bs58"; import { ASSOCIATED_TOKEN_PROGRAM_ID, NATIVE_MINT, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { Connection, @@ -15,8 +14,6 @@ import { Transaction, Commitment, ConnectionConfig, - sendAndConfirmRawTransaction, - BlockheightBasedTransactionConfirmationStrategy, } from "@solana/web3.js"; import { createAssociatedTokenAccountInstruction } from "@solana/spl-token"; import * as borsh from "borsh"; @@ -38,9 +35,9 @@ import { decodeStream, extractSolanaErrorCode, getProgramAccounts, - isSignerWallet, sendAndConfirmStreamRawTransaction, signAllTransactionWithRecipients, + signAndExecuteTransaction, } from "./utils"; import { PROGRAM_ID, @@ -232,7 +229,7 @@ export default class SolanaStreamClient extends BaseStreamClient { tx.partialSign(metadata); } - const signature = await this.sign(sender, tx, hash); + const signature = await signAndExecuteTransaction(this.connection, sender, tx, hash); return { ixs, txId: signature, metadataId: metadataPubKey.toBase58() }; } @@ -347,7 +344,7 @@ export default class SolanaStreamClient extends BaseStreamClient { tx.partialSign(metadata); } - const signature = await this.sign(sender, tx, hash); + const signature = await signAndExecuteTransaction(this.connection, sender, tx, hash); return { ixs, txId: signature, metadataId: metadataPubKey.toBase58() }; } @@ -506,7 +503,7 @@ export default class SolanaStreamClient extends BaseStreamClient { lastValidBlockHeight: hash.lastValidBlockHeight, }).add(...ixs); - const signature = await this.sign(invoker, tx, hash); + const signature = await signAndExecuteTransaction(this.connection, invoker, tx, hash); return { ixs, txId: signature }; } @@ -564,7 +561,7 @@ export default class SolanaStreamClient extends BaseStreamClient { lastValidBlockHeight: hash.lastValidBlockHeight, }).add(...ixs); - const signature = await this.sign(invoker, tx, hash); + const signature = await signAndExecuteTransaction(this.connection, invoker, tx, hash); return { ixs, txId: signature }; } @@ -617,7 +614,7 @@ export default class SolanaStreamClient extends BaseStreamClient { lastValidBlockHeight: hash.lastValidBlockHeight, }).add(...ixs); - const signature = await this.sign(invoker, tx, hash); + const signature = await signAndExecuteTransaction(this.connection, invoker, tx, hash); return { ixs, txId: signature }; } @@ -674,7 +671,7 @@ export default class SolanaStreamClient extends BaseStreamClient { lastValidBlockHeight: hash.lastValidBlockHeight, }).add(...nativeInstructions, ...ixs); - const signature = await this.sign(invoker, tx, hash); + const signature = await signAndExecuteTransaction(this.connection, invoker, tx, hash); return { ixs, txId: signature }; } @@ -739,40 +736,6 @@ export default class SolanaStreamClient extends BaseStreamClient { return sortedStreams.filter((stream) => stream[1].type === type); } - private async sign( - invoker: any, - tx: Transaction, - hash: Readonly<{ - blockhash: string; - lastValidBlockHeight: number; - }> - ) { - let signedTx: Transaction; - if (isSignerWallet(invoker)) { - signedTx = await invoker.signTransaction(tx); - } else { - tx.partialSign(invoker); - signedTx = tx; - } - - const rawTx = signedTx.serialize(); - - if (!hash.lastValidBlockHeight || !signedTx.signature || !hash.blockhash) - throw Error("Error with transaction parameters."); - - const confirmationStrategy: BlockheightBasedTransactionConfirmationStrategy = { - lastValidBlockHeight: hash.lastValidBlockHeight, - signature: bs58.encode(signedTx.signature), - blockhash: hash.blockhash, - }; - const signature = await sendAndConfirmRawTransaction( - this.connection, - rawTx, - confirmationStrategy - ); - return signature; - } - /** * Attempts updating the stream auto withdrawal params and amount per period */ @@ -807,7 +770,7 @@ export default class SolanaStreamClient extends BaseStreamClient { lastValidBlockHeight: hash.lastValidBlockHeight, }).add(updateIx); - const signature = await this.sign(invoker, tx, hash); + const signature = await signAndExecuteTransaction(this.connection, invoker, tx, hash); return { ixs: [updateIx], @@ -981,9 +944,7 @@ export default class SolanaStreamClient extends BaseStreamClient { invoker.publicKey!, accountArrays[i][1], accountArrays[i][0], - data.mint, - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID + data.mint ) ); } diff --git a/packages/stream/solana/types.ts b/packages/stream/solana/types.ts index 894b06bc..ac2e5b08 100644 --- a/packages/stream/solana/types.ts +++ b/packages/stream/solana/types.ts @@ -452,4 +452,9 @@ export interface BatchItemError extends BatchItem { error: string; } +export interface AtaParams { + mint: PublicKey; + owner: PublicKey; +} + export type BatchItemResult = BatchItemSuccess | BatchItemError; diff --git a/packages/stream/solana/utils.ts b/packages/stream/solana/utils.ts index 98e65060..18792ad9 100644 --- a/packages/stream/solana/utils.ts +++ b/packages/stream/solana/utils.ts @@ -1,17 +1,23 @@ -import { getAssociatedTokenAddress } from "@solana/spl-token"; +import { + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddress, +} from "@solana/spl-token"; import { SignerWalletAdapter } from "@solana/wallet-adapter-base"; import { BlockheightBasedTransactionConfirmationStrategy, Connection, Keypair, PublicKey, + TransactionInstruction, + Transaction, sendAndConfirmRawTransaction, + BlockhashWithExpiryBlockHeight, } from "@solana/web3.js"; import BN from "bn.js"; import bs58 from "bs58"; import { streamLayout } from "./layout"; -import { DecodedStream, Account, BatchItem, BatchItemResult } from "./types"; +import { AtaParams, DecodedStream, Account, BatchItem, BatchItemResult } from "./types"; import { SOLANA_ERROR_MAP, SOLANA_ERROR_MATCH_REGEX } from "./constants"; const decoder = new TextDecoder("utf-8"); @@ -97,7 +103,9 @@ export async function getProgramAccounts( * @param {Keypair | SignerWalletAdapter} walletOrKeypair - Wallet or Keypair in question * @return {boolean} - Returns true if parameter is a Wallet. */ -export function isSignerWallet(walletOrKeypair: Keypair | SignerWalletAdapter): boolean { +export function isSignerWallet( + walletOrKeypair: Keypair | SignerWalletAdapter +): walletOrKeypair is SignerWalletAdapter { return (walletOrKeypair).signTransaction !== undefined; } @@ -106,7 +114,7 @@ export function isSignerWallet(walletOrKeypair: Keypair | SignerWalletAdapter): * @param walletOrKeypair {Keypair | SignerWalletAdapter} walletOrKeypair - Wallet or Keypair in question * @returns {boolean} - Returns true if parameter is a Keypair. */ -function isSignerKeypair( +export function isSignerKeypair( walletOrKeypair: Keypair | SignerWalletAdapter ): walletOrKeypair is Keypair { return ( @@ -116,6 +124,20 @@ function isSignerKeypair( ); } +export async function signTransaction( + invoker: Keypair | SignerWalletAdapter, + tx: Transaction +): Promise { + let signedTx: Transaction; + if (isSignerWallet(invoker)) { + signedTx = await invoker.signTransaction(tx); + } else { + tx.partialSign(invoker); + signedTx = tx; + } + return signedTx; +} + /** * Sign passed BatchItems with wallet request or KeyPair * @param {Keypair | SignerWalletAdapter} sender - Wallet or Keypair of sendin account @@ -146,6 +168,35 @@ export async function signAllTransactionWithRecipients( } } +/** + * Signs, sends and confirms Transaction + * @param connection - Solana client connection + * @param invoker - Keypair used as signer + * @param tx - Transaction instance + * @param hash - blockhash information, the same hash should be used in the Transaction + * @returns Transaction signature + */ +export async function signAndExecuteTransaction( + connection: Connection, + invoker: Keypair | SignerWalletAdapter, + tx: Transaction, + hash: BlockhashWithExpiryBlockHeight +): Promise { + const signedTx = await signTransaction(invoker, tx); + const rawTx = signedTx.serialize(); + + if (!hash.lastValidBlockHeight || !signedTx.signature || !hash.blockhash) + throw Error("Error with transaction parameters."); + + const confirmationStrategy: BlockheightBasedTransactionConfirmationStrategy = { + lastValidBlockHeight: hash.lastValidBlockHeight, + signature: bs58.encode(signedTx.signature), + blockhash: hash.blockhash, + }; + const signature = await sendAndConfirmRawTransaction(connection, rawTx, confirmationStrategy); + return signature; +} + /** * Sign passed BatchItems with wallet request or KeyPair * @param {Connection} connection - Solana web3 connection object. @@ -191,6 +242,72 @@ export function ata(mint: PublicKey, owner: PublicKey): Promise { return getAssociatedTokenAddress(mint, owner, true); } +/** + * Function that checks whether ATA exists for each provided owner + * @param connection - Solana client connection + * @param paramsBatch - Array of Params for an each ATA account: {mint, owner} + * @returns Array of boolean where each members corresponds to owners member + */ +export async function ataBatchExist( + connection: Connection, + paramsBatch: AtaParams[] +): Promise { + const tokenAccounts = await Promise.all( + paramsBatch.map(async ({ mint, owner }) => { + const pubkey = await ata(mint, owner); + return pubkey; + }) + ); + const response = await connection.getMultipleAccountsInfo(tokenAccounts); + return response.map((accInfo) => !!accInfo); +} + +/** + * Generates a Transaction to create ATA for an array of owners + * @param connection - Solana client connection + * @param payer - Transaction invoker, should be a signer + * @param coparamsBatchnfigs - Array of Params for an each ATA account: {mint, owner} + * @returns Unsigned Transaction with create ATA instructions + */ +export async function generateCreateAtaBatchTx( + connection: Connection, + payer: PublicKey, + paramsBatch: AtaParams[] +): Promise<{ + tx: Transaction; + hash: BlockhashWithExpiryBlockHeight; +}> { + const ixs: TransactionInstruction[] = await Promise.all( + paramsBatch.map(async ({ mint, owner }) => { + return createAssociatedTokenAccountInstruction(payer, await ata(mint, owner), owner, mint); + }) + ); + const hash = await connection.getLatestBlockhash(); + const tx = new Transaction({ + feePayer: payer, + blockhash: hash.blockhash, + lastValidBlockHeight: hash.lastValidBlockHeight, + }).add(...ixs); + return { tx, hash }; +} + +/** + * Creates ATA for an array of owners + * @param connection - Solana client connection + * @param invoker - Transaction invoker and payer + * @param paramsBatch - Array of Params for an each ATA account: {mint, owner} + * @returns Transaction signature + */ +export async function createAtaBatch( + connection: Connection, + invoker: Keypair | SignerWalletAdapter, + paramsBatch: AtaParams[] +): Promise { + const { tx, hash } = await generateCreateAtaBatchTx(connection, invoker.publicKey!, paramsBatch); + const signature = await signAndExecuteTransaction(connection, invoker, tx, hash); + return signature; +} + export function extractSolanaErrorCode(errorText: string): string | null { const match = SOLANA_ERROR_MATCH_REGEX.exec(errorText); diff --git a/packages/stream/sui/index.ts b/packages/stream/sui/index.ts index 2976e882..7404ba11 100644 --- a/packages/stream/sui/index.ts +++ b/packages/stream/sui/index.ts @@ -1,3 +1,7 @@ export { default as SuiStreamClient } from "./StreamClient"; +export * from "./utils"; + export * from "./types"; + +export * as constants from "./constants";