From a52cf43defbe88989fbdc5a5385b7ade9dab5654 Mon Sep 17 00:00:00 2001 From: Wilhelm Thieme Date: Tue, 19 Nov 2024 16:34:34 -0500 Subject: [PATCH 1/2] Remove common-sdk from this repo --- packages/common-sdk/.prettierrc | 9 - packages/common-sdk/LICENSE | 11 - packages/common-sdk/README.md | 13 - packages/common-sdk/jest.config.js | 15 - packages/common-sdk/package.json | 38 -- packages/common-sdk/src/index.ts | 2 - packages/common-sdk/src/math/decimal-util.ts | 26 - packages/common-sdk/src/math/index.ts | 3 - packages/common-sdk/src/math/math-util.ts | 79 --- packages/common-sdk/src/math/percentage.ts | 51 -- packages/common-sdk/src/tsconfig.json | 11 - packages/common-sdk/src/web3/address-util.ts | 37 -- packages/common-sdk/src/web3/ata-util.ts | 170 ----- packages/common-sdk/src/web3/index.ts | 8 - .../src/web3/lookup-table-fetcher.ts | 28 - .../src/web3/network/account-requests.ts | 103 --- .../src/web3/network/fetcher/index.ts | 62 -- .../network/fetcher/simple-fetcher-impl.ts | 163 ----- packages/common-sdk/src/web3/network/index.ts | 4 - .../common-sdk/src/web3/network/parsing.ts | 76 --- packages/common-sdk/src/web3/network/types.ts | 12 - .../common-sdk/src/web3/public-key-utils.ts | 41 -- packages/common-sdk/src/web3/token-util.ts | 309 --------- .../src/web3/transactions/compute-budget.ts | 71 -- .../src/web3/transactions/constants.ts | 13 - .../common-sdk/src/web3/transactions/index.ts | 5 - .../src/web3/transactions/jito-tip.ts | 19 - .../web3/transactions/transactions-builder.ts | 558 ---------------- .../transactions/transactions-processor.ts | 167 ----- .../common-sdk/src/web3/transactions/types.ts | 43 -- packages/common-sdk/src/web3/wallet.ts | 20 - packages/common-sdk/tests/test-context.ts | 55 -- packages/common-sdk/tests/tsconfig.json | 8 - .../common-sdk/tests/utils/expectations.ts | 9 - .../common-sdk/tests/utils/test-wallet.ts | 32 - .../common-sdk/tests/web3/ata-util.test.ts | 455 ------------- .../web3/network/account-requests.test.ts | 76 --- .../tests/web3/network/parsing.test.ts | 52 -- .../web3/network/simple-fetcher-impl.test.ts | 627 ------------------ .../tests/web3/transactions/constants.test.ts | 15 - .../transactions/transactions-builder.test.ts | 112 ---- packages/common-sdk/tsconfig-base.json | 25 - 42 files changed, 3633 deletions(-) delete mode 100644 packages/common-sdk/.prettierrc delete mode 100644 packages/common-sdk/LICENSE delete mode 100644 packages/common-sdk/README.md delete mode 100644 packages/common-sdk/jest.config.js delete mode 100644 packages/common-sdk/package.json delete mode 100644 packages/common-sdk/src/index.ts delete mode 100644 packages/common-sdk/src/math/decimal-util.ts delete mode 100644 packages/common-sdk/src/math/index.ts delete mode 100644 packages/common-sdk/src/math/math-util.ts delete mode 100644 packages/common-sdk/src/math/percentage.ts delete mode 100644 packages/common-sdk/src/tsconfig.json delete mode 100644 packages/common-sdk/src/web3/address-util.ts delete mode 100644 packages/common-sdk/src/web3/ata-util.ts delete mode 100644 packages/common-sdk/src/web3/index.ts delete mode 100644 packages/common-sdk/src/web3/lookup-table-fetcher.ts delete mode 100644 packages/common-sdk/src/web3/network/account-requests.ts delete mode 100644 packages/common-sdk/src/web3/network/fetcher/index.ts delete mode 100644 packages/common-sdk/src/web3/network/fetcher/simple-fetcher-impl.ts delete mode 100644 packages/common-sdk/src/web3/network/index.ts delete mode 100644 packages/common-sdk/src/web3/network/parsing.ts delete mode 100644 packages/common-sdk/src/web3/network/types.ts delete mode 100644 packages/common-sdk/src/web3/public-key-utils.ts delete mode 100644 packages/common-sdk/src/web3/token-util.ts delete mode 100644 packages/common-sdk/src/web3/transactions/compute-budget.ts delete mode 100644 packages/common-sdk/src/web3/transactions/constants.ts delete mode 100644 packages/common-sdk/src/web3/transactions/index.ts delete mode 100644 packages/common-sdk/src/web3/transactions/jito-tip.ts delete mode 100644 packages/common-sdk/src/web3/transactions/transactions-builder.ts delete mode 100644 packages/common-sdk/src/web3/transactions/transactions-processor.ts delete mode 100644 packages/common-sdk/src/web3/transactions/types.ts delete mode 100644 packages/common-sdk/src/web3/wallet.ts delete mode 100644 packages/common-sdk/tests/test-context.ts delete mode 100644 packages/common-sdk/tests/tsconfig.json delete mode 100644 packages/common-sdk/tests/utils/expectations.ts delete mode 100644 packages/common-sdk/tests/utils/test-wallet.ts delete mode 100644 packages/common-sdk/tests/web3/ata-util.test.ts delete mode 100644 packages/common-sdk/tests/web3/network/account-requests.test.ts delete mode 100644 packages/common-sdk/tests/web3/network/parsing.test.ts delete mode 100644 packages/common-sdk/tests/web3/network/simple-fetcher-impl.test.ts delete mode 100644 packages/common-sdk/tests/web3/transactions/constants.test.ts delete mode 100644 packages/common-sdk/tests/web3/transactions/transactions-builder.test.ts delete mode 100644 packages/common-sdk/tsconfig-base.json diff --git a/packages/common-sdk/.prettierrc b/packages/common-sdk/.prettierrc deleted file mode 100644 index 19a167c..0000000 --- a/packages/common-sdk/.prettierrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "singleQuote": false, - "explicitTypes": "always", - "bracketSpacing": true -} - diff --git a/packages/common-sdk/LICENSE b/packages/common-sdk/LICENSE deleted file mode 100644 index 1c663f2..0000000 --- a/packages/common-sdk/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -Copyright 2022 Orca Foundation - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. - -You may obtain a copy of the License at: - - http://www.apache.org/licenses/LICENSE-2.0. - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/common-sdk/README.md b/packages/common-sdk/README.md deleted file mode 100644 index c00101b..0000000 --- a/packages/common-sdk/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Orca Common SDK -This package contains a set of utility functions used by other Typescript components in Orca. - -## Run Typescript tests via local validator -To startup local validator, run: -``` -solana-test-validator -``` - -In the common-sdk folder, run: -``` -yarn run test --verbose -``` diff --git a/packages/common-sdk/jest.config.js b/packages/common-sdk/jest.config.js deleted file mode 100644 index 3205f65..0000000 --- a/packages/common-sdk/jest.config.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - "roots": [ - "/src", - "/tests" - ], - "testMatch": [ - "**/__tests__/**/*.+(ts|tsx|js)", - "**/?(*.)+(spec|test).+(ts|tsx|js)" - ], - transform: { - "^.+\\.(ts|tsx)$": ["ts-jest", { - tsconfig: "./tests/tsconfig.json" - }] - }, -} diff --git a/packages/common-sdk/package.json b/packages/common-sdk/package.json deleted file mode 100644 index edc7e13..0000000 --- a/packages/common-sdk/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@orca-so/common-sdk", - "version": "0.6.4", - "description": "Common Typescript components across Orca", - "repository": "https://github.com/orca-so/orca-sdks", - "author": "Orca Foundation", - "license": "MIT", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "peerDependencies": { - "@solana/spl-token": "^0.4.1", - "@solana/web3.js": "^1.90.0", - "decimal.js": "^10.4.3" - }, - "dependencies": { - "tiny-invariant": "^1.3.1" - }, - "devDependencies": { - "@solana/spl-token": "^0.4.1", - "@solana/web3.js": "^1.90.0", - "decimal.js": "^10.4.3" - }, - "scripts": { - "build": "rimraf dist && tsc -p src", - "clean": "rimraf dist", - "watch": "tsc -w -p src", - "prepublishOnly": "yarn build", - "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' 'tests/**/*.ts' --write", - "test": "jest --detectOpenHandles --verbose", - "docs": "npx typedoc --excludePrivate --categorizeByGroup false --tsconfig src/tsconfig.json" - }, - "lint-staged": { - "*.{ts,md}": "yarn run prettier-format" - }, - "files": [ - "/dist" - ] -} diff --git a/packages/common-sdk/src/index.ts b/packages/common-sdk/src/index.ts deleted file mode 100644 index 3e948bd..0000000 --- a/packages/common-sdk/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./math"; -export * from "./web3"; diff --git a/packages/common-sdk/src/math/decimal-util.ts b/packages/common-sdk/src/math/decimal-util.ts deleted file mode 100644 index da9f3a6..0000000 --- a/packages/common-sdk/src/math/decimal-util.ts +++ /dev/null @@ -1,26 +0,0 @@ -import BN from "bn.js"; -import Decimal from "decimal.js"; - -export class DecimalUtil { - public static adjustDecimals(input: Decimal, shift = 0): Decimal { - return input.div(Decimal.pow(10, shift)); - } - - public static fromBN(input: BN, shift = 0): Decimal { - return new Decimal(input.toString()).div(new Decimal(10).pow(shift)); - } - - public static fromNumber(input: number, shift = 0): Decimal { - return new Decimal(input).div(new Decimal(10).pow(shift)); - } - - public static toBN(input: Decimal, shift = 0): BN { - if (input.isNeg()) { - throw new Error("Negative decimal value ${input} cannot be converted to BN."); - } - - const shiftedValue = input.mul(new Decimal(10).pow(shift)); - const zeroDecimalValue = shiftedValue.trunc(); - return new BN(zeroDecimalValue.toString()); - } -} diff --git a/packages/common-sdk/src/math/index.ts b/packages/common-sdk/src/math/index.ts deleted file mode 100644 index 8f0c21a..0000000 --- a/packages/common-sdk/src/math/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./decimal-util"; -export * from "./math-util"; -export * from "./percentage"; diff --git a/packages/common-sdk/src/math/math-util.ts b/packages/common-sdk/src/math/math-util.ts deleted file mode 100644 index 24997d8..0000000 --- a/packages/common-sdk/src/math/math-util.ts +++ /dev/null @@ -1,79 +0,0 @@ -import BN from "bn.js"; -import Decimal from "decimal.js"; - -/** - * @category Math - */ -export const ZERO = new BN(0); - -/** - * @category Math - */ -export const ONE = new BN(1); - -/** - * @category Math - */ -export const TWO = new BN(2); - -/** - * @category Math - */ -export const U128 = TWO.pow(new BN(128)); - -/** - * @category Math - */ -export const U64_MAX = TWO.pow(new BN(64)).sub(ONE); - -/** - * @category Math - */ -export class MathUtil { - public static toX64_BN(num: BN): BN { - return num.mul(new BN(2).pow(new BN(64))); - } - - public static toX64_Decimal(num: Decimal): Decimal { - return num.mul(Decimal.pow(2, 64)); - } - - public static toX64(num: Decimal): BN { - return new BN(num.mul(Decimal.pow(2, 64)).floor().toFixed()); - } - - public static fromX64(num: BN): Decimal { - return new Decimal(num.toString()).mul(Decimal.pow(2, -64)); - } - - public static fromX64_Decimal(num: Decimal): Decimal { - return num.mul(Decimal.pow(2, -64)); - } - - public static fromX64_BN(num: BN): BN { - return num.div(new BN(2).pow(new BN(64))); - } - - public static shiftRightRoundUp(n: BN): BN { - let result = n.shrn(64); - - if (n.mod(U64_MAX).gt(ZERO)) { - result = result.add(ONE); - } - - return result; - } - - public static divRoundUp(n0: BN, n1: BN): BN { - const hasRemainder = !n0.mod(n1).eq(ZERO); - if (hasRemainder) { - return n0.div(n1).add(new BN(1)); - } else { - return n0.div(n1); - } - } - - public static subUnderflowU128(n0: BN, n1: BN): BN { - return n0.add(U128).sub(n1).mod(U128); - } -} diff --git a/packages/common-sdk/src/math/percentage.ts b/packages/common-sdk/src/math/percentage.ts deleted file mode 100644 index ab33e72..0000000 --- a/packages/common-sdk/src/math/percentage.ts +++ /dev/null @@ -1,51 +0,0 @@ -import BN from "bn.js"; -import Decimal from "decimal.js"; - -/** - * @category Math - */ -export class Percentage { - readonly numerator: BN; - readonly denominator: BN; - - constructor(numerator: BN, denominator: BN) { - this.numerator = numerator; - this.denominator = denominator; - } - - public static fromDecimal(number: Decimal): Percentage { - return Percentage.fromFraction(number.mul(100000).toNumber(), 10000000); - } - - public static fromFraction(numerator: BN | number, denominator: BN | number): Percentage { - const num = typeof numerator === "number" ? new BN(numerator.toString()) : numerator; - const denom = typeof denominator === "number" ? new BN(denominator.toString()) : denominator; - return new Percentage(num, denom); - } - - public toString = (): string => { - return `${this.numerator.toString()}/${this.denominator.toString()}`; - }; - - public toDecimal() { - if (this.denominator.eq(new BN(0))) { - return new Decimal(0); - } - return new Decimal(this.numerator.toString()).div(new Decimal(this.denominator.toString())); - } - - public add(p2: Percentage): Percentage { - const denomGcd = this.denominator.gcd(p2.denominator); - const denomLcm = this.denominator.div(denomGcd).mul(p2.denominator); - - const p1DenomAdjustment = denomLcm.div(this.denominator); - const p2DenomAdjustment = denomLcm.div(p2.denominator); - - const p1NumeratorAdjusted = this.numerator.mul(p1DenomAdjustment); - const p2NumeratorAdjusted = p2.numerator.mul(p2DenomAdjustment); - - const newNumerator = p1NumeratorAdjusted.add(p2NumeratorAdjusted); - - return new Percentage(new BN(newNumerator.toString()), new BN(denomLcm.toString())); - } -} diff --git a/packages/common-sdk/src/tsconfig.json b/packages/common-sdk/src/tsconfig.json deleted file mode 100644 index 8d6c266..0000000 --- a/packages/common-sdk/src/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../tsconfig-base.json", - "compilerOptions": { - "composite": true - }, - "include": ["./**/*"], - "typedocOptions": { - "entryPoints": ["index.ts"], - "out": "../../target/typedocs" - } -} diff --git a/packages/common-sdk/src/web3/address-util.ts b/packages/common-sdk/src/web3/address-util.ts deleted file mode 100644 index ebecfd6..0000000 --- a/packages/common-sdk/src/web3/address-util.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; - -export declare type Address = PublicKey | string; - -/** - * @category Util - */ -export type PDA = { publicKey: PublicKey; bump: number }; - -/** - * @category Util - */ -export class AddressUtil { - public static toPubKey(address: Address): PublicKey { - return address instanceof PublicKey ? address : new PublicKey(address); - } - - public static toPubKeys(addresses: Address[]): PublicKey[] { - return addresses.map((address) => AddressUtil.toPubKey(address)); - } - - public static toString(address: Address): string { - if (typeof address === "string") { - return address; - } - return AddressUtil.toPubKey(address).toBase58(); - } - - public static toStrings(addresses: Address[]): string[] { - return addresses.map((address) => AddressUtil.toString(address)); - } - - public static findProgramAddress(seeds: (Uint8Array | Buffer)[], programId: PublicKey): PDA { - const [publicKey, bump] = PublicKey.findProgramAddressSync(seeds, programId); - return { publicKey, bump }; - } -} diff --git a/packages/common-sdk/src/web3/ata-util.ts b/packages/common-sdk/src/web3/ata-util.ts deleted file mode 100644 index 2f06fb7..0000000 --- a/packages/common-sdk/src/web3/ata-util.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { - NATIVE_MINT, - NATIVE_MINT_2022, - createAssociatedTokenAccountIdempotentInstruction, - createAssociatedTokenAccountInstruction, - getAssociatedTokenAddressSync, -} from "@solana/spl-token"; -import { Connection, PublicKey } from "@solana/web3.js"; -import BN from "bn.js"; -import { ZERO } from "../math"; -import { ParsableMintInfo, ParsableTokenAccountInfo, getMultipleParsedAccounts } from "./network"; -import { ResolvedTokenAddressInstruction, TokenUtil, WrappedSolAccountCreateMethod } from "./token-util"; -import { EMPTY_INSTRUCTION } from "./transactions/types"; -import invariant from "tiny-invariant"; - -/** - * IMPORTANT: wrappedSolAmountIn should only be used for input/source token that - * could be SOL. This is because when SOL is the output, it is the end - * destination, and thus does not need to be wrapped with an amount. - * - * @param connection Solana connection class - * @param ownerAddress The user's public key - * @param tokenMint Token mint address - * @param wrappedSolAmountIn Optional. Only use for input/source token that could be SOL - * @param payer Payer that would pay the rent for the creation of the ATAs - * @param modeIdempotent Optional. Use CreateIdempotent instruction instead of Create instruction - * @param allowPDAOwnerAddress Optional. Allow PDA to be used as the ATA owner address - * @param wrappedSolAccountCreateMethod - Optional. How to create the temporary WSOL account. - * @returns - */ -export async function resolveOrCreateATA( - connection: Connection, - ownerAddress: PublicKey, - tokenMint: PublicKey, - getAccountRentExempt: () => Promise, - wrappedSolAmountIn = ZERO, - payer = ownerAddress, - modeIdempotent: boolean = false, - allowPDAOwnerAddress: boolean = false, - wrappedSolAccountCreateMethod: WrappedSolAccountCreateMethod = "keypair", -): Promise { - const instructions = await resolveOrCreateATAs( - connection, - ownerAddress, - [{ tokenMint, wrappedSolAmountIn }], - getAccountRentExempt, - payer, - modeIdempotent, - allowPDAOwnerAddress, - wrappedSolAccountCreateMethod, - ); - return instructions[0]!; -} - -type ResolvedTokenAddressRequest = { - tokenMint: PublicKey; - wrappedSolAmountIn?: BN; -}; - -/** - * IMPORTANT: wrappedSolAmountIn should only be used for input/source token that - * could be SOL. This is because when SOL is the output, it is the end - * destination, and thus does not need to be wrapped with an amount. - * - * @param connection Solana connection class - * @param ownerAddress The user's public key - * @param tokenMint Token mint address - * @param wrappedSolAmountIn Optional. Only use for input/source token that could be SOL - * @param payer Payer that would pay the rent for the creation of the ATAs - * @param modeIdempotent Optional. Use CreateIdempotent instruction instead of Create instruction - * @param allowPDAOwnerAddress Optional. Allow PDA to be used as the ATA owner address - * @param wrappedSolAccountCreateMethod - Optional. How to create the temporary WSOL account. - * @returns - */ -export async function resolveOrCreateATAs( - connection: Connection, - ownerAddress: PublicKey, - requests: ResolvedTokenAddressRequest[], - getAccountRentExempt: () => Promise, - payer = ownerAddress, - modeIdempotent: boolean = false, - allowPDAOwnerAddress: boolean = false, - wrappedSolAccountCreateMethod: WrappedSolAccountCreateMethod = "keypair", -): Promise { - const nonNativeMints = requests.filter(({ tokenMint }) => !tokenMint.equals(NATIVE_MINT)); - const nativeMints = requests.filter(({ tokenMint }) => tokenMint.equals(NATIVE_MINT)); - const nativeMint2022 = requests.filter(({ tokenMint }) => tokenMint.equals(NATIVE_MINT_2022)); - - if (nativeMints.length > 1) { - throw new Error("Cannot resolve multiple WSolAccounts"); - } - - if (nativeMint2022.length > 0) { - throw new Error("NATIVE_MINT_2022 is not supported"); - } - - let instructionMap: { [tokenMint: string]: ResolvedTokenAddressInstruction } = {}; - if (nonNativeMints.length > 0) { - const mints = await getMultipleParsedAccounts( - connection, - nonNativeMints.map((a) => a.tokenMint), - ParsableMintInfo - ); - - const nonNativeAddresses = nonNativeMints.map(({ tokenMint }, index) => - getAssociatedTokenAddressSync(tokenMint, ownerAddress, allowPDAOwnerAddress, mints[index]!.tokenProgram) - ); - - const tokenAccounts = await getMultipleParsedAccounts( - connection, - nonNativeAddresses, - ParsableTokenAccountInfo - ); - - tokenAccounts.forEach((tokenAccount, index) => { - const ataAddress = nonNativeAddresses[index]!; - let resolvedInstruction: ResolvedTokenAddressInstruction; - if (tokenAccount) { - // ATA whose owner has been changed is abnormal entity. - // To prevent to send swap/withdraw/collect output to the ATA, an error should be thrown. - if (!tokenAccount.owner.equals(ownerAddress)) { - throw new Error(`ATA with change of ownership detected: ${ataAddress.toBase58()}`); - } - - resolvedInstruction = { address: ataAddress, tokenProgram: tokenAccount.tokenProgram, ...EMPTY_INSTRUCTION }; - } else { - const createAtaInstruction = modeIdempotent - ? createAssociatedTokenAccountIdempotentInstruction( - payer, - ataAddress, - ownerAddress, - nonNativeMints[index]!.tokenMint, - mints[index]!.tokenProgram, - ) - : createAssociatedTokenAccountInstruction( - payer, - ataAddress, - ownerAddress, - nonNativeMints[index]!.tokenMint, - mints[index]!.tokenProgram, - ); - - resolvedInstruction = { - address: ataAddress, - tokenProgram: mints[index]!.tokenProgram, - instructions: [createAtaInstruction], - cleanupInstructions: [], - signers: [], - }; - } - instructionMap[nonNativeMints[index].tokenMint.toBase58()] = resolvedInstruction; - }); - } - - if (nativeMints.length > 0) { - const accountRentExempt = await getAccountRentExempt(); - const wrappedSolAmountIn = nativeMints[0]?.wrappedSolAmountIn || ZERO; - instructionMap[NATIVE_MINT.toBase58()] = TokenUtil.createWrappedNativeAccountInstruction( - ownerAddress, - wrappedSolAmountIn, - accountRentExempt, - payer, - undefined, // use default - wrappedSolAccountCreateMethod - ); - } - - // Preserve order of resolution - return requests.map(({ tokenMint }) => instructionMap[tokenMint.toBase58()]); -} diff --git a/packages/common-sdk/src/web3/index.ts b/packages/common-sdk/src/web3/index.ts deleted file mode 100644 index 1852a68..0000000 --- a/packages/common-sdk/src/web3/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./address-util"; -export * from "./ata-util"; -export * from "./lookup-table-fetcher"; -export * from "./network"; -export * from "./public-key-utils"; -export * from "./token-util"; -export * from "./transactions"; -export * from "./wallet"; diff --git a/packages/common-sdk/src/web3/lookup-table-fetcher.ts b/packages/common-sdk/src/web3/lookup-table-fetcher.ts deleted file mode 100644 index e70f4e2..0000000 --- a/packages/common-sdk/src/web3/lookup-table-fetcher.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { AddressLookupTableAccount, PublicKey } from "@solana/web3.js"; - -export interface LookupTable { - address: string; - containedAddresses: string[]; -} - -/** - * Interface for fetching lookup tables for a set of addresses. - * - * Implementations of this class is expected to cache the lookup tables for quicker read lookups. - */ -export interface LookupTableFetcher { - /** - * Given a set of public key addresses, fetches the lookup table accounts that contains these addresses - * and caches them for future lookups. - * @param addresses The addresses to fetch lookup tables for. - * @return The lookup tables that contains the given addresses. - */ - loadLookupTables(addresses: PublicKey[]): Promise; - - /** - * Given a set of public key addresses, fetches the lookup table accounts that contains these addresses. - * @param addresses - The addresses to fetch lookup tables for. - * @return The lookup table accounts that contains the given addresses. - */ - getLookupTableAccountsForAddresses(addresses: PublicKey[]): Promise; -} diff --git a/packages/common-sdk/src/web3/network/account-requests.ts b/packages/common-sdk/src/web3/network/account-requests.ts deleted file mode 100644 index be639ae..0000000 --- a/packages/common-sdk/src/web3/network/account-requests.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { AccountInfo, Connection, PublicKey } from "@solana/web3.js"; -import invariant from "tiny-invariant"; -import { Address, AddressUtil } from "../address-util"; -import { ParsableEntity } from "./parsing"; - -export async function getParsedAccount( - connection: Connection, - address: Address, - parser: ParsableEntity -): Promise { - const value = await connection.getAccountInfo(AddressUtil.toPubKey(address)); - const key = AddressUtil.toPubKey(address); - return parser.parse(key, value); -} - -export async function getMultipleParsedAccounts( - connection: Connection, - addresses: Address[], - parser: ParsableEntity, - chunkSize = 100 -): Promise<(T | null)[]> { - if (addresses.length === 0) { - return []; - } - - const values = await getMultipleAccounts( - connection, - AddressUtil.toPubKeys(addresses), - 10, - chunkSize - ); - const results = values.map((val) => { - if (val[1] === null) { - return null; - } - return parser.parse(val[0], val[1]); - }); - invariant(results.length === addresses.length, "not enough results fetched"); - return results; -} - -// An entry between the key of an address and the account data for that address. -export type FetchedAccountEntry = [PublicKey, AccountInfo | null]; -export type FetchedAccountMap = Map | null>; - -export async function getMultipleAccountsInMap( - connection: Connection, - addresses: Address[], - timeoutAfterSeconds = 10, - chunkSize = 100 -): Promise> { - const results = await getMultipleAccounts(connection, addresses, timeoutAfterSeconds, chunkSize); - return results.reduce((map, [key, value]) => { - map.set(key.toBase58(), value); - return map; - }, new Map | null>()); -} - -export async function getMultipleAccounts( - connection: Connection, - addresses: Address[], - timeoutAfterSeconds = 10, - chunkSize = 100 -): Promise> { - if (addresses.length === 0) { - return []; - } - - const promises: Promise[] = []; - const chunks = Math.ceil(addresses.length / chunkSize); - const result: Array = new Array(chunks); - - for (let i = 0; i < result.length; i++) { - const slice = addresses.slice(i * chunkSize, (i + 1) * chunkSize); - const addressChunk = AddressUtil.toPubKeys(slice); - const promise = new Promise(async (resolve) => { - const res = await connection.getMultipleAccountsInfo(addressChunk); - const fetchedAccountChunk = res.map((result, index) => { - return [addressChunk[index], result] as FetchedAccountEntry; - }); - result[i] = fetchedAccountChunk; - resolve(); - }); - promises.push(promise); - } - - await Promise.race([ - Promise.all(promises), - timeoutAfter(timeoutAfterSeconds, "connection.getMultipleAccountsInfo timeout"), - ]); - - const flattenedResult = result.flat(); - invariant(flattenedResult.length === addresses.length, "getMultipleAccounts not enough results"); - return flattenedResult; -} - -function timeoutAfter(seconds: number, message: string) { - return new Promise((_, reject) => { - setTimeout(() => { - reject(new Error(message)); - }, seconds * 1000); - }); -} diff --git a/packages/common-sdk/src/web3/network/fetcher/index.ts b/packages/common-sdk/src/web3/network/fetcher/index.ts deleted file mode 100644 index 3321baf..0000000 --- a/packages/common-sdk/src/web3/network/fetcher/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { AccountWithTokenProgram, MintWithTokenProgram, ParsableEntity } from ".."; -import { Address } from "../../address-util"; - -export * from "./simple-fetcher-impl"; - -export type BasicSupportedTypes = AccountWithTokenProgram | MintWithTokenProgram; - -/** - * Interface for fetching and caching on-chain accounts - */ -export interface AccountFetcher { - /** - * Fetch an account from the cache or from the network - * @param address The account address to fetch from cache or network - * @param parser The parser to used for theses accounts - * @param opts Options when fetching the accounts - * @returns - */ - getAccount: ( - address: Address, - parser: ParsableEntity, - opts?: AccountFetchOptions - ) => Promise; - - /** - * Fetch multiple accounts from the cache or from the network - * @param address A list of account addresses to fetch from cache or network - * @param parser The parser to used for theses accounts - * @param opts Options when fetching the accounts - * @returns a Map of addresses to accounts. The ordering of the Map iteration is the same as the ordering of the input addresses. - */ - getAccounts: ( - address: Address[], - parser: ParsableEntity, - opts?: AccountFetchOptions - ) => Promise>; - - /** - * Fetch multiple accounts from the cache or from the network and return as an array - * @param address A list of account addresses to fetch from cache or network - * @param parser The parser to used for theses accounts - * @param opts Options when fetching the accounts - * @returns an array of accounts. The ordering of the array is the same as the ordering of the input addresses. - */ - getAccountsAsArray: ( - address: Address[], - parser: ParsableEntity, - opts?: AccountFetchOptions - ) => Promise>; - - /** - * Populate the cache with the given accounts. - * @param accounts A list of accounts addresses to fetched accounts to populate the cache with - * @param parser The parser that was used to parse theses accounts - * @param now The timestamp to use for the cache entries - */ - populateAccounts: ( - accounts: ReadonlyMap, - parser: ParsableEntity, - now: number - ) => void; -} diff --git a/packages/common-sdk/src/web3/network/fetcher/simple-fetcher-impl.ts b/packages/common-sdk/src/web3/network/fetcher/simple-fetcher-impl.ts deleted file mode 100644 index ca7f370..0000000 --- a/packages/common-sdk/src/web3/network/fetcher/simple-fetcher-impl.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { Connection } from "@solana/web3.js"; -import { AccountFetcher } from "."; -import { Address, AddressUtil } from "../../address-util"; -import { getMultipleAccountsInMap } from "../account-requests"; -import { ParsableEntity } from "../parsing"; - -type CachedContent = { - parser: ParsableEntity; - fetchedAt: number; - value: T | null; -}; - -export type RetentionPolicy = ReadonlyMap, number>; - -/** - * Options when fetching the accounts - */ -export type SimpleAccountFetchOptions = { - // Accepted maxAge in milliseconds for a cache entry hit for this account request. - maxAge?: number; - // Timeout in seconds before the RPC call is considered failed. - timeoutInSeconds?: number; -}; - -// SimpleAccountFetcher is a simple implementation of AccountCache that stores the fetched -// accounts in memory. If TTL is not provided, it will use TTL defined in the the retention policy -// for the parser. If that is also not provided, the request will always prefer the cache value. -export class SimpleAccountFetcher - implements AccountFetcher { - cache: Map> = new Map(); - constructor(readonly connection: Connection, readonly retentionPolicy: RetentionPolicy) { - this.cache = new Map>(); - } - - async getAccount( - address: Address, - parser: ParsableEntity, - opts?: FetchOptions | undefined, - now: number = Date.now() - ): Promise { - const addressKey = AddressUtil.toPubKey(address); - const addressStr = AddressUtil.toString(address); - - const cached = this.cache.get(addressStr); - const maxAge = this.getMaxAge(this.retentionPolicy.get(parser), opts); - const elapsed = !!cached ? now - (cached?.fetchedAt ?? 0) : Number.NEGATIVE_INFINITY; - const expired = elapsed > maxAge; - - if (!!cached && !expired) { - return cached.value as U | null; - } - - try { - const accountInfo = await this.connection.getAccountInfo(addressKey); - const value = parser.parse(addressKey, accountInfo); - this.cache.set(addressStr, { parser, value, fetchedAt: now }); - return value; - } catch (e) { - this.cache.set(addressStr, { parser, value: null, fetchedAt: now }); - return null; - } - } - - private getMaxAge(parserMaxAge?: number, opts?: SimpleAccountFetchOptions): number { - if (opts?.maxAge !== undefined) { - return opts.maxAge; - } - return parserMaxAge === undefined ? Number.POSITIVE_INFINITY : parserMaxAge; - } - - async getAccounts( - addresses: Address[], - parser: ParsableEntity, - opts?: SimpleAccountFetchOptions | undefined, - now: number = Date.now() - ): Promise> { - const addressStrs = AddressUtil.toStrings(addresses); - await this.fetchAndPopulateCache(addressStrs, parser, opts, now); - - // Build a map of the results, insert by the order of the addresses parameter - const result = new Map(); - addressStrs.forEach((addressStr) => { - const cached = this.cache.get(addressStr); - const value = cached?.value as U | null; - result.set(addressStr, value); - }); - - return result; - } - - async getAccountsAsArray( - addresses: Address[], - parser: ParsableEntity, - opts?: FetchOptions | undefined, - now: number = Date.now() - ): Promise> { - const addressStrs = AddressUtil.toStrings(addresses); - await this.fetchAndPopulateCache(addressStrs, parser, opts, now); - - // Rebuild an array containing the results, insert by the order of the addresses parameter - const result = new Array(); - addressStrs.forEach((addressStr) => { - const cached = this.cache.get(addressStr); - const value = cached?.value as U | null; - result.push(value); - }); - - return result; - } - - populateAccounts( - accounts: ReadonlyMap, - parser: ParsableEntity, - now: number - ): void { - Array.from(accounts.entries()).forEach(([key, value]) => { - this.cache.set(key, { parser, value, fetchedAt: now }); - }); - } - - async refreshAll(now: number = Date.now(), timeoutInSeconds?: number) { - const addresses = Array.from(this.cache.keys()); - const fetchedAccountsMap = await getMultipleAccountsInMap(this.connection, addresses, timeoutInSeconds); - - for (const [key, cachedContent] of this.cache.entries()) { - const parser = cachedContent.parser; - const fetchedEntry = fetchedAccountsMap.get(key); - const value = parser.parse(AddressUtil.toPubKey(key), fetchedEntry); - this.cache.set(key, { parser, value, fetchedAt: now }); - } - } - - private async fetchAndPopulateCache( - addresses: Address[], - parser: ParsableEntity, - opts?: SimpleAccountFetchOptions | undefined, - now: number = Date.now() - ) { - const addressStrs = AddressUtil.toStrings(addresses); - const maxAge = this.getMaxAge(this.retentionPolicy.get(parser), opts); - - // Filter out all unexpired accounts to get the accounts to fetch - const undefinedAccounts = addressStrs.filter((addressStr) => { - const cached = this.cache.get(addressStr); - const elapsed = !!cached ? now - (cached?.fetchedAt ?? 0) : Number.NEGATIVE_INFINITY; - const expired = elapsed > maxAge; - return !cached || expired; - }); - - // Fetch all undefined accounts and place in cache - // TODO: We currently do not support contextSlot consistency across the batched getMultipleAccounts call - // If the addresses list contain accounts in the 1st gMA call as subsequent calls and the gMA returns on different contextSlots, - // the returned results can be inconsistent and unexpected by the user. - if (undefinedAccounts.length > 0) { - const fetchedAccountsMap = await getMultipleAccountsInMap(this.connection, undefinedAccounts, opts?.timeoutInSeconds); - undefinedAccounts.forEach((key) => { - const fetchedEntry = fetchedAccountsMap.get(key); - const value = parser.parse(AddressUtil.toPubKey(key), fetchedEntry); - this.cache.set(key, { parser, value, fetchedAt: now }); - }); - } - } -} diff --git a/packages/common-sdk/src/web3/network/index.ts b/packages/common-sdk/src/web3/network/index.ts deleted file mode 100644 index 1d1f278..0000000 --- a/packages/common-sdk/src/web3/network/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./account-requests"; -export * from "./fetcher"; -export * from "./parsing"; -export * from "./types"; diff --git a/packages/common-sdk/src/web3/network/parsing.ts b/packages/common-sdk/src/web3/network/parsing.ts deleted file mode 100644 index 269a118..0000000 --- a/packages/common-sdk/src/web3/network/parsing.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { unpackAccount, unpackMint } from "@solana/spl-token"; -import { AccountInfo, PublicKey } from "@solana/web3.js"; -import { AccountWithTokenProgram, MintWithTokenProgram } from "./types"; - -/** - * Static abstract class definition to parse entities. - * @category Parsables - */ -export interface ParsableEntity { - /** - * Parse account data - * - * @param accountData Buffer data for the entity - * @returns Parsed entity - */ - parse: (address: PublicKey, accountData: AccountInfo | undefined | null) => T | null; -} - -/** - * @category Parsables - */ -@staticImplements>() -export class ParsableTokenAccountInfo { - private constructor() {} - - public static parse( - address: PublicKey, - data: AccountInfo | undefined | null - ): AccountWithTokenProgram | null { - if (!data) { - return null; - } - - try { - return { ...unpackAccount(address, data, data.owner), tokenProgram: data.owner }; - } catch (e) { - console.error(`error while parsing TokenAccount ${address.toBase58()}: ${e}`); - - return null; - } - } -} - -/** - * @category Parsables - */ -@staticImplements>() -export class ParsableMintInfo { - private constructor() {} - - public static parse( - address: PublicKey, - data: AccountInfo | undefined | null - ): MintWithTokenProgram | null { - if (!data) { - return null; - } - - try { - return { ...unpackMint(address, data, data.owner), tokenProgram: data.owner }; - } catch (e) { - console.error(`error while parsing Mint ${address.toBase58()}: ${e}`); - return null; - } - } -} - -/** - * Class decorator to define an interface with static methods - * Reference: https://github.com/Microsoft/TypeScript/issues/13462#issuecomment-295685298 - */ -export function staticImplements() { - return (constructor: U) => { - constructor; - }; -} diff --git a/packages/common-sdk/src/web3/network/types.ts b/packages/common-sdk/src/web3/network/types.ts deleted file mode 100644 index b1847ff..0000000 --- a/packages/common-sdk/src/web3/network/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Account, Mint } from "@solana/spl-token"; -import { PublicKey } from "@solana/web3.js"; - -/** - * @category Parsables - */ -export type MintWithTokenProgram = Mint & { tokenProgram: PublicKey }; - -/** - * @category Parsables - */ -export type AccountWithTokenProgram = Account & { tokenProgram: PublicKey }; diff --git a/packages/common-sdk/src/web3/public-key-utils.ts b/packages/common-sdk/src/web3/public-key-utils.ts deleted file mode 100644 index 1b253ed..0000000 --- a/packages/common-sdk/src/web3/public-key-utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; - -export class PublicKeyUtils { - /** - * Check whether a string is a Base58 string. - * @param value - * @returns Whether the string is a Base58 string. - */ - public static isBase58(value: string) { - return /^[A-HJ-NP-Za-km-z1-9]*$/.test(value); - } - - /** - * Order a list of public keys by bytes. - * @param keys a list of public keys to order - * @returns an ordered array of public keys - */ - public static orderKeys(...keys: PublicKey[]): PublicKey[] { - return keys.sort(comparePublicKeys); - } -} - -function comparePublicKeys(key1: PublicKey, key2: PublicKey): number { - const bytes1 = key1.toBytes(); - const bytes2 = key2.toBytes(); - - // PublicKeys should be zero-padded 32 byte length - if (bytes1.byteLength !== bytes2.byteLength) { - return bytes1.byteLength - bytes2.byteLength; - } - - for (let i = 0; i < bytes1.byteLength; i++) { - let byte1 = bytes1[i]; - let byte2 = bytes2[i]; - if (byte1 !== byte2) { - return byte1 - byte2; - } - } - - return 0; -} diff --git a/packages/common-sdk/src/web3/token-util.ts b/packages/common-sdk/src/web3/token-util.ts deleted file mode 100644 index 4003dc8..0000000 --- a/packages/common-sdk/src/web3/token-util.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { - AccountLayout, - NATIVE_MINT, - NATIVE_MINT_2022, - TOKEN_PROGRAM_ID, - createAssociatedTokenAccountIdempotentInstruction, - createCloseAccountInstruction, - createInitializeAccountInstruction, - createSyncNativeInstruction, - createTransferCheckedWithTransferHookInstruction, - getAssociatedTokenAddressSync, -} from "@solana/spl-token"; -import { Connection, Keypair, PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js"; -import { sha256 } from '@noble/hashes/sha256'; -import BN from "bn.js"; -import invariant from "tiny-invariant"; -import { ZERO } from "../math"; -import { Instruction, resolveOrCreateATA } from "../web3"; -/** - * @category Util - */ -export type ResolvedTokenAddressInstruction = { - address: PublicKey; - tokenProgram: PublicKey; -} & Instruction; - -/** - * @category Util - */ -export type WrappedSolAccountCreateMethod = "keypair" | "withSeed" | "ata"; - -/** - * @category Util - */ -export class TokenUtil { - public static isNativeMint(mint: PublicKey) { - return mint.equals(NATIVE_MINT); - } - - /** - * Create an ix to send a native-mint and unwrap it to the user's wallet. - * @param owner - PublicKey for the owner of the temporary WSOL account. - * @param amountIn - Amount of SOL to wrap. - * @param rentExemptLamports - Rent exempt lamports for the temporary WSOL account. - * @param payer - PublicKey for the payer that would fund the temporary WSOL accounts. (must sign the txn) - * @param unwrapDestination - PublicKey for the receiver that would receive the unwrapped SOL including rent. - * @param createAccountMethod - How to create the temporary WSOL account. - * @returns - */ - public static createWrappedNativeAccountInstruction( - owner: PublicKey, - amountIn: BN, - rentExemptLamports: number, - payer?: PublicKey, - unwrapDestination?: PublicKey, - createAccountMethod: WrappedSolAccountCreateMethod = "keypair", - ): ResolvedTokenAddressInstruction { - const payerKey = payer ?? owner; - const unwrapDestinationKey = unwrapDestination ?? owner; - - switch (createAccountMethod) { - case "ata": - return createWrappedNativeAccountInstructionWithATA( - owner, - amountIn, - rentExemptLamports, - payerKey, - unwrapDestinationKey - ); - case "keypair": - return createWrappedNativeAccountInstructionWithKeypair( - owner, - amountIn, - rentExemptLamports, - payerKey, - unwrapDestinationKey - ); - case "withSeed": - return createWrappedNativeAccountInstructionWithSeed( - owner, - amountIn, - rentExemptLamports, - payerKey, - unwrapDestinationKey - ); - default: - throw new Error(`Invalid createAccountMethod: ${createAccountMethod}`); - } - } - - /** - * Create an ix to send a spl-token / native-mint to another wallet. - * This function will handle the associated token accounts internally for spl-token. - * SOL is sent directly to the user's wallet. - * - * @param connection - Connection object - * @param sourceWallet - PublicKey for the sender's wallet - * @param destinationWallet - PublicKey for the receiver's wallet - * @param tokenMint - Mint for the token that is being sent. - * @param tokenDecimals - Decimal for the token that is being sent. - * @param amount - Amount of token to send - * @param getAccountRentExempt - Fn to fetch the account rent exempt value - * @param payer - PublicKey for the payer that would fund the possibly new token-accounts. (must sign the txn) - * @param allowPDASourceWallet - Allow PDA to be used as the source wallet. - * @returns - */ - static async createSendTokensToWalletInstruction( - connection: Connection, - sourceWallet: PublicKey, - destinationWallet: PublicKey, - tokenMint: PublicKey, - tokenDecimals: number, - amount: BN, - getAccountRentExempt: () => Promise, - payer?: PublicKey, - allowPDASourceWallet: boolean = false - ): Promise { - invariant(!amount.eq(ZERO), "SendToken transaction must send more than 0 tokens."); - invariant(!tokenMint.equals(NATIVE_MINT_2022), "NATIVE_MINT_2022 is not supported."); - - // Specifically handle SOL, which is not a spl-token. - if (tokenMint.equals(NATIVE_MINT)) { - const sendSolTxn = SystemProgram.transfer({ - fromPubkey: sourceWallet, - toPubkey: destinationWallet, - lamports: BigInt(amount.toString()), - }); - return { - instructions: [sendSolTxn], - cleanupInstructions: [], - signers: [], - }; - } - - const mintAccountInfo = await connection.getAccountInfo(tokenMint); - if (mintAccountInfo === null) throw Error("Cannot fetch tokenMint."); - const tokenProgram = mintAccountInfo.owner; - - const sourceTokenAccount = getAssociatedTokenAddressSync(tokenMint, sourceWallet, allowPDASourceWallet, tokenProgram); - const { address: destinationTokenAccount, ...destinationAtaIx } = await resolveOrCreateATA( - connection, - destinationWallet, - tokenMint, - getAccountRentExempt, - amount, - payer, - undefined, - true - ); - - const transferIx = await createTransferCheckedWithTransferHookInstruction( - connection, - sourceTokenAccount, - tokenMint, - destinationTokenAccount, - sourceWallet, - BigInt(amount.toString()), - tokenDecimals, - undefined, - undefined, - tokenProgram - ); - - return { - instructions: destinationAtaIx.instructions.concat(transferIx), - cleanupInstructions: destinationAtaIx.cleanupInstructions, - signers: destinationAtaIx.signers, - }; - } -} - -function createWrappedNativeAccountInstructionWithATA( - owner: PublicKey, - amountIn: BN, - _rentExemptLamports: number, - payerKey: PublicKey, - unwrapDestinationKey: PublicKey, -): ResolvedTokenAddressInstruction { - const tempAccount = getAssociatedTokenAddressSync(NATIVE_MINT, owner); - - const instructions: TransactionInstruction[] = [ - createAssociatedTokenAccountIdempotentInstruction( - payerKey, - tempAccount, - owner, - NATIVE_MINT - ) - ]; - - if (amountIn.gt(ZERO)) { - instructions.push(SystemProgram.transfer({ - fromPubkey: payerKey, - toPubkey: tempAccount, - lamports: amountIn.toNumber(), - })); - - instructions.push(createSyncNativeInstruction( - tempAccount, - )); - } - - const closeWSOLAccountInstruction = createCloseAccountInstruction( - tempAccount, - unwrapDestinationKey, - owner - ); - - return { - address: tempAccount, - tokenProgram: TOKEN_PROGRAM_ID, - instructions, - cleanupInstructions: [closeWSOLAccountInstruction], - signers: [], - }; -} - -function createWrappedNativeAccountInstructionWithKeypair( - owner: PublicKey, - amountIn: BN, - rentExemptLamports: number, - payerKey: PublicKey, - unwrapDestinationKey: PublicKey, -): ResolvedTokenAddressInstruction { - const tempAccount = new Keypair(); - - const createAccountInstruction = SystemProgram.createAccount({ - fromPubkey: payerKey, - newAccountPubkey: tempAccount.publicKey, - lamports: amountIn.toNumber() + rentExemptLamports, - space: AccountLayout.span, - programId: TOKEN_PROGRAM_ID, - }); - - const initAccountInstruction = createInitializeAccountInstruction( - tempAccount.publicKey, - NATIVE_MINT, - owner - ); - - const closeWSOLAccountInstruction = createCloseAccountInstruction( - tempAccount.publicKey, - unwrapDestinationKey, - owner - ); - - return { - address: tempAccount.publicKey, - tokenProgram: TOKEN_PROGRAM_ID, - instructions: [createAccountInstruction, initAccountInstruction], - cleanupInstructions: [closeWSOLAccountInstruction], - signers: [tempAccount], - }; -} - -function createWrappedNativeAccountInstructionWithSeed( - owner: PublicKey, - amountIn: BN, - rentExemptLamports: number, - payerKey: PublicKey, - unwrapDestinationKey: PublicKey, -): ResolvedTokenAddressInstruction { - // seed is always shorter than a signature. - // So createWrappedNativeAccountInstructionWithSeed always generates small size instructions - // than createWrappedNativeAccountInstructionWithKeypair. - const seed = Keypair.generate().publicKey.toBase58().slice(0, 32); // 32 chars - - const tempAccount = (() => { - // same to PublicKey.createWithSeed, but this one is synchronous - const fromPublicKey = owner; - const programId = TOKEN_PROGRAM_ID; - const buffer = Buffer.concat([ - fromPublicKey.toBuffer(), - Buffer.from(seed), - programId.toBuffer(), - ]); - const publicKeyBytes = sha256(buffer); - return new PublicKey(publicKeyBytes); - })(); - - const createAccountInstruction = SystemProgram.createAccountWithSeed({ - fromPubkey: payerKey, - basePubkey: owner, - seed, - newAccountPubkey: tempAccount, - lamports: amountIn.toNumber() + rentExemptLamports, - space: AccountLayout.span, - programId: TOKEN_PROGRAM_ID, - }); - - const initAccountInstruction = createInitializeAccountInstruction( - tempAccount, - NATIVE_MINT, - owner - ); - - const closeWSOLAccountInstruction = createCloseAccountInstruction( - tempAccount, - unwrapDestinationKey, - owner - ); - - return { - address: tempAccount, - tokenProgram: TOKEN_PROGRAM_ID, - instructions: [createAccountInstruction, initAccountInstruction], - cleanupInstructions: [closeWSOLAccountInstruction], - signers: [], - }; -} diff --git a/packages/common-sdk/src/web3/transactions/compute-budget.ts b/packages/common-sdk/src/web3/transactions/compute-budget.ts deleted file mode 100644 index 1b24f3a..0000000 --- a/packages/common-sdk/src/web3/transactions/compute-budget.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { AddressLookupTableAccount, Connection, PublicKey, RecentPrioritizationFees, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; -import { Instruction } from "./types"; - -export const MICROLAMPORTS_PER_LAMPORT = 1_000_000; -export const DEFAULT_PRIORITY_FEE_PERCENTILE = 0.9; -export const DEFAULT_MAX_PRIORITY_FEE_LAMPORTS = 1000000; // 0.001 SOL -export const DEFAULT_MIN_PRIORITY_FEE_LAMPORTS = 0; // 0 SOL -export const DEFAULT_MAX_COMPUTE_UNIT_LIMIT = 1_400_000; - -export async function estimateComputeBudgetLimit( - connection: Connection, - instructions: Instruction[], - lookupTableAccounts: AddressLookupTableAccount[] | undefined, - payer: PublicKey, - margin: number, -): Promise { - try { - const txMainInstructions = instructions.flatMap((instruction) => instruction.instructions); - const txCleanupInstruction = instructions.flatMap((instruction) => instruction.cleanupInstructions); - const txMessage = new TransactionMessage({ - recentBlockhash: PublicKey.default.toBase58(), - payerKey: payer, - instructions: [...txMainInstructions, ...txCleanupInstruction], - }).compileToV0Message(lookupTableAccounts); - - const tx = new VersionedTransaction(txMessage); - - const simulation = await connection.simulateTransaction(tx, { sigVerify: false, replaceRecentBlockhash: true }); - if (!simulation.value.unitsConsumed) { - return DEFAULT_MAX_COMPUTE_UNIT_LIMIT - } - const marginUnits = Math.max(100_000, margin * simulation.value.unitsConsumed); - const estimatedUnits = Math.ceil(simulation.value.unitsConsumed + marginUnits); - return Math.min(DEFAULT_MAX_COMPUTE_UNIT_LIMIT, estimatedUnits); - } catch { - return DEFAULT_MAX_COMPUTE_UNIT_LIMIT; - } -} - -export async function getPriorityFeeInLamports( - connection: Connection, - computeBudgetLimit: number, - lockedWritableAccounts: PublicKey[], - percentile: number = DEFAULT_PRIORITY_FEE_PERCENTILE, - getRecentPrioritizationFees?: (lockedWritableAccounts: PublicKey[]) => Promise -): Promise { - const recentPriorityFees = await (getRecentPrioritizationFees ? getRecentPrioritizationFees(lockedWritableAccounts) : connection.getRecentPrioritizationFees({ - lockedWritableAccounts, - })); - const priorityFee = getPriorityFeeSuggestion(recentPriorityFees, percentile); - return (priorityFee * computeBudgetLimit) / MICROLAMPORTS_PER_LAMPORT; -} - -function getPriorityFeeSuggestion(recentPriorityFees: RecentPrioritizationFees[], percentile: number): number { - // Take the Xth percentile of all the slots returned - const sortedPriorityFees = recentPriorityFees - .sort((a, b) => a.prioritizationFee - b.prioritizationFee); - const percentileIndex = Math.min( - Math.max(Math.floor(sortedPriorityFees.length * percentile), 0), - sortedPriorityFees.length - 1 - ); - return sortedPriorityFees[percentileIndex].prioritizationFee; -} - -export function getLockWritableAccounts(instructions: Instruction[]): PublicKey[] { - return instructions - .flatMap((instruction) => [...instruction.instructions, ...instruction.cleanupInstructions]) - .flatMap((instruction) => instruction.keys) - .filter((key) => key.isWritable) - .map((key) => key.pubkey); -} diff --git a/packages/common-sdk/src/web3/transactions/constants.ts b/packages/common-sdk/src/web3/transactions/constants.ts deleted file mode 100644 index a5887eb..0000000 --- a/packages/common-sdk/src/web3/transactions/constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PACKET_DATA_SIZE } from "@solana/web3.js"; - -// The hard-coded limit of a transaction size in bytes -export const TX_SIZE_LIMIT = PACKET_DATA_SIZE; // 1232 - -// The hard-coded limit of an encoded transaction size in bytes -export const TX_BASE64_ENCODED_SIZE_LIMIT = Math.ceil(TX_SIZE_LIMIT / 3) * 4; // 1644 - -// A dummy blockhash to use for measuring transaction sizes -export const MEASUREMENT_BLOCKHASH = { - blockhash: "65FJ2gp6jC2x87bycfdZpxDyjiodcAoymxR6PMZzfavY", - lastValidBlockHeight: 160381350, -}; diff --git a/packages/common-sdk/src/web3/transactions/index.ts b/packages/common-sdk/src/web3/transactions/index.ts deleted file mode 100644 index 5ad167e..0000000 --- a/packages/common-sdk/src/web3/transactions/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./compute-budget"; -export * from "./constants"; -export * from "./transactions-builder"; -export * from "./transactions-processor"; -export * from "./types"; diff --git a/packages/common-sdk/src/web3/transactions/jito-tip.ts b/packages/common-sdk/src/web3/transactions/jito-tip.ts deleted file mode 100644 index aad4f25..0000000 --- a/packages/common-sdk/src/web3/transactions/jito-tip.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; - -// https://jito-foundation.gitbook.io/mev/mev-payment-and-distribution/on-chain-addresses -const jitoTipAddresses = [ - "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", - "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", - "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", - "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", - "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", - "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", - "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", - "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", -]; - -export function getJitoTipAddress(): PublicKey { - // just pick a random one from the list. There are multiple addresses so that no single one - // can cause local congestion. - return new PublicKey(jitoTipAddresses[Math.floor(Math.random() * jitoTipAddresses.length)]); -} diff --git a/packages/common-sdk/src/web3/transactions/transactions-builder.ts b/packages/common-sdk/src/web3/transactions/transactions-builder.ts deleted file mode 100644 index 093bc6e..0000000 --- a/packages/common-sdk/src/web3/transactions/transactions-builder.ts +++ /dev/null @@ -1,558 +0,0 @@ -import { - AddressLookupTableAccount, - Commitment, - ComputeBudgetProgram, - Connection, - PACKET_DATA_SIZE, - PublicKey, - RecentPrioritizationFees, - SendOptions, - Signer, - SystemProgram, - Transaction, - TransactionInstruction, - TransactionMessage, - VersionedTransaction, -} from "@solana/web3.js"; -import { Wallet } from "../wallet"; -import { - DEFAULT_MAX_COMPUTE_UNIT_LIMIT, - DEFAULT_MAX_PRIORITY_FEE_LAMPORTS, - DEFAULT_MIN_PRIORITY_FEE_LAMPORTS, - DEFAULT_PRIORITY_FEE_PERCENTILE, - MICROLAMPORTS_PER_LAMPORT, - estimateComputeBudgetLimit, - getLockWritableAccounts, - getPriorityFeeInLamports, -} from "./compute-budget"; -import { MEASUREMENT_BLOCKHASH } from "./constants"; -import { Instruction, TransactionPayload } from "./types"; -import { getJitoTipAddress } from "./jito-tip"; - -/** - Build options when building a transaction using TransactionBuilder - @param latestBlockhash - The latest blockhash to use when building the transaction. - @param blockhashCommitment - If latestBlockhash is not provided, the commitment level to use when fetching the latest blockhash. - @param maxSupportedTransactionVersion - The transaction version to build. If set to "legacy", the transaction will - be built using the legacy transaction format. Otherwise, the transaction - will be built using the VersionedTransaction format. - @param lookupTableAccounts - If the build support VersionedTransactions, allow providing the lookup - table accounts to use when building the transaction. This is only used - when maxSupportedTransactionVersion is set to a number. - @param computeBudgetOption - The compute budget limit and priority fee to use when building the transaction. - This defaults to 'none'. - */ -export type BuildOptions = LegacyBuildOption | V0BuildOption; - -type LegacyBuildOption = { - maxSupportedTransactionVersion: "legacy"; -} & BaseBuildOption; - -type V0BuildOption = { - maxSupportedTransactionVersion: number; - lookupTableAccounts?: AddressLookupTableAccount[]; -} & BaseBuildOption; - -type BaseBuildOption = { - latestBlockhash?: { - blockhash: string; - lastValidBlockHeight: number; - }; - computeBudgetOption?: ComputeBudgetOption; - blockhashCommitment: Commitment; -}; - -type ComputeBudgetOption = - | { - type: "none"; - } - | { - type: "fixed"; - priorityFeeLamports: number; - computeBudgetLimit?: number; - jitoTipLamports?: number; - } - | { - type: "auto"; - maxPriorityFeeLamports?: number; - minPriorityFeeLamports?: number; - jitoTipLamports?: number; - computeLimitMargin?: number; - computePricePercentile?: number; - getPriorityFeePerUnit?: ( - lockedWritableAccounts: PublicKey[], - ) => Promise; - }; - -type SyncBuildOptions = BuildOptions & Required; - -const LEGACY_TX_UNIQUE_KEYS_LIMIT = 35; - -/** - * A set of options that the builder will use by default, unless overridden by the user in each method. - */ -export type TransactionBuilderOptions = { - defaultBuildOption: BuildOptions; - defaultSendOption: SendOptions; - defaultConfirmationCommitment: Commitment; -}; - -export const defaultTransactionBuilderOptions: TransactionBuilderOptions = { - defaultBuildOption: { - maxSupportedTransactionVersion: 0, - blockhashCommitment: "confirmed", - }, - defaultSendOption: { - skipPreflight: false, - preflightCommitment: "confirmed", - maxRetries: 3, - }, - defaultConfirmationCommitment: "confirmed", -}; - -/** - * Transaction builder for composing, building and sending transactions. - * @category Transactions - */ -export class TransactionBuilder { - private instructions: Instruction[]; - private signers: Signer[]; - readonly opts: TransactionBuilderOptions; - - constructor( - readonly connection: Connection, - readonly wallet: Wallet, - defaultOpts?: TransactionBuilderOptions, - ) { - this.instructions = []; - this.signers = []; - this.opts = defaultOpts ?? defaultTransactionBuilderOptions; - } - - /** - * Append an instruction into this builder. - * @param instruction - An Instruction - * @returns Returns this transaction builder. - */ - addInstruction(instruction: Instruction): TransactionBuilder { - this.instructions.push(instruction); - return this; - } - - /** - * Append a list of instructions into this builder. - * @param instructions - A list of Instructions - * @returns Returns this transaction builder. - */ - addInstructions(instructions: Instruction[]): TransactionBuilder { - this.instructions = this.instructions.concat(instructions); - return this; - } - - /** - * Prepend a list of instructions into this builder. - * @param instruction - An Instruction - * @returns Returns this transaction builder. - */ - prependInstruction(instruction: Instruction): TransactionBuilder { - this.instructions.unshift(instruction); - return this; - } - - /** - * Prepend a list of instructions into this builder. - * @param instructions - A list of Instructions - * @returns Returns this transaction builder. - */ - prependInstructions(instructions: Instruction[]): TransactionBuilder { - this.instructions = instructions.concat(this.instructions); - return this; - } - - addSigner(signer: Signer): TransactionBuilder { - this.signers.push(signer); - return this; - } - - /** - * Checks whether this builder contains any instructions. - * @returns Whether this builder contains any instructions. - */ - isEmpty(): boolean { - return this.instructions.length == 0; - } - - /** - * Compresses all instructions & signers in this builder - * into one single instruction - * @param compressPost Compress all post instructions into the instructions field - * @returns Instruction object containing all - */ - compressIx(compressPost: boolean): Instruction { - let instructions: TransactionInstruction[] = []; - let cleanupInstructions: TransactionInstruction[] = []; - let signers: Signer[] = []; - this.instructions.forEach((curr) => { - instructions = instructions.concat(curr.instructions); - // Cleanup instructions should execute in reverse order - cleanupInstructions = curr.cleanupInstructions.concat(cleanupInstructions); - signers = signers.concat(curr.signers); - }); - - if (compressPost) { - instructions = instructions.concat(cleanupInstructions); - cleanupInstructions = []; - } - - return { - instructions: [...instructions], - cleanupInstructions: [...cleanupInstructions], - signers, - }; - } - - /** - * Returns the size of the current transaction in bytes. Measurement method can differ based on the maxSupportedTransactionVersion. - * @param userOptions - Options to override the default build options - * @returns the size of the current transaction in bytes. - * @throws error if there is an error measuring the transaction size. - * This can happen if the transaction is too large, or if the transaction contains too many keys to be serialized. - */ - txnSize(userOptions?: Partial): number { - const finalOptions: SyncBuildOptions = { - ...this.opts.defaultBuildOption, - ...userOptions, - latestBlockhash: MEASUREMENT_BLOCKHASH, - computeBudgetOption: this.opts.defaultBuildOption.computeBudgetOption ?? { type: "none" }, - }; - if (this.isEmpty()) { - return 0; - } - const request = this.buildSync(finalOptions); - const tx = request.transaction; - return isVersionedTransaction(tx) ? measureV0Tx(tx) : measureLegacyTx(tx); - } - - /** - * Constructs a transaction payload with the gathered instructions synchronously - * @param options - Options used to build the transaction - * @returns a TransactionPayload object that can be excuted or agregated into other transactions - */ - buildSync(options: SyncBuildOptions): TransactionPayload { - const { latestBlockhash, maxSupportedTransactionVersion, computeBudgetOption } = options; - - const ix = this.compressIx(true); - let prependInstructions: TransactionInstruction[] = []; - - if (computeBudgetOption.type === "fixed") { - const computeLimit = computeBudgetOption.computeBudgetLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT; - const microLamports = Math.floor( - (computeBudgetOption.priorityFeeLamports * MICROLAMPORTS_PER_LAMPORT) / computeLimit, - ); - - prependInstructions = [ - ComputeBudgetProgram.setComputeUnitLimit({ - units: computeLimit, - }), - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports, - }), - ]; - if (computeBudgetOption.jitoTipLamports && computeBudgetOption.jitoTipLamports > 0) { - prependInstructions.push( - SystemProgram.transfer({ - fromPubkey: this.wallet.publicKey, - toPubkey: getJitoTipAddress(), - lamports: computeBudgetOption.jitoTipLamports, - }), - ); - } - } - - if (computeBudgetOption.type === "auto") { - // Auto only works using `build` so when we encounter `auto` here we - // just use the use 0 priority budget and default compute budget. - // This should only be happening for calucling the tx size so it should be fine. - prependInstructions = [ - ComputeBudgetProgram.setComputeUnitLimit({ - units: DEFAULT_MAX_COMPUTE_UNIT_LIMIT, - }), - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 0, - }), - ]; - if (computeBudgetOption.jitoTipLamports && computeBudgetOption.jitoTipLamports > 0) { - prependInstructions.push( - SystemProgram.transfer({ - fromPubkey: this.wallet.publicKey, - toPubkey: getJitoTipAddress(), - lamports: computeBudgetOption.jitoTipLamports, - }), - ); - } - } - - const allSigners = ix.signers.concat(this.signers); - - const recentBlockhash = latestBlockhash; - - if (maxSupportedTransactionVersion === "legacy") { - const transaction = new Transaction({ - ...recentBlockhash, - feePayer: this.wallet.publicKey, - }); - if (prependInstructions.length > 0) { - transaction.add(...prependInstructions); - } - transaction.add(...ix.instructions); - transaction.feePayer = this.wallet.publicKey; - - return { - transaction: transaction, - signers: allSigners, - recentBlockhash, - }; - } - - const txnMsg = new TransactionMessage({ - recentBlockhash: recentBlockhash.blockhash, - payerKey: this.wallet.publicKey, - instructions: [...prependInstructions, ...ix.instructions], - }); - - const { lookupTableAccounts } = options; - - const msg = txnMsg.compileToV0Message(lookupTableAccounts); - const v0txn = new VersionedTransaction(msg); - - return { - transaction: v0txn, - signers: allSigners, - recentBlockhash, - }; - } - - /** - * Estimates the fee for this transaction - * @param getPriorityFeePerUnit - A function to get the priority fee per unit - * @param computeLimitMargin - The margin for the compute budget limit - * @param selectionPercentile - The percentile to use when calculating the priority fee - * @param lookupTableAccounts - The lookup table accounts that will be used in the transaction - * @returns An object containing the estimated values for consumed compute units, priority fee per unit in lamports, and the total priority fee in lamports - */ - async estimateFee( - getPriorityFeePerUnit?: ( - lockedWritableAccounts: PublicKey[], - ) => Promise, - computeLimitMargin?: number, - selectionPercentile?: number, - lookupTableAccounts?: AddressLookupTableAccount[], - ) { - const estConsumedComputeUnits = await estimateComputeBudgetLimit( - this.connection, - this.instructions, - lookupTableAccounts, - this.wallet.publicKey, - computeLimitMargin ?? 0.1, - ); - - const lockedWritableAccounts = getLockWritableAccounts(this.instructions); - - const estPriorityFeePerUnitInLamports = await (getPriorityFeePerUnit - ? getPriorityFeePerUnit(lockedWritableAccounts) - : this.connection.getRecentPrioritizationFees({ - lockedWritableAccounts, - })); - - const estPriorityFeeInLamports = await getPriorityFeeInLamports( - this.connection, - estConsumedComputeUnits, - lockedWritableAccounts, - selectionPercentile ?? DEFAULT_PRIORITY_FEE_PERCENTILE, - getPriorityFeePerUnit, - ); - - return { - estConsumedComputeUnits, - estPriorityFeePerUnitInLamports, - estPriorityFeeInLamports, - }; - } - - /** - * Constructs a transaction payload with the gathered instructions - * @param userOptions - Options to override the default build options - * @returns a TransactionPayload object that can be excuted or agregated into other transactions - */ - async build(userOptions?: Partial): Promise { - const finalOptions = { ...this.opts.defaultBuildOption, ...userOptions }; - const { latestBlockhash, blockhashCommitment, computeBudgetOption } = finalOptions; - let recentBlockhash = latestBlockhash; - if (!recentBlockhash) { - recentBlockhash = await this.connection.getLatestBlockhash(blockhashCommitment); - } - let finalComputeBudgetOption = computeBudgetOption ?? { type: "none" }; - if (finalComputeBudgetOption.type === "auto") { - const margin = finalComputeBudgetOption.computeLimitMargin ?? 0.1; - const lookupTableAccounts = - finalOptions.maxSupportedTransactionVersion === "legacy" - ? undefined - : finalOptions.lookupTableAccounts; - const computeBudgetLimit = await estimateComputeBudgetLimit( - this.connection, - this.instructions, - lookupTableAccounts, - this.wallet.publicKey, - margin, - ); - const percentile = - finalComputeBudgetOption.computePricePercentile ?? DEFAULT_PRIORITY_FEE_PERCENTILE; - const priorityFee = await getPriorityFeeInLamports( - this.connection, - computeBudgetLimit, - getLockWritableAccounts(this.instructions), - percentile, - finalComputeBudgetOption.getPriorityFeePerUnit, - ); - const maxPriorityFeeLamports = - finalComputeBudgetOption.maxPriorityFeeLamports ?? DEFAULT_MAX_PRIORITY_FEE_LAMPORTS; - const minPriorityFeeLamports = - finalComputeBudgetOption.minPriorityFeeLamports ?? DEFAULT_MIN_PRIORITY_FEE_LAMPORTS; - const priorityFeeLamports = Math.max( - Math.min(priorityFee, maxPriorityFeeLamports), - minPriorityFeeLamports, - ); - finalComputeBudgetOption = { - type: "fixed", - priorityFeeLamports, - computeBudgetLimit, - jitoTipLamports: finalComputeBudgetOption.jitoTipLamports, - }; - } - return this.buildSync({ - ...finalOptions, - latestBlockhash: recentBlockhash, - computeBudgetOption: finalComputeBudgetOption, - }); - } - - /** - * Constructs a transaction payload with the gathered instructions, sign it with the provider and send it out - * @param options - Options to build the transaction. . Overrides the default options provided in the constructor. - * @param sendOptions - Options to send the transaction. Overrides the default options provided in the constructor. - * @param confirmCommitment - Commitment level to wait for transaction confirmation. Overrides the default options provided in the constructor. - * @returns the txId of the transaction - */ - async buildAndExecute( - options?: Partial, - sendOptions?: Partial, - confirmCommitment?: Commitment, - ): Promise { - const sendOpts = { ...this.opts.defaultSendOption, ...sendOptions }; - const btx = await this.build(options); - const txn = btx.transaction; - const resolvedConfirmCommitment = confirmCommitment ?? this.opts.defaultConfirmationCommitment; - - let txId: string; - if (isVersionedTransaction(txn)) { - const signedTxn = await this.wallet.signTransaction(txn); - signedTxn.sign(btx.signers); - txId = await this.connection.sendTransaction(signedTxn, sendOpts); - } else { - const signedTxn = await this.wallet.signTransaction(txn); - btx.signers - .filter((s): s is Signer => s !== undefined) - .forEach((keypair) => signedTxn.partialSign(keypair)); - txId = await this.connection.sendRawTransaction(signedTxn.serialize(), sendOpts); - } - - const result = await this.connection.confirmTransaction( - { - signature: txId, - ...btx.recentBlockhash, - }, - resolvedConfirmCommitment, - ); - - const confirmTxErr = result.value.err; - if (confirmTxErr) { - throw new Error(confirmTxErr.toString()); - } - - return txId; - } -} - -/** - * Checks if a transaction is a versioned transaction. - * @param tx Transaction to check. - * @returns True if the transaction is a versioned transaction. - */ -export const isVersionedTransaction = ( - tx: Transaction | VersionedTransaction, -): tx is VersionedTransaction => { - return "version" in tx; -}; - -function measureLegacyTx(tx: Transaction): number { - // Due to the high cost of serialize, if the number of unique accounts clearly exceeds the limit of legacy transactions, - // serialize is not performed and a determination of infeasibility is made. - const uniqueKeys = new Set(); - for (const instruction of tx.instructions) { - for (const key of instruction.keys) { - uniqueKeys.add(key.pubkey.toBase58()); - } - uniqueKeys.add(instruction.programId.toBase58()); - } - if (uniqueKeys.size > LEGACY_TX_UNIQUE_KEYS_LIMIT) { - throw new Error("Unable to measure transaction size. Too many unique keys in transaction."); - } - - try { - // (Legacy)Transaction.serialize ensures that the size of successfully serialized data - // is less than or equal to PACKET_DATA_SIZE(1232). - // https://github.com/solana-labs/solana-web3.js/blob/77f78a8/packages/library-legacy/src/transaction/legacy.ts#L806 - const serialized = tx.serialize({ requireAllSignatures: false }); - return serialized.length; - } catch (e: unknown) { - throw new Error("Unable to measure transaction size. Unable to serialize transaction."); - } -} - -function measureV0Tx(tx: VersionedTransaction): number { - let serialized: Uint8Array; - try { - serialized = tx.serialize(); - } catch (e) { - throw new Error("Unable to measure transaction size. Unable to serialize transaction."); - } - - // VersionedTransaction.serialize does NOT ensures that the size of successfully serialized data is - // less than or equal to PACKET_DATA_SIZE(1232). - // https://github.com/solana-labs/solana-web3.js/blob/77f78a8/packages/library-legacy/src/transaction/versioned.ts#L65 - // - // BufferLayout.encode throws an error for writes that exceed the buffer size, - // so obviously large transactions will throws an error. - // However, depending on the size of the signature and message body, a size between 1233 - 2048 may be returned - // as a successful result, so we need to check it here. - if (serialized.length > PACKET_DATA_SIZE) { - throw new Error("Unable to measure transaction size. Transaction too large."); - } - - return serialized.length; -} - -const toBuffer = (arr: Buffer | Uint8Array | Array): Buffer => { - if (Buffer.isBuffer(arr)) { - return arr; - } else if (arr instanceof Uint8Array) { - return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength); - } else { - return Buffer.from(arr); - } -}; diff --git a/packages/common-sdk/src/web3/transactions/transactions-processor.ts b/packages/common-sdk/src/web3/transactions/transactions-processor.ts deleted file mode 100644 index 3942cf7..0000000 --- a/packages/common-sdk/src/web3/transactions/transactions-processor.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - Commitment, - Connection, - PublicKey, - Transaction, - VersionedTransaction, -} from "@solana/web3.js"; -import { Wallet } from "../wallet"; -import { isVersionedTransaction } from "./transactions-builder"; -import { SendTxRequest } from "./types"; - -/** - * @deprecated - */ -export class TransactionProcessor { - constructor( - readonly connection: Connection, - readonly wallet: Wallet, - readonly commitment: Commitment = "confirmed" - ) {} - - public async signTransaction(txRequest: SendTxRequest): Promise<{ - transaction: Transaction | VersionedTransaction; - lastValidBlockHeight: number; - blockhash: string; - }> { - const { transactions, lastValidBlockHeight, blockhash } = await this.signTransactions([ - txRequest, - ]); - return { transaction: transactions[0], lastValidBlockHeight, blockhash }; - } - - public async signTransactions(txRequests: SendTxRequest[]): Promise<{ - transactions: (Transaction | VersionedTransaction)[]; - lastValidBlockHeight: number; - blockhash: string; - }> { - const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash( - this.commitment - ); - const feePayer = this.wallet.publicKey; - const pSignedTxs = txRequests.map((txRequest) => { - return rewriteTransaction(txRequest, feePayer, blockhash); - }); - const transactions = await this.wallet.signAllTransactions(pSignedTxs); - return { - transactions, - lastValidBlockHeight, - blockhash, - }; - } - - public async sendTransaction( - transaction: Transaction | VersionedTransaction, - lastValidBlockHeight: number, - blockhash: string - ): Promise { - const execute = this.constructSendTransactions([transaction], lastValidBlockHeight, blockhash); - const txs = await execute(); - const ex = txs[0]; - if (ex.status === "fulfilled") { - return ex.value; - } else { - throw ex.reason; - } - } - - public constructSendTransactions( - transactions: (Transaction | VersionedTransaction)[], - lastValidBlockHeight: number, - blockhash: string, - parallel: boolean = true - ): () => Promise[]> { - const executeTx = async (tx: Transaction | VersionedTransaction) => { - const rawTxs = tx.serialize(); - return this.connection.sendRawTransaction(rawTxs, { - preflightCommitment: this.commitment, - }); - }; - - const confirmTx = async (txId: string) => { - const result = await this.connection.confirmTransaction( - { - signature: txId, - lastValidBlockHeight: lastValidBlockHeight, - blockhash, - }, - this.commitment - ); - - if (result.value.err) { - throw new Error(`Transaction failed: ${JSON.stringify(result.value)}`); - } - }; - - return async () => { - if (parallel) { - const results = transactions.map(async (tx) => { - const txId = await executeTx(tx); - await confirmTx(txId); - return txId; - }); - - return Promise.allSettled(results); - } else { - const results = []; - for (const tx of transactions) { - const txId = await executeTx(tx); - await confirmTx(txId); - results.push(txId); - } - return Promise.allSettled(results); - } - }; - } - - public async signAndConstructTransaction(txRequest: SendTxRequest): Promise<{ - signedTx: Transaction | VersionedTransaction; - execute: () => Promise; - }> { - const { transaction, lastValidBlockHeight, blockhash } = await this.signTransaction(txRequest); - return { - signedTx: transaction, - execute: async () => this.sendTransaction(transaction, lastValidBlockHeight, blockhash), - }; - } - - public async signAndConstructTransactions( - txRequests: SendTxRequest[], - parallel: boolean = true - ): Promise<{ - signedTxs: (Transaction | VersionedTransaction)[]; - execute: () => Promise[]>; - }> { - const { transactions, lastValidBlockHeight, blockhash } = await this.signTransactions( - txRequests - ); - const execute = this.constructSendTransactions( - transactions, - lastValidBlockHeight, - blockhash, - parallel - ); - return { signedTxs: transactions, execute }; - } -} - -function rewriteTransaction(txRequest: SendTxRequest, feePayer: PublicKey, blockhash: string) { - if (isVersionedTransaction(txRequest.transaction)) { - let tx: VersionedTransaction = txRequest.transaction; - if (txRequest.signers) { - tx.sign(txRequest.signers ?? []); - } - return tx; - } else { - let tx: Transaction = txRequest.transaction; - let signers = txRequest.signers ?? []; - - tx.feePayer = feePayer; - tx.recentBlockhash = blockhash; - - signers.forEach((kp) => { - tx.partialSign(kp); - }); - return tx; - } -} diff --git a/packages/common-sdk/src/web3/transactions/types.ts b/packages/common-sdk/src/web3/transactions/types.ts deleted file mode 100644 index fce00c3..0000000 --- a/packages/common-sdk/src/web3/transactions/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - BlockhashWithExpiryBlockHeight, - Signer, - Transaction, - TransactionInstruction, - VersionedTransaction, -} from "@solana/web3.js"; - -/** - * @category Transactions Util - */ -export const EMPTY_INSTRUCTION: Instruction = { - instructions: [], - cleanupInstructions: [], - signers: [], -}; - -/** - * @category Transactions Util - */ -export type Instruction = { - instructions: TransactionInstruction[]; - cleanupInstructions: TransactionInstruction[]; - signers: Signer[]; -}; - -/** - * @category Transactions Util - */ -export type TransactionPayload = { - transaction: Transaction | VersionedTransaction; - signers: Signer[]; - recentBlockhash: BlockhashWithExpiryBlockHeight; -}; - -/** - * @category Transactions Util - * @deprecated - */ -export type SendTxRequest = { - transaction: Transaction | VersionedTransaction; - signers?: Signer[]; -}; diff --git a/packages/common-sdk/src/web3/wallet.ts b/packages/common-sdk/src/web3/wallet.ts deleted file mode 100644 index 951a428..0000000 --- a/packages/common-sdk/src/web3/wallet.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js"; - -export interface Wallet { - signTransaction(tx: T): Promise; - signAllTransactions(txs: T[]): Promise; - publicKey: PublicKey; -} - -export class ReadOnlyWallet implements Wallet { - constructor(public publicKey: PublicKey = PublicKey.default) {} - - signTransaction(_transaction: T): Promise { - throw new Error("Read only wallet cannot sign transaction."); - } - signAllTransactions( - _transactions: T[] - ): Promise { - throw new Error("Read only wallet cannot sign transactions."); - } -} diff --git a/packages/common-sdk/tests/test-context.ts b/packages/common-sdk/tests/test-context.ts deleted file mode 100644 index 5d33119..0000000 --- a/packages/common-sdk/tests/test-context.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotent, createMint } from "@solana/spl-token"; -import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; -import TestWallet from "./utils/test-wallet"; -export const DEFAULT_RPC_ENDPOINT_URL = "http://localhost:8899"; - -export interface TestContext { - connection: Connection; - wallet: TestWallet; -} - -export function createTestContext(url: string = DEFAULT_RPC_ENDPOINT_URL): TestContext { - return { - connection: new Connection(url, "confirmed"), - wallet: new TestWallet(Keypair.generate()), - }; -} - -export async function requestAirdrop(ctx: TestContext, numSol: number = 1000): Promise { - const signature = await ctx.connection.requestAirdrop( - ctx.wallet.publicKey, - numSol * LAMPORTS_PER_SOL - ); - const latestBlockhash = await ctx.connection.getLatestBlockhash(); - await ctx.connection.confirmTransaction({ signature, ...latestBlockhash }); -} - -export function createNewMint(ctx: TestContext, tokenProgramId: PublicKey): Promise { - return createMint( - ctx.connection, - ctx.wallet.payer, - ctx.wallet.publicKey, - ctx.wallet.publicKey, - 6, - undefined, - undefined, - tokenProgramId, - ); -} - -export async function createAssociatedTokenAccount( - ctx: TestContext, - tokenProgramId: PublicKey, - mint?: PublicKey, -): Promise<{ ata: PublicKey; mint: PublicKey }> { - let tokenMint = mint || (await createNewMint(ctx, tokenProgramId)); - const ataKey = await createAssociatedTokenAccountIdempotent( - ctx.connection, - ctx.wallet.payer, - tokenMint, - ctx.wallet.publicKey, - undefined, - tokenProgramId, - ); - return { ata: ataKey, mint: tokenMint }; -} diff --git a/packages/common-sdk/tests/tsconfig.json b/packages/common-sdk/tests/tsconfig.json deleted file mode 100644 index 2339569..0000000 --- a/packages/common-sdk/tests/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../tsconfig-base.json", - "references": [ - { - "path": "../src" - } - ] -} diff --git a/packages/common-sdk/tests/utils/expectations.ts b/packages/common-sdk/tests/utils/expectations.ts deleted file mode 100644 index 1af7c6e..0000000 --- a/packages/common-sdk/tests/utils/expectations.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Mint } from "@solana/spl-token"; - -export function expectMintEquals(actual: Mint, expected: Mint) { - expect(actual.decimals).toEqual(expected.decimals); - expect(actual.isInitialized).toEqual(expected.isInitialized); - expect(actual.mintAuthority!.equals(expected.mintAuthority!)).toBeTruthy(); - expect(actual.freezeAuthority!.equals(expected.freezeAuthority!)).toBeTruthy(); - expect(actual.supply === expected.supply).toBeTruthy(); -} diff --git a/packages/common-sdk/tests/utils/test-wallet.ts b/packages/common-sdk/tests/utils/test-wallet.ts deleted file mode 100644 index 2e79533..0000000 --- a/packages/common-sdk/tests/utils/test-wallet.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Keypair, PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js"; -import { isVersionedTransaction, Wallet } from "../../src/web3"; - -// Ported from @coral-xyz/anchor for testing purposes. -export default class TestWallet implements Wallet { - constructor(readonly payer: Keypair) {} - - async signTransaction(tx: T): Promise { - if (isVersionedTransaction(tx)) { - tx.sign([this.payer]); - } else { - tx.partialSign(this.payer); - } - - return tx; - } - - async signAllTransactions(txs: T[]): Promise { - return txs.map((t) => { - if (isVersionedTransaction(t)) { - t.sign([this.payer]); - } else { - t.partialSign(this.payer); - } - return t; - }); - } - - get publicKey(): PublicKey { - return this.payer.publicKey; - } -} diff --git a/packages/common-sdk/tests/web3/ata-util.test.ts b/packages/common-sdk/tests/web3/ata-util.test.ts deleted file mode 100644 index 125771f..0000000 --- a/packages/common-sdk/tests/web3/ata-util.test.ts +++ /dev/null @@ -1,455 +0,0 @@ -import { - ASSOCIATED_TOKEN_PROGRAM_ID, - AccountLayout, - NATIVE_MINT, - TOKEN_2022_PROGRAM_ID, - TOKEN_PROGRAM_ID, - createAssociatedTokenAccount, - getAccount, - getAssociatedTokenAddressSync, - setAuthority, -} from "@solana/spl-token"; -import { Keypair, LAMPORTS_PER_SOL, SystemProgram } from "@solana/web3.js"; -import { BN } from "bn.js"; -import { ZERO } from "../../src/math"; -import { resolveOrCreateATA, resolveOrCreateATAs } from "../../src/web3/ata-util"; -import { TransactionBuilder } from "../../src/web3/transactions"; -import { createNewMint, createTestContext, requestAirdrop } from "../test-context"; - -jest.setTimeout(100 * 1000 /* ms */); - -describe("ata-util", () => { - const ctx = createTestContext(); - const { connection, wallet } = ctx; - - beforeAll(async () => { - await requestAirdrop(ctx); - }); - - const tokenPrograms = [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]; - tokenPrograms.forEach((tokenProgram) => describe(`TokenProgram: ${tokenProgram.toBase58()}`, () => { - it("resolveOrCreateATA, wrapped sol", async () => { - const { connection, wallet } = ctx; - - // verify address & instruction - const notExpected = getAssociatedTokenAddressSync(wallet.publicKey, NATIVE_MINT); - const resolved = await resolveOrCreateATA( - connection, - wallet.publicKey, - NATIVE_MINT, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - new BN(LAMPORTS_PER_SOL), - wallet.publicKey, - false - ); - expect(resolved.address.equals(notExpected)).toBeFalsy(); // non-ATA address - expect(resolved.instructions.length).toEqual(2); - expect(resolved.instructions[0].programId.equals(SystemProgram.programId)).toBeTruthy(); - expect(resolved.instructions[1].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.cleanupInstructions.length).toEqual(1); - expect(resolved.cleanupInstructions[0].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); - }); - - it("resolveOrCreateATA, not exist, modeIdempotent = false", async () => { - const mint = await createNewMint(ctx, tokenProgram); - - // verify address & instruction - const expected = getAssociatedTokenAddressSync(mint, wallet.publicKey, undefined, tokenProgram); - const resolved = await resolveOrCreateATA( - connection, - wallet.publicKey, - mint, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - ZERO, - wallet.publicKey, - false - ); - expect(resolved.address.equals(expected)).toBeTruthy(); - expect(resolved.instructions.length).toEqual(1); - expect(resolved.instructions[0].data.length).toEqual(0); // no instruction data - - // verify transaction - const preAccountData = await connection.getAccountInfo(resolved.address); - expect(preAccountData).toBeNull(); - - const builder = new TransactionBuilder(connection, wallet); - builder.addInstruction(resolved); - await builder.buildAndExecute(); - - const postAccountData = await connection.getAccountInfo(resolved.address); - expect(postAccountData?.owner.equals(tokenProgram)).toBeTruthy(); - }); - - it("resolveOrCreateATA, exist, modeIdempotent = false", async () => { - const mint = await createNewMint(ctx, tokenProgram); - - const expected = await createAssociatedTokenAccount( - ctx.connection, - wallet.payer, - mint, - wallet.publicKey, - undefined, - tokenProgram, - ); - const preAccountData = await connection.getAccountInfo(expected); - expect(preAccountData).not.toBeNull(); - - // verify address & instruction - const resolved = await resolveOrCreateATA( - connection, - wallet.publicKey, - mint, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - ZERO, - wallet.publicKey, - false - ); - expect(resolved.address.equals(expected)).toBeTruthy(); - expect(resolved.instructions.length).toEqual(0); - }); - - it("resolveOrCreateATA, created before execution, modeIdempotent = false", async () => { - const mint = await createNewMint(ctx, tokenProgram); - - const expected = getAssociatedTokenAddressSync(mint, wallet.publicKey, undefined, tokenProgram); - const resolved = await resolveOrCreateATA( - connection, - wallet.publicKey, - mint, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - ZERO, - wallet.publicKey, - false - ); - expect(resolved.address.equals(expected)).toBeTruthy(); - expect(resolved.instructions.length).toEqual(1); - expect(resolved.instructions[0].data.length).toEqual(0); // no instruction data - - // created before execution - await createAssociatedTokenAccount(connection, wallet.payer, mint, wallet.publicKey, undefined, tokenProgram); - const accountData = await connection.getAccountInfo(expected); - expect(accountData).not.toBeNull(); - - // Tx should be fail - const builder = new TransactionBuilder(connection, wallet); - builder.addInstruction(resolved); - await expect(builder.buildAndExecute()).rejects.toThrow(); - }); - - it("resolveOrCreateATA, created before execution, modeIdempotent = true", async () => { - const mint = await createNewMint(ctx, tokenProgram); - - const expected = getAssociatedTokenAddressSync(mint, wallet.publicKey, undefined, tokenProgram); - const resolved = await resolveOrCreateATA( - connection, - wallet.publicKey, - mint, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - ZERO, - wallet.publicKey, - true - ); - expect(resolved.address.equals(expected)).toBeTruthy(); - expect(resolved.instructions.length).toEqual(1); - expect(resolved.instructions[0].data[0]).toEqual(1); // 1 byte data - - // created before execution - await createAssociatedTokenAccount(connection, wallet.payer, mint, wallet.publicKey, undefined, tokenProgram); - const accountData = await connection.getAccountInfo(expected); - expect(accountData).not.toBeNull(); - - // Tx should be success even if ATA has been created - const builder = new TransactionBuilder(connection, wallet); - builder.addInstruction(resolved); - await expect(builder.buildAndExecute()).resolves.toBeTruthy(); - }); - - it("resolveOrCreateATAs, created before execution, modeIdempotent = false", async () => { - const mints = await Promise.all([createNewMint(ctx, tokenProgram), createNewMint(ctx, tokenProgram), createNewMint(ctx, tokenProgram)]); - - // create first ATA - await createAssociatedTokenAccount(connection, wallet.payer, mints[0], wallet.publicKey, undefined, tokenProgram); - - const expected = mints.map((mint) => getAssociatedTokenAddressSync(mint, wallet.publicKey, undefined, tokenProgram)); - const resolved = await resolveOrCreateATAs( - connection, - wallet.publicKey, - mints.map((mint) => ({ tokenMint: mint, wrappedSolAmountIn: ZERO })), - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - wallet.publicKey, - false - ); - expect(resolved[0].address.equals(expected[0])).toBeTruthy(); - expect(resolved[1].address.equals(expected[1])).toBeTruthy(); - expect(resolved[2].address.equals(expected[2])).toBeTruthy(); - expect(resolved[0].instructions.length).toEqual(0); // already exists - expect(resolved[1].instructions.length).toEqual(1); - expect(resolved[2].instructions.length).toEqual(1); - expect(resolved[1].instructions[0].data.length).toEqual(0); // no instruction data - expect(resolved[2].instructions[0].data.length).toEqual(0); // no instruction data - - // create second ATA before execution - await createAssociatedTokenAccount(connection, wallet.payer, mints[1], wallet.publicKey, undefined, tokenProgram); - - const preAccountData = await connection.getMultipleAccountsInfo(expected); - expect(preAccountData[0]).not.toBeNull(); - expect(preAccountData[1]).not.toBeNull(); - expect(preAccountData[2]).toBeNull(); - - // Tx should be fail - const builder = new TransactionBuilder(connection, wallet); - builder.addInstructions(resolved); - await expect(builder.buildAndExecute()).rejects.toThrow(); - }); - - it("resolveOrCreateATAs, created before execution, modeIdempotent = true", async () => { - const mints = await Promise.all([createNewMint(ctx, tokenProgram), createNewMint(ctx, tokenProgram), createNewMint(ctx, tokenProgram)]); - - // create first ATA - await createAssociatedTokenAccount(connection, wallet.payer, mints[0], wallet.publicKey, undefined, tokenProgram); - - const expected = mints.map((mint) => getAssociatedTokenAddressSync(mint, wallet.publicKey, undefined, tokenProgram)); - - const resolved = await resolveOrCreateATAs( - connection, - wallet.publicKey, - mints.map((mint) => ({ tokenMint: mint, wrappedSolAmountIn: ZERO })), - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - wallet.publicKey, - true - ); - expect(resolved[0].address.equals(expected[0])).toBeTruthy(); - expect(resolved[1].address.equals(expected[1])).toBeTruthy(); - expect(resolved[2].address.equals(expected[2])).toBeTruthy(); - expect(resolved[0].instructions.length).toEqual(0); // already exists - expect(resolved[1].instructions.length).toEqual(1); - expect(resolved[2].instructions.length).toEqual(1); - expect(resolved[1].instructions[0].data[0]).toEqual(1); // 1 byte data - expect(resolved[2].instructions[0].data[0]).toEqual(1); // 1 byte data - - // create second ATA before execution - await createAssociatedTokenAccount(connection, wallet.payer, mints[1], wallet.publicKey, undefined, tokenProgram); - - const preAccountData = await connection.getMultipleAccountsInfo(expected); - expect(preAccountData[0]).not.toBeNull(); - expect(preAccountData[1]).not.toBeNull(); - expect(preAccountData[2]).toBeNull(); - - // Tx should be success even if second ATA has been created - const builder = new TransactionBuilder(connection, wallet); - builder.addInstructions(resolved); - await expect(builder.buildAndExecute()).resolves.toBeTruthy(); - - const postAccountData = await connection.getMultipleAccountsInfo(expected); - expect(postAccountData[0]).not.toBeNull(); - expect(postAccountData[1]).not.toBeNull(); - expect(postAccountData[2]).not.toBeNull(); - }); - - it("resolveOrCreateATA, owner changed ATA detected", async () => { - // in Token-2022, owner of ATA cannot be changed - if (tokenProgram.equals(TOKEN_2022_PROGRAM_ID)) return; - - const anotherWallet = Keypair.generate(); - const mint = await createNewMint(ctx, tokenProgram); - - const ata = await createAssociatedTokenAccount( - connection, - wallet.payer, - mint, - wallet.publicKey, - undefined, - tokenProgram - ); - - // should be ok - const preOwnerChanged = await resolveOrCreateATA(connection, wallet.publicKey, mint, () => - connection.getMinimumBalanceForRentExemption(AccountLayout.span) - ); - expect(preOwnerChanged.address.equals(ata)).toBeTruthy(); - - // owner change - await setAuthority( - connection, - ctx.wallet.payer, - ata, - wallet.publicKey, - 2, - anotherWallet.publicKey, - [] - ); - - // verify that owner have been changed - const changed = await getAccount(connection, ata); - expect(changed.owner.equals(anotherWallet.publicKey)).toBeTruthy(); - - // should be failed - const postOwnerChangedPromise = resolveOrCreateATA(connection, wallet.publicKey, mint, () => - connection.getMinimumBalanceForRentExemption(AccountLayout.span) - ); - await expect(postOwnerChangedPromise).rejects.toThrow(/ATA with change of ownership detected/); - }); - - it("resolveOrCreateATA, allowPDAOwnerAddress = false", async () => { - const mint = await createNewMint(ctx, tokenProgram); - - const pda = getAssociatedTokenAddressSync(mint, wallet.publicKey, undefined, tokenProgram); // ATA is one of PDAs - const allowPDAOwnerAddress = false; - - try { - await resolveOrCreateATA( - connection, - pda, - mint, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - ZERO, - wallet.publicKey, - false, - allowPDAOwnerAddress - ); - - fail("should be failed"); - } catch (e: any) { - expect(e.name).toMatch("TokenOwnerOffCurveError"); - } - }); - - it("resolveOrCreateATA, allowPDAOwnerAddress = true", async () => { - const mint = await createNewMint(ctx, tokenProgram); - - const pda = getAssociatedTokenAddressSync(mint, wallet.publicKey, undefined, tokenProgram); // ATA is one of PDAs - const allowPDAOwnerAddress = true; - - try { - await resolveOrCreateATA( - connection, - pda, - mint, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - ZERO, - wallet.publicKey, - false, - allowPDAOwnerAddress - ); - } catch (e: any) { - fail("should be failed"); - } - }); - - it("resolveOrCreateATA, wrappedSolAccountCreateMethod = ata", async () => { - const { connection, wallet } = ctx; - - const wrappedSolAccountCreateMethod = "ata"; - - const resolved = await resolveOrCreateATA( - connection, - wallet.publicKey, - NATIVE_MINT, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - new BN(LAMPORTS_PER_SOL), - wallet.publicKey, - false, - false, - wrappedSolAccountCreateMethod, - ); - - expect(resolved.instructions.length).toEqual(3); - expect(resolved.instructions[0].programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.instructions[1].programId.equals(SystemProgram.programId)).toBeTruthy(); - expect(resolved.instructions[2].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.cleanupInstructions.length).toEqual(1); - expect(resolved.cleanupInstructions[0].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.signers.length).toEqual(0); - - const builder = new TransactionBuilder(connection, wallet); - builder.addInstruction(resolved); - await expect(builder.buildAndExecute()).resolves.toBeTruthy(); - }); - - it("resolveOrCreateATA, wrappedSolAccountCreateMethod = ata, amount = 0", async () => { - const { connection, wallet } = ctx; - - const wrappedSolAccountCreateMethod = "ata"; - - const resolved = await resolveOrCreateATA( - connection, - wallet.publicKey, - NATIVE_MINT, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - ZERO, - wallet.publicKey, - false, - false, - wrappedSolAccountCreateMethod, - ); - - expect(resolved.instructions.length).toEqual(1); - expect(resolved.instructions[0].programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.cleanupInstructions.length).toEqual(1); - expect(resolved.cleanupInstructions[0].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.signers.length).toEqual(0); - - const builder = new TransactionBuilder(connection, wallet); - builder.addInstruction(resolved); - await expect(builder.buildAndExecute()).resolves.toBeTruthy(); - }); - - it("resolveOrCreateATA, wrappedSolAccountCreateMethod = keypair", async () => { - const { connection, wallet } = ctx; - - const wrappedSolAccountCreateMethod = "keypair"; - - const resolved = await resolveOrCreateATA( - connection, - wallet.publicKey, - NATIVE_MINT, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - new BN(LAMPORTS_PER_SOL), - wallet.publicKey, - false, - false, - wrappedSolAccountCreateMethod, - ); - - expect(resolved.instructions.length).toEqual(2); - expect(resolved.instructions[0].programId.equals(SystemProgram.programId)).toBeTruthy(); - expect(resolved.instructions[1].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.cleanupInstructions.length).toEqual(1); - expect(resolved.cleanupInstructions[0].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.signers.length).toEqual(1); - - const builder = new TransactionBuilder(connection, wallet); - builder.addInstruction(resolved); - await expect(builder.buildAndExecute()).resolves.toBeTruthy(); - }); - - it("resolveOrCreateATA, wrappedSolAccountCreateMethod = withSeed", async () => { - const { connection, wallet } = ctx; - - const wrappedSolAccountCreateMethod = "withSeed"; - - const resolved = await resolveOrCreateATA( - connection, - wallet.publicKey, - NATIVE_MINT, - () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), - new BN(LAMPORTS_PER_SOL), - wallet.publicKey, - false, - false, - wrappedSolAccountCreateMethod, - ); - - expect(resolved.instructions.length).toEqual(2); - expect(resolved.instructions[0].programId.equals(SystemProgram.programId)).toBeTruthy(); - expect(resolved.instructions[1].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.cleanupInstructions.length).toEqual(1); - expect(resolved.cleanupInstructions[0].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); - expect(resolved.signers.length).toEqual(0); - - const builder = new TransactionBuilder(connection, wallet); - builder.addInstruction(resolved); - await expect(builder.buildAndExecute()).resolves.toBeTruthy(); - }); - })); -}); diff --git a/packages/common-sdk/tests/web3/network/account-requests.test.ts b/packages/common-sdk/tests/web3/network/account-requests.test.ts deleted file mode 100644 index 315531e..0000000 --- a/packages/common-sdk/tests/web3/network/account-requests.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { getMint, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; -import { Keypair } from "@solana/web3.js"; -import { getMultipleParsedAccounts, getParsedAccount, ParsableMintInfo } from "../../../src/web3"; -import { - createAssociatedTokenAccount, - createNewMint, - createTestContext, - requestAirdrop, -} from "../../test-context"; -import { expectMintEquals } from "../../utils/expectations"; - -jest.setTimeout(100 * 1000 /* ms */); - -describe("account-requests", () => { - const ctx = createTestContext(); - // Silence the errors when we evaluate invalid token accounts. - beforeEach(() => { - jest.spyOn(console, "error").mockImplementation(() => {}); - }); - - beforeAll(async () => { - await requestAirdrop(ctx); - }); - - const tokenPrograms = [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]; - tokenPrograms.forEach((tokenProgram) => describe(`TokenProgram: ${tokenProgram.toBase58()}`, () => { - it("getParsedAccount, ok", async () => { - const mint = await createNewMint(ctx, tokenProgram); - const expected = { ...(await getMint(ctx.connection, mint, undefined, tokenProgram)), tokenProgram }; - - const mintInfo = await getParsedAccount(ctx.connection, mint, ParsableMintInfo); - expectMintEquals(mintInfo!, expected); - }); - - it("getMultipleParsedAccounts, some null", async () => { - const mint = await createNewMint(ctx, tokenProgram); - const missing = Keypair.generate().publicKey; - const mintInfos = await getMultipleParsedAccounts( - ctx.connection, - [mint, missing], - ParsableMintInfo - ); - - const expected = { ...(await getMint(ctx.connection, mint, undefined, tokenProgram)), tokenProgram }; - - expectMintEquals(mintInfos[0]!, expected); - expect(mintInfos[1]).toBeNull(); - }); - - it("getMultipleParsedAccounts, invalid type returns null", async () => { - const mint = await createNewMint(ctx, tokenProgram); - const { ata } = await createAssociatedTokenAccount(ctx, tokenProgram, mint); - const mintInfos = await getMultipleParsedAccounts( - ctx.connection, - [mint, ata], - ParsableMintInfo - ); - const expected = { ...(await getMint(ctx.connection, mint, undefined, tokenProgram)), tokenProgram }; - expectMintEquals(mintInfos[0]!, expected); - expect(mintInfos[1]).toBeNull(); - }); - - it("getMultipleParsedAccounts, separate chunks", async () => { - const mints = await Promise.all( - Array.from({ length: 10 }, async () => await createNewMint(ctx, tokenProgram)) - ); - const mintInfos = await getMultipleParsedAccounts(ctx.connection, mints, ParsableMintInfo, 2); - - // Verify all mints are fetched and are in order - expect(mintInfos.length === mints.length); - mints.forEach((mint, i) => { - expect(mintInfos[i]!.address.equals(mint)); - }); - }); - })); -}); diff --git a/packages/common-sdk/tests/web3/network/parsing.test.ts b/packages/common-sdk/tests/web3/network/parsing.test.ts deleted file mode 100644 index 01860d3..0000000 --- a/packages/common-sdk/tests/web3/network/parsing.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; -import { ParsableMintInfo, ParsableTokenAccountInfo } from "../../../src/web3"; -import { - createAssociatedTokenAccount, - createNewMint, - createTestContext, - requestAirdrop, -} from "../../test-context"; - -jest.setTimeout(100 * 1000 /* ms */); - -describe("parsing", () => { - const ctx = createTestContext(); - - beforeAll(async () => { - await requestAirdrop(ctx); - }); - - const tokenPrograms = [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]; - tokenPrograms.forEach((tokenProgram) => describe(`TokenProgram: ${tokenProgram.toBase58()}`, () => { - it("ParsableMintInfo", async () => { - const mint = await createNewMint(ctx, tokenProgram); - const account = await ctx.connection.getAccountInfo(mint); - const parsed = ParsableMintInfo.parse(mint, account); - - expect(parsed).toBeDefined(); - if (!parsed) { - throw new Error("parsed is undefined"); - } - const parsedData = parsed; - expect(parsedData.isInitialized).toEqual(true); - expect(parsedData.decimals).toEqual(6); - expect(parsedData.tokenProgram.equals(TOKEN_PROGRAM_ID)); - }); - - it("ParsableTokenAccountInfo", async () => { - const { ata, mint } = await createAssociatedTokenAccount(ctx, tokenProgram); - const account = await ctx.connection.getAccountInfo(ata); - const parsed = ParsableTokenAccountInfo.parse(ata, account); - - expect(parsed).toBeDefined(); - if (!parsed) { - throw new Error("parsed is undefined"); - } - const parsedData = parsed; - expect(parsedData.mint.equals(mint)).toBeTruthy(); - expect(parsedData.tokenProgram.equals(TOKEN_PROGRAM_ID)); - expect(parsedData.isInitialized).toEqual(true); - expect(parsedData.amount === 0n).toBeTruthy(); - }); - })); -}); diff --git a/packages/common-sdk/tests/web3/network/simple-fetcher-impl.test.ts b/packages/common-sdk/tests/web3/network/simple-fetcher-impl.test.ts deleted file mode 100644 index 5fe0c9c..0000000 --- a/packages/common-sdk/tests/web3/network/simple-fetcher-impl.test.ts +++ /dev/null @@ -1,627 +0,0 @@ -import { Mint, TOKEN_PROGRAM_ID, getMint } from "@solana/spl-token"; -import { PublicKey } from "@solana/web3.js"; -import { - BasicSupportedTypes, - ParsableEntity, - ParsableMintInfo, - ParsableTokenAccountInfo, - SimpleAccountFetcher, -} from "../../../src/web3"; -import { TestContext, createNewMint, createTestContext, requestAirdrop } from "../../test-context"; -import { expectMintEquals } from "../../utils/expectations"; - -jest.setTimeout(100 * 1000 /* ms */); - -describe("simple-account-fetcher", () => { - let ctx: TestContext = createTestContext(); - const retentionPolicy = new Map, number>([ - [ParsableMintInfo, 1000], - [ParsableTokenAccountInfo, 1000], - ]); - const testMints: PublicKey[] = []; - - beforeAll(async () => { - await requestAirdrop(ctx); - for (let i = 0; i < 10; i++) { - testMints.push(await createNewMint(ctx, TOKEN_PROGRAM_ID)); - } - }); - - beforeEach(() => { - ctx = createTestContext(); - jest.spyOn(console, "error").mockImplementation(() => {}); - }); - - afterEach(() => { - // jest.resetAllMocks doesn't work (I guess that jest.spyOn rewrite prototype of Connection) - jest.restoreAllMocks(); - }); - - describe("getAccount", () => { - it("fetch brand new account equals on-chain", async () => { - const mintKey = testMints[0]; - - const expected = await getMint(ctx.connection, mintKey); - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const spy = jest.spyOn(ctx.connection, "getAccountInfo"); - const cached = await fetcher.getAccount(mintKey, ParsableMintInfo); - - expect(spy).toBeCalledTimes(1); - expect(cached).toBeDefined(); - expectMintEquals(cached!, expected); - }); - - it("returns cached value within retention window", async () => { - const mintKey = testMints[0]; - const expected = await getMint(ctx.connection, mintKey); - const now = Date.now(); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const spy = jest.spyOn(ctx.connection, "getAccountInfo"); - await fetcher.getAccount(mintKey, ParsableMintInfo, undefined, now); - const cached = await fetcher.getAccount( - mintKey, - ParsableMintInfo, - undefined, - now + retention - ); - - expect(spy).toBeCalledTimes(1); - expect(cached).toBeDefined(); - expectMintEquals(cached!, expected); - }); - - it("fetch new value when call is outside of retention window", async () => { - const mintKey = testMints[0]; - const expected = await getMint(ctx.connection, mintKey); - const now = 32523523523; - const retention = retentionPolicy.get(ParsableMintInfo)!; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const spy = jest.spyOn(ctx.connection, "getAccountInfo"); - await fetcher.getAccount(mintKey, ParsableMintInfo, undefined, now); - const cached = await fetcher.getAccount( - mintKey, - ParsableMintInfo, - undefined, - now + retention + 1 - ); - - expect(spy).toBeCalledTimes(2); - expect(cached).toBeDefined(); - expectMintEquals(cached!, expected); - }); - - it("getAccount - return cache value when call does not exceed custom ttl", async () => { - const mintKey = testMints[0]; - const expected = await getMint(ctx.connection, mintKey); - const now = 32523523523; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const ttl = 50; - const spy = jest.spyOn(ctx.connection, "getAccountInfo"); - await fetcher.getAccount(mintKey, ParsableMintInfo, { maxAge: ttl }, now); - const cached = await fetcher.getAccount( - mintKey, - ParsableMintInfo, - { maxAge: ttl }, - now + ttl - ); - - expect(spy).toBeCalledTimes(1); - expect(cached).toBeDefined(); - expectMintEquals(cached!, expected); - }); - - it("fetch new value when call exceed custom ttl", async () => { - const mintKey = testMints[0]; - const expected = await getMint(ctx.connection, mintKey); - const now = 32523523523; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const ttl = 50; - const spy = jest.spyOn(ctx.connection, "getAccountInfo"); - await fetcher.getAccount(mintKey, ParsableMintInfo, { maxAge: ttl }, now); - const cached = await fetcher.getAccount( - mintKey, - ParsableMintInfo, - { maxAge: ttl }, - now + ttl + 1 - ); - - expect(spy).toBeCalledTimes(2); - expect(cached).toBeDefined(); - expectMintEquals(cached!, expected); - }); - - it("fetch new value when call ttl === 0", async () => { - const mintKey = testMints[0]; - const expected = await getMint(ctx.connection, mintKey); - const now = 32523523523; - - const fetcher = new SimpleAccountFetcher(ctx.connection, new Map()); - - const spy = jest.spyOn(ctx.connection, "getAccountInfo"); - await fetcher.getAccount(mintKey, ParsableMintInfo, { maxAge: 0 }, now); - const cached = await fetcher.getAccount(mintKey, ParsableMintInfo, { maxAge: 0 }, now + 1); - - expect(spy).toBeCalledTimes(2); - expect(cached).toBeDefined(); - expectMintEquals(cached!, expected); - }); - - it("fetch new value when call retention === 0", async () => { - const mintKey = testMints[0]; - const expected = await getMint(ctx.connection, mintKey); - const now = 32523523523; - const retentionPolicy = new Map, number>([ - [ParsableMintInfo, 0], - ]); - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const spy = jest.spyOn(ctx.connection, "getAccountInfo"); - await fetcher.getAccount(mintKey, ParsableMintInfo, undefined, now); - const cached = await fetcher.getAccount(mintKey, ParsableMintInfo, undefined, now + 1); - - expect(spy).toBeCalledTimes(2); - expect(cached).toBeDefined(); - expectMintEquals(cached!, expected); - }); - - it("fetching invalid account returns null", async () => { - const mintKey = PublicKey.default; - const now = 32523523523; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const cached = await fetcher.getAccount(mintKey, ParsableMintInfo, undefined, now); - - expect(cached).toBeNull(); - }); - - it("fetching valid account but invalid account type returns null", async () => { - const mintKey = testMints[0]; - const now = 32523523523; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const cached = await fetcher.getAccount(mintKey, ParsableTokenAccountInfo, undefined, now); - - expect(cached).toBeNull(); - }); - - it("fetching null-cached accounts will respect ttl", async () => { - const mintKey = testMints[0]; - const now = 32523523523; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const spy = jest.spyOn(ctx.connection, "getAccountInfo"); - await fetcher.getAccount(mintKey, ParsableTokenAccountInfo, undefined, now); - const cached = await fetcher.getAccount( - mintKey, - ParsableTokenAccountInfo, - undefined, - now + 5 - ); - - expect(spy).toBeCalledTimes(1); - expect(cached).toBeNull(); - }); - }); - - describe("getAccounts", () => { - let expectedMintInfos: Mint[] = []; - - beforeAll(async () => { - for (const mint of testMints) { - expectedMintInfos.push(await getMint(ctx.connection, mint)); - } - }); - - it("nothing cached, fetching all values", async () => { - const mintKeys = testMints; - const now = 32523523523; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const resultMap = await fetcher.getAccounts(mintKeys, ParsableMintInfo, undefined, now); - - expect(spy).toBeCalledTimes(1); - - Array.from(resultMap.values()).forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("all are cached, fetching all values will not call for update", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts(mintKeys, ParsableMintInfo, undefined, now); - const resultMap = await fetcher.getAccounts( - mintKeys, - ParsableMintInfo, - undefined, - now + retention - ); - expect(spy).toBeCalledTimes(1); - Array.from(resultMap.values()).forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("all are cached but expired, fetching all values will call for update", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts(mintKeys, ParsableMintInfo, undefined, now); - const resultMap = await fetcher.getAccounts( - mintKeys, - ParsableMintInfo, - undefined, - now + retention + 1 - ); - expect(spy).toBeCalledTimes(2); - Array.from(resultMap.values()).forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("some are cached, fetching all values will call for update", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts([testMints[0], testMints[1]], ParsableMintInfo, undefined, now); - const resultMap = await fetcher.getAccounts( - mintKeys, - ParsableMintInfo, - undefined, - now + retention - ); - expect(spy).toBeCalledTimes(2); - Array.from(resultMap.values()).forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("some are cached, some expired, fetching all values will call for update", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts([testMints[0], testMints[1]], ParsableMintInfo, undefined, now); - await fetcher.getAccounts([testMints[2], testMints[3]], ParsableMintInfo, undefined, now + 5); - const resultMap = await fetcher.getAccounts( - mintKeys, - ParsableMintInfo, - undefined, - now + retention + 1 - ); - expect(spy).toBeCalledTimes(3); - Array.from(resultMap.values()).forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("some are cached, some expired, some invalid", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts([testMints[0], testMints[1]], ParsableMintInfo, undefined, now); - await fetcher.getAccounts( - [testMints[2], testMints[3], PublicKey.default], - ParsableMintInfo, - undefined, - now + 5 - ); - const resultMap = await fetcher.getAccounts( - [...mintKeys, PublicKey.default], - ParsableMintInfo, - undefined, - now + retention + 1 - ); - expect(spy).toBeCalledTimes(3); - Array.from(resultMap.values()).forEach((value, index) => { - if (index <= mintKeys.length - 1) { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - } else { - // Expect the last value, which is invalid, to be null - expect(value).toBeNull(); - } - }); - }); - }); - - describe("getAccountsAsArray", () => { - let expectedMintInfos: Mint[] = []; - - beforeAll(async () => { - for (const mint of testMints) { - expectedMintInfos.push(await getMint(ctx.connection, mint)); - } - }); - - it("nothing cached, fetching all values", async () => { - const mintKeys = testMints; - const now = 32523523523; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const resultArray = await fetcher.getAccountsAsArray( - mintKeys, - ParsableMintInfo, - undefined, - now - ); - - expect(spy).toBeCalledTimes(1); - - resultArray.forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("duplicated values are shown", async () => { - const mintKeys = [...testMints, ...testMints]; - const expected = [...expectedMintInfos, ...expectedMintInfos]; - const now = 32523523523; - - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const resultArray = await fetcher.getAccountsAsArray( - mintKeys, - ParsableMintInfo, - undefined, - now - ); - - expect(spy).toBeCalledTimes(1); - - resultArray.forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expected[index]); - }); - }); - - it("all are cached, fetching all values will not call for update", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts(mintKeys, ParsableMintInfo, undefined, now); - const result = await fetcher.getAccountsAsArray( - mintKeys, - ParsableMintInfo, - undefined, - now + retention - ); - expect(spy).toBeCalledTimes(1); - result.forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("all are cached but expired, fetching all values will call for update", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts(mintKeys, ParsableMintInfo, undefined, now); - const result = await fetcher.getAccountsAsArray( - mintKeys, - ParsableMintInfo, - undefined, - now + retention + 1 - ); - expect(spy).toBeCalledTimes(2); - result.forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("some are cached, fetching all values will call for update", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts([testMints[0], testMints[1]], ParsableMintInfo, undefined, now); - const result = await fetcher.getAccountsAsArray( - mintKeys, - ParsableMintInfo, - undefined, - now + retention - ); - expect(spy).toBeCalledTimes(2); - result.forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("some are cached, some expired, fetching all values will call for update", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts([testMints[0], testMints[1]], ParsableMintInfo, undefined, now); - await fetcher.getAccounts([testMints[2], testMints[3]], ParsableMintInfo, undefined, now + 5); - const result = await fetcher.getAccountsAsArray( - mintKeys, - ParsableMintInfo, - undefined, - now + retention + 1 - ); - expect(spy).toBeCalledTimes(3); - result.forEach((value, index) => { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - }); - }); - - it("some are cached, some expired, some invalid", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const retention = retentionPolicy.get(ParsableMintInfo)!; - - await fetcher.getAccounts([testMints[0], testMints[1]], ParsableMintInfo, undefined, now); - await fetcher.getAccounts( - [testMints[2], testMints[3], PublicKey.default], - ParsableMintInfo, - undefined, - now + 5 - ); - const result = await fetcher.getAccountsAsArray( - [...mintKeys, PublicKey.default], - ParsableMintInfo, - undefined, - now + retention + 1 - ); - expect(spy).toBeCalledTimes(3); - result.forEach((value, index) => { - if (index <= mintKeys.length - 1) { - expect(value).toBeDefined(); - expectMintEquals(value!, expectedMintInfos[index]); - } else { - // Expect the last value, which is invalid, to be null - expect(value).toBeNull(); - } - }); - }); - }); - - describe("refreshAll", () => { - it("refresh all updates all keys", async () => { - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const now = 32523523523; - - // Populate cache - await fetcher.getAccounts(testMints, ParsableMintInfo, undefined, now); - - const spy = jest.spyOn(ctx.connection, "getMultipleAccountsInfo"); - const renewNow = now + 500000; - await fetcher.refreshAll(renewNow); - expect(spy).toBeCalledTimes(1); - fetcher.cache.forEach((value, _) => { - expect(value.fetchedAt).toEqual(renewNow); - }); - }); - }); - - describe.only("populateAccounts", () => { - let expectedMintInfos: Mint[] = []; - - beforeAll(async () => { - for (const mint of testMints) { - expectedMintInfos.push(await getMint(ctx.connection, mint)); - } - }); - - it("populateAccounts updates all keys from empty state", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const other = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - const testSet = [mintKeys[0], mintKeys[1], mintKeys[2]]; - const otherFetched = await other.getAccounts(testSet, ParsableMintInfo, undefined, now); - - // Populate the fetcher with prefetched accounts and fetch from the fetcher to see if the cached values are set - fetcher.populateAccounts(otherFetched, ParsableMintInfo, now); - const results = await fetcher.getAccountsAsArray( - testSet, - ParsableMintInfo, - { - maxAge: Number.POSITIVE_INFINITY, - }, - now + 5 - ); - - results.forEach((value, index) => { - expectMintEquals(value!, expectedMintInfos[index]); - }); - fetcher.cache.forEach((value, _) => { - expect(value.fetchedAt).toEqual(now); - }); - }); - - it("populateAccounts updates all keys from non-empty state", async () => { - const mintKeys = testMints; - const now = 32523523523; - const fetcher = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - const other = new SimpleAccountFetcher(ctx.connection, retentionPolicy); - - await fetcher.getAccount(mintKeys[0], ParsableMintInfo, undefined, now - 5); - const testSet = [mintKeys[0], mintKeys[1], mintKeys[2]]; - const otherFetched = await other.getAccounts(testSet, ParsableMintInfo, undefined, now); - - expect(fetcher.cache.size).toEqual(1); - fetcher.cache.forEach((value, _) => { - expect(value.fetchedAt).toEqual(now - 5); - }); - - // Populate the fetcher with prefetched accounts and fetch from the fetcher to see if the cached values are set - fetcher.populateAccounts(otherFetched, ParsableMintInfo, now); - const results = await fetcher.getAccountsAsArray( - testSet, - ParsableMintInfo, - { - maxAge: Number.POSITIVE_INFINITY, - }, - now + 5 - ); - - results.forEach((value, index) => { - expectMintEquals(value!, expectedMintInfos[index]); - }); - - fetcher.cache.forEach((value, _) => { - expect(value.fetchedAt).toEqual(now); - }); - }); - }); -}); diff --git a/packages/common-sdk/tests/web3/transactions/constants.test.ts b/packages/common-sdk/tests/web3/transactions/constants.test.ts deleted file mode 100644 index 1066d98..0000000 --- a/packages/common-sdk/tests/web3/transactions/constants.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PACKET_DATA_SIZE } from "@solana/web3.js"; -import { TX_SIZE_LIMIT, TX_BASE64_ENCODED_SIZE_LIMIT } from "../../../src/web3"; - -jest.setTimeout(100 * 1000 /* ms */); - -describe("transactions-constants", () => { - it("TX_SIZE_LIMIT", async () => { - expect(TX_SIZE_LIMIT).toEqual(1232); - expect(TX_SIZE_LIMIT).toEqual(PACKET_DATA_SIZE); - }); - - it("TX_BASE64_ENCODED_SIZE_LIMIT", async () => { - expect(TX_BASE64_ENCODED_SIZE_LIMIT).toEqual(1644); - }); -}); diff --git a/packages/common-sdk/tests/web3/transactions/transactions-builder.test.ts b/packages/common-sdk/tests/web3/transactions/transactions-builder.test.ts deleted file mode 100644 index 8c5a7ec..0000000 --- a/packages/common-sdk/tests/web3/transactions/transactions-builder.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { TransactionInstruction, SystemProgram, Keypair } from "@solana/web3.js"; -import { - defaultTransactionBuilderOptions, - isVersionedTransaction, - MEASUREMENT_BLOCKHASH, - TransactionBuilder, -} from "../../../src/web3"; -import { createTestContext } from "../../test-context"; - -jest.setTimeout(100 * 1000 /* ms */); - -describe("transactions-builder", () => { - const ctx = createTestContext(); - - describe("txnSize", () => { - const buildTransactionBuilder = (transferIxNum: number, version: "legacy" | number) => { - const { wallet, connection } = ctx; - - const ixs: TransactionInstruction[] = []; - for (let i = 0; i < transferIxNum; i++) { - ixs.push( - SystemProgram.transfer({ - programId: SystemProgram.programId, - fromPubkey: wallet.publicKey, - lamports: 10_000_000, - toPubkey: Keypair.generate().publicKey, - }) - ); - } - - const builder = new TransactionBuilder(connection, wallet, { - ...defaultTransactionBuilderOptions, - defaultBuildOption: { - maxSupportedTransactionVersion: version, - latestBlockhash: MEASUREMENT_BLOCKHASH, - blockhashCommitment: "confirmed", - }, - }); - - builder.addInstruction({ - instructions: ixs, - cleanupInstructions: [], - signers: [], - }); - - return builder; - }; - - it("empty", async () => { - const { wallet, connection } = ctx; - const builder = new TransactionBuilder(connection, wallet); - - const size = builder.txnSize(); - expect(size).toEqual(0); - }); - - it("legacy: size < PACKET_DATA_SIZE", async () => { - const builder = buildTransactionBuilder(15, "legacy"); - - // should be legacy - const transaction = await builder.build(); - expect(isVersionedTransaction(transaction.transaction)).toBeFalsy(); - - const size = builder.txnSize(); - expect(size).toEqual(901); - }); - - it("legacy: size > PACKET_DATA_SIZE", async () => { - const builder = buildTransactionBuilder(22, "legacy"); - - // should be legacy - const transaction = await builder.build(); - expect(isVersionedTransaction(transaction.transaction)).toBeFalsy(); - - // logical size: 1244 > PACKET_DATA_SIZE - expect(() => builder.txnSize()).toThrow(/Unable to measure transaction size/); - }); - - it("v0: size < PACKET_DATA_SIZE", async () => { - const builder = buildTransactionBuilder(15, 0); - - // should be versioned - const transaction = await builder.build(); - expect(isVersionedTransaction(transaction.transaction)).toBeTruthy(); - - const size = builder.txnSize(); - expect(size).toEqual(903); - }); - - it("v0: size > PACKET_DATA_SIZE", async () => { - const builder = buildTransactionBuilder(22, 0); - - // should be versioned - const transaction = await builder.build(); - expect(isVersionedTransaction(transaction.transaction)).toBeTruthy(); - - // logical size: 1246 > PACKET_DATA_SIZE - expect(() => builder.txnSize()).toThrow(/Unable to measure transaction size/); - }); - - it("v0: size >> PACKET_DATA_SIZE", async () => { - const builder = buildTransactionBuilder(42, 0); - - // should be versioned - const transaction = await builder.build(); - expect(isVersionedTransaction(transaction.transaction)).toBeTruthy(); - - // logical size: 2226 >> PACKET_DATA_SIZE - expect(() => builder.txnSize()).toThrow(/Unable to measure transaction size/); - }); - }); -}); diff --git a/packages/common-sdk/tsconfig-base.json b/packages/common-sdk/tsconfig-base.json deleted file mode 100644 index a52f9c8..0000000 --- a/packages/common-sdk/tsconfig-base.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "allowJs": false, - "declaration": true, - "lib": ["es2020", "esnext", "DOM", "DOM.Iterable", "ScriptHost"], - "outDir": "./dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "experimentalDecorators": true, - "moduleResolution": "node" - }, - "exclude": ["./dist/**/*"], - "references": [ - { - "path": "./src/tsconfig.json" - }, - { - "path": "./tests/tsconfig.json" - } - ] -} From e8202529fdf2a3e9a5fd6e53833c2a44727c3130 Mon Sep 17 00:00:00 2001 From: Wilhelm Thieme Date: Mon, 9 Dec 2024 15:05:18 -0500 Subject: [PATCH 2/2] Remove mintlist cli from this repo --- packages/mintlist-cli/LICENSE | 11 -- packages/mintlist-cli/README.md | 13 -- packages/mintlist-cli/jest.config.js | 18 --- packages/mintlist-cli/package.json | 23 --- packages/mintlist-cli/src/cmd/add-mint.ts | 36 ----- packages/mintlist-cli/src/cmd/format.ts | 29 ---- packages/mintlist-cli/src/cmd/lint.ts | 66 -------- packages/mintlist-cli/src/cmd/remove-mint.ts | 19 --- packages/mintlist-cli/src/index.ts | 11 -- packages/mintlist-cli/src/program.ts | 39 ----- packages/mintlist-cli/src/tsconfig.json | 11 -- .../src/util/mintlist-file-util.ts | 146 ------------------ packages/mintlist-cli/tests/tsconfig.json | 8 - .../tests/util/mintlist-file-util.test.ts | 56 ------- packages/mintlist-cli/tsconfig-base.json | 24 --- 15 files changed, 510 deletions(-) delete mode 100644 packages/mintlist-cli/LICENSE delete mode 100644 packages/mintlist-cli/README.md delete mode 100644 packages/mintlist-cli/jest.config.js delete mode 100644 packages/mintlist-cli/package.json delete mode 100644 packages/mintlist-cli/src/cmd/add-mint.ts delete mode 100644 packages/mintlist-cli/src/cmd/format.ts delete mode 100644 packages/mintlist-cli/src/cmd/lint.ts delete mode 100644 packages/mintlist-cli/src/cmd/remove-mint.ts delete mode 100644 packages/mintlist-cli/src/index.ts delete mode 100644 packages/mintlist-cli/src/program.ts delete mode 100644 packages/mintlist-cli/src/tsconfig.json delete mode 100644 packages/mintlist-cli/src/util/mintlist-file-util.ts delete mode 100644 packages/mintlist-cli/tests/tsconfig.json delete mode 100644 packages/mintlist-cli/tests/util/mintlist-file-util.test.ts delete mode 100644 packages/mintlist-cli/tsconfig-base.json diff --git a/packages/mintlist-cli/LICENSE b/packages/mintlist-cli/LICENSE deleted file mode 100644 index 1c663f2..0000000 --- a/packages/mintlist-cli/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -Copyright 2022 Orca Foundation - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. - -You may obtain a copy of the License at: - - http://www.apache.org/licenses/LICENSE-2.0. - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/mintlist-cli/README.md b/packages/mintlist-cli/README.md deleted file mode 100644 index a061951..0000000 --- a/packages/mintlist-cli/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Mintlist CLI Utilities - -CLI for working with JSON mintlists and other token classes from the `@orca-so/token-sdk` package. - -This package is currently used by the `@orca-so/orca-mintlists-demo` repository for managing mintlists. -The goal is to separate dev dependencies from the `token-sdk` package and the `orca-mintlists` package. - -## Commands - -- `add-mint`: Adds a mint to a mintlist. No-op if duplicate. Sorts mints. -- `remove-mint`: Removes a mint from a mintlist. No-op if duplicate. Sorts mints. -- `lint`: Lints mintlists and overrides files by detecting formatting issues and alphabetical sorting. -- `format`: Formats files by fixing any detected lint errors. diff --git a/packages/mintlist-cli/jest.config.js b/packages/mintlist-cli/jest.config.js deleted file mode 100644 index 5469341..0000000 --- a/packages/mintlist-cli/jest.config.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - "roots": [ - "/src", - "/tests" - ], - "testMatch": [ - "**/__tests__/**/*.+(ts|tsx|js)", - "**/?(*.)+(spec|test).+(ts|tsx|js)" - ], - "transform": { - "^.+\\.(ts|tsx)$": "ts-jest" - }, - globals: { - "ts-jest": { - tsconfig: "./tests/tsconfig.json" - } - } -} diff --git a/packages/mintlist-cli/package.json b/packages/mintlist-cli/package.json deleted file mode 100644 index c527944..0000000 --- a/packages/mintlist-cli/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@orca-so/mintlist-cli", - "version": "0.2.2", - "license": "Apache-2.0", - "main": "dist/index.js", - "dependencies": { - "@orca-so/token-sdk": "^0.3.5", - "@solana/web3.js": "^1.88.0", - "commander": "^11.1.0", - "mz": "^2.7.0" - }, - "scripts": { - "clean": "rimraf dist", - "build": "tsc -p src", - "test": "jest" - }, - "bin": { - "mintlist": "./dist/index.js" - }, - "files": [ - "/dist" - ] -} diff --git a/packages/mintlist-cli/src/cmd/add-mint.ts b/packages/mintlist-cli/src/cmd/add-mint.ts deleted file mode 100644 index c8ff940..0000000 --- a/packages/mintlist-cli/src/cmd/add-mint.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; -import { MintlistFileUtil } from "../util/mintlist-file-util"; - -export function addMint(mintlistPath: string, addMints: string[]) { - let mintlist = MintlistFileUtil.readMintlistSync(mintlistPath); - const mints = mintlist.mints; - const addedMints = []; - for (const mint of addMints) { - // Check mint is valid pubkey - try { - new PublicKey(mint); - } catch (e) { - console.log(`Invalid mint ${mint}`); - continue; - } - - // Check mint doesn't already exist - const exists = mints.indexOf(mint) !== -1; - if (exists) { - continue; - } - - mints.push(mint); - addedMints.push(mint); - } - - mints.sort(MintlistFileUtil.cmpMint); - mintlist.mints = mints; - MintlistFileUtil.writeJsonSync(mintlistPath, mintlist); - - if (addedMints.length === 0) { - console.log("No mints added"); - } else { - console.log(`Added ${addedMints.length} mints to ${mintlist.name}:\n${addedMints.join("\n")}`); - } -} diff --git a/packages/mintlist-cli/src/cmd/format.ts b/packages/mintlist-cli/src/cmd/format.ts deleted file mode 100644 index c4a673e..0000000 --- a/packages/mintlist-cli/src/cmd/format.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { MintlistFileUtil } from "../util/mintlist-file-util"; - -export function format(dir: string) { - // Read directory contents - const files = fs.readdirSync(dir); - - // Iterate over each file or sub-directory - for (const file of files) { - const filePath = path.join(dir, file); - const stat = fs.statSync(filePath); - - // If it's a directory, recurse into it - if (stat.isDirectory()) { - format(filePath); - } else if (stat.isFile()) { - // If it's a file, check if it's a JSON file and has a specific pattern in its name - if (path.extname(file) === ".json") { - if (MintlistFileUtil.validMintlistName(file)) { - MintlistFileUtil.formatMintlist(filePath); - } - if (MintlistFileUtil.validOverridesName(file)) { - MintlistFileUtil.formatOverrides(filePath); - } - } - } - } -} diff --git a/packages/mintlist-cli/src/cmd/lint.ts b/packages/mintlist-cli/src/cmd/lint.ts deleted file mode 100644 index 38db7e4..0000000 --- a/packages/mintlist-cli/src/cmd/lint.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { MintlistFileUtil } from "../util/mintlist-file-util"; - -/** - * Initiates the check for formatting issues in JSON files within a given directory. - * If any errors are found, the process exits with a status code of 1. - * - * @param {string} dir - The directory path to start checking. - */ -export function lint(dir: string) { - const errors = traverseDirectory(dir, checkFileFormat); - if (errors.length > 0) { - console.error(`${errors.length} file(s) had formatting errors:\n${errors.join("\n")}`); - process.exit(1); - } -} - -/** - * Recursively traverses a directory and applies a given callback function to each file. - * - * @param {string} dir - The directory to traverse. - * @param {(filePath: string) => string[]} callback - The function to apply to each file. Should return an array of error strings. - * @returns {string[]} - An array of file paths that had issues as determined by the callback. - */ -function traverseDirectory(dir: string, callback: (filePath: string) => string[]): string[] { - let errors: string[] = []; - const files = fs.readdirSync(dir); - - for (const file of files) { - const filePath = path.join(dir, file); - const stat = fs.statSync(filePath); - - if (stat.isDirectory()) { - errors = errors.concat(traverseDirectory(filePath, callback)); - } else if (stat.isFile()) { - errors = errors.concat(callback(filePath)); - } - } - - return errors; -} - -/** - * Checks the format of a given JSON file based on its name. - * - * @param {string} filePath - The full path of the file to check. - * @returns {string[]} - An array containing the file path if it had a formatting error, or an empty array otherwise. - */ -function checkFileFormat(filePath: string): string[] { - const errors: string[] = []; - const file = path.basename(filePath); - - if (path.extname(file) === ".json") { - if ( - (MintlistFileUtil.validMintlistName(file) && - !MintlistFileUtil.checkMintlistFormat(filePath)) || - (MintlistFileUtil.validOverridesName(file) && - !MintlistFileUtil.checkOverridesFormat(filePath)) - ) { - errors.push(filePath); - } - } - - return errors; -} diff --git a/packages/mintlist-cli/src/cmd/remove-mint.ts b/packages/mintlist-cli/src/cmd/remove-mint.ts deleted file mode 100644 index f0f0f93..0000000 --- a/packages/mintlist-cli/src/cmd/remove-mint.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MintlistFileUtil } from "../util/mintlist-file-util"; - -export function removeMint(mintlistPath: string, removeMints: string[]) { - let mintlist = MintlistFileUtil.readMintlistSync(mintlistPath); - const mints = mintlist.mints; - let numRemoved = 0; - for (const mint of removeMints) { - const index = mints.indexOf(mint); - if (index !== -1) { - mints.splice(index, 1); - numRemoved++; - } - } - mints.sort(MintlistFileUtil.cmpMint); - mintlist.mints = mints; - MintlistFileUtil.writeJsonSync(mintlistPath, mintlist); - - console.log(`Removed ${numRemoved} mints from ${mintlist.name} (${mintlistPath})`); -} diff --git a/packages/mintlist-cli/src/index.ts b/packages/mintlist-cli/src/index.ts deleted file mode 100644 index 9731f3f..0000000 --- a/packages/mintlist-cli/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env node - -import program from "./program"; - -async function main() { - await program.parseAsync(); -} - -main() - .then(() => {}) - .catch((e) => console.error(e)); diff --git a/packages/mintlist-cli/src/program.ts b/packages/mintlist-cli/src/program.ts deleted file mode 100644 index c4145d0..0000000 --- a/packages/mintlist-cli/src/program.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Command } from "commander"; -import { addMint } from "./cmd/add-mint"; -import { lint } from "./cmd/lint"; -import { format } from "./cmd/format"; -import { removeMint } from "./cmd/remove-mint"; - -const program = new Command(); - -program.name("mintlist-cli").description("CLI for managing mintlists").version("0.1.0"); - -program - .command("add-mint") - .description("Add a mint to a mintlist") - .argument("", "Path to mintlist file") - .argument("", "Mint(s) to add to mintlist") - .action(addMint); - -program - .command("remove-mint") - .alias("rm-mint") - .description("Remove a mint from a mintlist") - .argument("", "Path to mintlist file") - .argument("", "Mint(s) to remove from mintlist") - .action(removeMint); - -program - .command("format") - .description("Format the provided mintlists and overrides") - .alias("fmt") - .argument("", "Root directory of mintlists and overrides to format") - .action(format); - -program - .command("lint") - .description("Check the provided mintlists and overrides for formatting errors") - .argument("", "Root directory of mintlists and overrides to check") - .action(lint); - -export default program; diff --git a/packages/mintlist-cli/src/tsconfig.json b/packages/mintlist-cli/src/tsconfig.json deleted file mode 100644 index 8d6c266..0000000 --- a/packages/mintlist-cli/src/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../tsconfig-base.json", - "compilerOptions": { - "composite": true - }, - "include": ["./**/*"], - "typedocOptions": { - "entryPoints": ["index.ts"], - "out": "../../target/typedocs" - } -} diff --git a/packages/mintlist-cli/src/util/mintlist-file-util.ts b/packages/mintlist-cli/src/util/mintlist-file-util.ts deleted file mode 100644 index 6f44095..0000000 --- a/packages/mintlist-cli/src/util/mintlist-file-util.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Mintlist, Overrides } from "@orca-so/token-sdk"; -import { readFileSync, writeFileSync } from "mz/fs"; -import { resolve } from "path"; -import path from "node:path"; -import { Address, AddressUtil } from "@orca-so/common-sdk"; - -export class MintlistFileUtil { - public static readMintlistSync(filePath: string): Mintlist { - const paths = MintlistFileUtil.toValidFilePaths(filePath); - if (paths.length !== 1) { - throw new Error(`No valid mintlist found at ${filePath} - must be in src/mintlists`); - } - try { - return this.fromString(readFileSync(resolve(paths[0]), "utf-8")); - } catch (e) { - throw new Error(`Failed to parse mintlist at ${paths[0]}`); - } - } - - public static readOverridesSync(filePath: string): Overrides { - try { - return JSON.parse(readFileSync(resolve(filePath), "utf-8")) as Overrides; - } catch (e) { - throw new Error(`Failed to parse overrides at ${filePath}`); - } - } - - public static checkMintlistFormat(filePath: string): boolean { - let mintlist; - try { - mintlist = MintlistFileUtil.readMintlistSync(filePath); - } catch (e) { - return false; - } - - const mints = AddressUtil.toStrings(mintlist.mints); - - // Check that all mints are valid - mints.forEach((mint) => { - try { - AddressUtil.toPubKey(mint); - } catch (e) { - return false; - } - }); - - // Check mints are in ascending order - for (let i = 1; i < mintlist.mints.length; i++) { - if (MintlistFileUtil.cmpMint(mints[i], mints[i - 1]) <= 0) { - return false; - } - } - return true; - } - - public static checkOverridesFormat(filePath: string): boolean { - let overrides: Overrides; - try { - overrides = MintlistFileUtil.readOverridesSync(filePath); - } catch (e) { - return false; - } - - const VALID_FIELDS = ["name", "symbol", "image"]; - Object.entries(overrides).forEach(([mint, metadata]) => { - // Check that all mints are valid - try { - AddressUtil.toPubKey(mint); - } catch (e) { - return false; - } - // Check that all metadata fields are valid - if (!Object.values(metadata).every((f) => VALID_FIELDS.includes(f))) { - return false; - } - }); - - // Check mints are in ascending order - const mints = Object.keys(overrides); - for (let i = 1; i < mints.length; i++) { - if (MintlistFileUtil.cmpMint(mints[i], mints[i - 1]) <= 0) { - return false; - } - } - return true; - } - - public static formatMintlist(filePath: string) { - const mintlist = MintlistFileUtil.readMintlistSync(filePath); - mintlist.mints.sort(MintlistFileUtil.cmpMint); - MintlistFileUtil.writeJsonSync(filePath, mintlist); - } - - public static formatOverrides(filePath: string) { - const overrides = MintlistFileUtil.readOverridesSync(filePath); - const formatted = Object.fromEntries( - Object.entries(overrides).sort(([mintA], [mintB]) => MintlistFileUtil.cmpMint(mintA, mintB)) - ); - MintlistFileUtil.writeJsonSync(filePath, formatted); - } - - public static fromString(str: string): T { - try { - return JSON.parse(str) as T; - } catch (e) { - throw new Error(`Failed to parse from string`); - } - } - - public static validMintlistName(name: string): boolean { - return /^[a-zA-Z][a-zA-Z\d]*(-[a-zA-Z\d]+)*\.mintlist\.json$/.test(name); - } - - public static validOverridesName(name: string): boolean { - return /^([a-zA-Z]+\.)?overrides\.json$/.test(name); - } - - public static writeJsonSync(filePath: string, obj: any) { - try { - const fullPath = resolve(filePath); - const json = JSON.stringify(obj, null, 2); - writeFileSync(fullPath, json + "\n"); - } catch (e) { - throw new Error(`Failed to write file at ${filePath}`); - } - } - - public static getFileName(filePath: string): string { - const name = filePath.split(path.sep).pop(); - if (!name) { - throw new Error("Invalid path"); - } - return name; - } - - public static toValidFilePaths(str: string): string[] { - return str - .split("\n") - .filter((line) => line.length > 0) - .filter((line) => MintlistFileUtil.validMintlistName(MintlistFileUtil.getFileName(line))); - } - - public static cmpMint(a: Address, b: Address): number { - return AddressUtil.toString(a).localeCompare(AddressUtil.toString(b)); - } -} diff --git a/packages/mintlist-cli/tests/tsconfig.json b/packages/mintlist-cli/tests/tsconfig.json deleted file mode 100644 index 2339569..0000000 --- a/packages/mintlist-cli/tests/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../tsconfig-base.json", - "references": [ - { - "path": "../src" - } - ] -} diff --git a/packages/mintlist-cli/tests/util/mintlist-file-util.test.ts b/packages/mintlist-cli/tests/util/mintlist-file-util.test.ts deleted file mode 100644 index fabe01c..0000000 --- a/packages/mintlist-cli/tests/util/mintlist-file-util.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { MintlistFileUtil } from "../../src/util/mintlist-file-util"; -jest.setTimeout(100 * 1000 /* ms */); - -describe("mintlist-file-util", () => { - it("valid json file names", async () => { - type TestCase = [string, boolean]; - const tests: TestCase[] = [ - ["xxxx.mintlist.json", true], - ["x235.mintlist.json", true], - ["x23x.mintlist.json", true], - ["xx-.mintlist.json", false], - ["xxxx.MintList.json", false], - ["xxxx.MINTLIST.json", false], - ["x.y.z.mintlist.json", false], - ["xx.yy.zz.mintlist.json", false], - ["x-y-z.mintlist.json", true], - ["x-y-z.n.mintlist.json", false], - ["-x-y-z.mintlist.json", false], - ["x--z.mintlist.json", false], - ["orca-whitelisted.mintlist.json", true], - ["orca-whitelisted.bob.json", false], - ["x.mintlist", false], - ["x.mintlist.jsx", false], - ]; - - for (const [name, expected] of tests) { - expect(MintlistFileUtil.validMintlistName(name)).toBe(expected); - } - }); - - it("valid overrides file names", async () => { - type TestCase = [string, boolean]; - const tests: TestCase[] = [ - ["overrides.json", true], - ["xxxx.overrides.json", true], - ["x23x.overrides.json", false], - ["xx-.overrides.json", false], - ["xxxx.Overrides.json", false], - ["xxxx.OVERRIDES.json", false], - ["x.y.z.overrides.json", false], - ["xx.yy.zz.overrides.json", false], - ["x-y-z.overrides.json", false], - ["x-y-z.n.overrides.json", false], - ["-x-y-z.overrides.json", false], - ["x--z.overrides.json", false], - ["orca-whitelisted.overrides.json", false], - ["orca-whitelisted.bob.json", false], - ["x.overrides", false], - ["x.overrides.jsx", false], - ]; - - for (const [name, expected] of tests) { - expect(MintlistFileUtil.validOverridesName(name)).toBe(expected); - } - }); -}); diff --git a/packages/mintlist-cli/tsconfig-base.json b/packages/mintlist-cli/tsconfig-base.json deleted file mode 100644 index 6c46157..0000000 --- a/packages/mintlist-cli/tsconfig-base.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "allowJs": false, - "declaration": true, - "outDir": "./dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "experimentalDecorators": true, - "moduleResolution": "node" - }, - "exclude": ["./dist/**/*"], - "references": [ - { - "path": "./src/tsconfig.json" - }, - { - "path": "./tests/tsconfig.json" - } - ] -}