PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-sol/src/lib
Просмотр файла: utils.ts
import {
BuildTransactionError,
isValidXpub,
NotSupported,
ParseTransactionError,
TransactionType,
UtilsError,
} from '@bitgo/sdk-core';
import { BaseCoin, BaseNetwork, CoinNotDefinedError, coins, SolCoin } from '@bitgo/statics';
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
decodeCloseAccountInstruction,
getAssociatedTokenAddress,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import {
Keypair,
PublicKey,
SignaturePubkeyPair,
Transaction as SolTransaction,
StakeInstruction,
StakeProgram,
SystemInstruction,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js';
import assert from 'assert';
import BigNumber from 'bignumber.js';
import bs58 from 'bs58';
import nacl from 'tweetnacl';
import {
ataCloseInstructionIndexes,
ataInitInstructionIndexes,
MAX_MEMO_LENGTH,
MEMO_PROGRAM_PK,
nonceAdvanceInstruction,
stakingActivateInstructionsIndexes,
marinadeStakingActivateInstructionsIndexes,
stakingAuthorizeInstructionsIndexes,
stakingDeactivateInstructionsIndexes,
marinadeStakingDeactivateInstructionsIndexes,
stakingDelegateInstructionsIndexes,
stakingPartialDeactivateInstructionsIndexes,
stakingWithdrawInstructionsIndexes,
VALID_SYSTEM_INSTRUCTION_TYPES,
validInstructionData,
validInstructionData2,
ValidInstructionTypesEnum,
walletInitInstructionIndexes,
} from './constants';
import { ValidInstructionTypes } from './iface';
const DECODED_BLOCK_HASH_LENGTH = 32; // https://docs.solana.com/developing/programming-model/transactions#blockhash-format
const DECODED_SIGNATURE_LENGTH = 64; // https://docs.solana.com/terminology#signature
const BASE_58_ENCONDING_REGEX = '[1-9A-HJ-NP-Za-km-z]';
const COMPUTE_BUDGET = 'ComputeBudget111111111111111111111111111111';
/** @inheritdoc */
export function isValidAddress(address: string): boolean {
return isValidPublicKey(address);
}
/** @inheritdoc */
export function isValidBlockId(hash: string): boolean {
try {
return (
!!hash && new RegExp(BASE_58_ENCONDING_REGEX).test(hash) && bs58.decode(hash).length === DECODED_BLOCK_HASH_LENGTH
);
} catch (e) {
return false;
}
}
/** @inheritdoc */
export function isValidPrivateKey(prvKey: string | Uint8Array): boolean {
try {
const key: Uint8Array = typeof prvKey === 'string' ? base58ToUint8Array(prvKey) : prvKey;
return !!Keypair.fromSecretKey(key);
} catch (e) {
return false;
}
}
/** @inheritdoc */
export function isValidPublicKey(pubKey: string): boolean {
try {
if (isValidXpub(pubKey)) return true;
new PublicKey(pubKey);
return true;
} catch {
return false;
}
}
/** @inheritdoc */
export function isValidSignature(signature: string): boolean {
try {
return !!signature && bs58.decode(signature).length === DECODED_SIGNATURE_LENGTH;
} catch (e) {
return false;
}
}
/** @inheritdoc */
// TransactionId are the first signature on a Transaction
export function isValidTransactionId(txId: string): boolean {
return isValidSignature(txId);
}
/**
* Returns whether or not the string is a valid amount of lamports number
*
* @param {string} amount - the string to validate
* @returns {boolean} - the validation result
*/
export function isValidAmount(amount: string): boolean {
const bigNumberAmount = new BigNumber(amount);
return bigNumberAmount.isInteger() && bigNumberAmount.isGreaterThanOrEqualTo(0);
}
/**
* Check if the string is a valid amount of lamports number on staking
*
* @param {string} amount - the string to validate
* @returns {boolean} - the validation result
*/
export function isValidStakingAmount(amount: string): boolean {
const bigNumberAmount = new BigNumber(amount);
return bigNumberAmount.isInteger() && bigNumberAmount.isGreaterThan(0);
}
/**
* Check if this is a valid memo or not.
*
* @param memo - the memo string
* @returns {boolean} - the validation result
*/
export function isValidMemo(memo: string): boolean {
return Buffer.from(memo).length <= MAX_MEMO_LENGTH;
}
/**
* Checks if raw transaction can be deserialized
*
* @param {string} rawTransaction - transaction in base64 string format
* @param {boolean} requireAllSignatures - require all signatures to be present
* @param {boolean} verifySignatures - verify signatures
* @returns {boolean} - the validation result
*/
export function isValidRawTransaction(
rawTransaction: string,
requireAllSignatures = false,
verifySignatures = false
): boolean {
try {
const tx = SolTransaction.from(Buffer.from(rawTransaction, 'base64'));
tx.serialize({ requireAllSignatures, verifySignatures });
return true;
} catch (e) {
return false;
}
}
/**
* Verifies if signature for message is valid.
*
* @param {Buffer} serializedTx - tx as a base64 string
* @param {string} signature - signature as a string
* @param {string} publicKey - public key as base 58
* @returns {Boolean} true if signature is valid.
*/
export function verifySignature(serializedTx: string, signature: string, publicKey: string): boolean {
if (!isValidRawTransaction(serializedTx)) {
throw new UtilsError('Invalid serializedTx');
}
if (!isValidPublicKey(publicKey)) {
throw new UtilsError('Invalid publicKey');
}
if (!isValidSignature(signature)) {
throw new UtilsError('Invalid signature');
}
const msg = SolTransaction.from(Buffer.from(serializedTx, 'base64')).serializeMessage();
const sig = base58ToUint8Array(signature);
const pub = new PublicKey(publicKey);
return nacl.sign.detached.verify(msg, sig, pub.toBuffer());
}
/**
* Converts a base58 string into a Uint8Array.
*
* @param {string} input - a string in base58
* @returns {Uint8Array} - an Uint8Array
*/
export function base58ToUint8Array(input: string): Uint8Array {
return new Uint8Array(bs58.decode(input));
}
/**
* Converts a Uint8Array to a base58 string.
*
* @param {Uint8Array} input - an Uint8Array
* @returns {string} - a string in base58
*/
export function Uint8ArrayTobase58(input: Uint8Array): string {
return bs58.encode(input);
}
/**
* Count the amount of signatures are not null.
*
* @param {SignaturePubkeyPair[]} signatures - an array of SignaturePubkeyPair
* @returns {number} - the amount of valid signatures
*/
export function countNotNullSignatures(signatures: SignaturePubkeyPair[]): number {
return signatures.filter((sig) => !!sig.signature).length;
}
/**
* Check if all signatures are completed.
*
* @param {SignaturePubkeyPair[]} signatures - signatures
* @returns {boolean}
*/
export function requiresAllSignatures(signatures: SignaturePubkeyPair[]): boolean {
return signatures.length > 0 && countNotNullSignatures(signatures) === signatures.length;
}
/**
* Check the transaction type matching instructions by order. Memo and AdvanceNonceAccount instructions
* are ignored.
*
* @param {TransactionInstruction[]} instructions - the array of supported Solana instructions to be parsed
* @param {Record<string, number>} instructionIndexes - the instructions indexes of the current transaction
* @returns true if it matches by order.
*/
export function matchTransactionTypeByInstructionsOrder(
instructions: TransactionInstruction[],
instructionIndexes: Record<string, number>
): boolean {
const instructionsCopy = [...instructions]; // Make a copy since we may modify the array below
// AdvanceNonceAccount is optional and the first instruction added, it does not matter to match the type
if (instructionsCopy.length > 0) {
if (getInstructionType(instructions[0]) === 'AdvanceNonceAccount') {
instructionsCopy.shift();
}
}
// Memo is optional and the last instruction added, it does not matter to match the type
// Why have it in instructionKeys if we are going to ignore it?
const instructionsKeys = Object.keys(instructionIndexes);
if (instructionsKeys[instructionsKeys.length - 1] === 'Memo') {
instructionsKeys.pop();
}
// Check instructions by order using the index.
for (const keyName of instructionsKeys) {
const result = getInstructionType(instructionsCopy[instructionIndexes[keyName]]);
if (result !== keyName) {
return false;
}
}
return true;
}
/**
* Returns the transaction Type based on the transaction instructions.
* Wallet initialization, Transfer and Staking transactions are supported.
*
* @param {SolTransaction} transaction - the solana transaction
* @returns {TransactionType} - the type of transaction
*/
export function getTransactionType(transaction: SolTransaction): TransactionType {
const { instructions } = transaction;
if (validateRawMsgInstruction(instructions)) {
return TransactionType.StakingAuthorizeRaw;
}
validateIntructionTypes(instructions);
// check if deactivate instruction does not exist because deactivate can be include a transfer instruction
const memoInstruction = instructions.find((instruction) => getInstructionType(instruction) === 'Memo');
const memoData = memoInstruction?.data.toString('utf-8');
if (instructions.filter((instruction) => getInstructionType(instruction) === 'Deactivate').length == 0) {
for (const instruction of instructions) {
const instructionType = getInstructionType(instruction);
// Check if memo instruction is there and if it contains 'PrepareForRevoke' because Marinade staking deactivate transaction will have this
if (
(instructionType === ValidInstructionTypesEnum.Transfer && !memoData?.includes('PrepareForRevoke')) ||
instructionType === ValidInstructionTypesEnum.TokenTransfer
) {
return TransactionType.Send;
}
}
}
if (matchTransactionTypeByInstructionsOrder(instructions, walletInitInstructionIndexes)) {
return TransactionType.WalletInitialization;
} else if (
matchTransactionTypeByInstructionsOrder(instructions, marinadeStakingActivateInstructionsIndexes) ||
matchTransactionTypeByInstructionsOrder(instructions, stakingActivateInstructionsIndexes)
) {
return TransactionType.StakingActivate;
} else if (matchTransactionTypeByInstructionsOrder(instructions, stakingAuthorizeInstructionsIndexes)) {
return TransactionType.StakingAuthorize;
} else if (matchTransactionTypeByInstructionsOrder(instructions, stakingDelegateInstructionsIndexes)) {
return TransactionType.StakingDelegate;
} else if (
matchTransactionTypeByInstructionsOrder(instructions, marinadeStakingDeactivateInstructionsIndexes) ||
matchTransactionTypeByInstructionsOrder(instructions, stakingDeactivateInstructionsIndexes) ||
matchTransactionTypeByInstructionsOrder(instructions, stakingPartialDeactivateInstructionsIndexes)
) {
return TransactionType.StakingDeactivate;
} else if (matchTransactionTypeByInstructionsOrder(instructions, stakingWithdrawInstructionsIndexes)) {
return TransactionType.StakingWithdraw;
} else if (matchTransactionTypeByInstructionsOrder(instructions, ataInitInstructionIndexes)) {
return TransactionType.AssociatedTokenAccountInitialization;
} else if (matchTransactionTypeByInstructionsOrder(instructions, ataCloseInstructionIndexes)) {
return TransactionType.CloseAssociatedTokenAccount;
} else {
throw new NotSupported('Invalid transaction, transaction not supported or invalid');
}
}
/**
* Returns the instruction Type based on the solana instructions.
* Throws if the solana instruction program is not supported
*
* @param {TransactionInstruction} instruction - a solana instruction
* @returns {ValidInstructionTypes} - a solana instruction type
*/
export function getInstructionType(instruction: TransactionInstruction): ValidInstructionTypes {
switch (instruction.programId.toString()) {
case new PublicKey(MEMO_PROGRAM_PK).toString():
return 'Memo';
case SystemProgram.programId.toString():
return SystemInstruction.decodeInstructionType(instruction);
case TOKEN_PROGRAM_ID.toString():
try {
const decodedInstruction = decodeCloseAccountInstruction(instruction);
if (decodedInstruction && decodedInstruction.data.instruction === 9) {
return 'CloseAssociatedTokenAccount';
}
} catch (e) {
// ignore error and default to TokenTransfer
return 'TokenTransfer';
}
return 'TokenTransfer';
case StakeProgram.programId.toString():
return StakeInstruction.decodeInstructionType(instruction);
case ASSOCIATED_TOKEN_PROGRAM_ID.toString():
// TODO: change this when @spl-token supports decoding associated token instructions
if (instruction.data.length === 0) {
return 'InitializeAssociatedTokenAccount';
} else {
throw new NotSupported(
'Invalid transaction, instruction program id not supported: ' + instruction.programId.toString()
);
}
case COMPUTE_BUDGET:
return 'SetPriorityFee';
default:
throw new NotSupported(
'Invalid transaction, instruction program id not supported: ' + instruction.programId.toString()
);
}
}
/**
* Validate solana instructions types to see if they are supported by the builder.
* Throws if the instruction type is invalid.
*
* @param {TransactionInstruction} instructions - a solana instruction
* @returns {void}
*/
export function validateIntructionTypes(instructions: TransactionInstruction[]): void {
for (const instruction of instructions) {
if (!VALID_SYSTEM_INSTRUCTION_TYPES.includes(getInstructionType(instruction))) {
throw new NotSupported('Invalid transaction, instruction type not supported: ' + getInstructionType(instruction));
}
}
}
/**
* Validate solana instructions match raw msg authorize transaction
*
* @param {TransactionInstruction} instructions - a solana instruction
* @returns {boolean} true if the instructions match the raw msg authorize transaction
*/
export function validateRawMsgInstruction(instructions: TransactionInstruction[]): boolean {
// as web3.js cannot decode authorize instruction from CLI, we need to check it manually first
if (instructions.length === 2) {
const programId1 = instructions[0].programId.toString();
const programId2 = instructions[1].programId.toString();
if (programId1 === SystemProgram.programId.toString() && programId2 === StakeProgram.programId.toString()) {
const instructionName1 = SystemInstruction.decodeInstructionType(instructions[0]);
const data = instructions[1].data.toString('hex');
if (
instructionName1 === nonceAdvanceInstruction &&
(data === validInstructionData || data === validInstructionData2)
) {
return true;
}
}
}
if (instructions.length === 3) {
const programId1 = instructions[0].programId.toString();
const programId2 = instructions[1].programId.toString();
const programId3 = instructions[2].programId.toString();
if (
programId1 === SystemProgram.programId.toString() &&
programId2 === StakeProgram.programId.toString() &&
programId3 === StakeProgram.programId.toString()
) {
const instructionName1 = SystemInstruction.decodeInstructionType(instructions[0]);
const data = instructions[1].data.toString('hex');
const data2 = instructions[2].data.toString('hex');
if (
instructionName1 === nonceAdvanceInstruction &&
(data === validInstructionData || data === validInstructionData2) &&
(data2 === validInstructionData || data2 === validInstructionData2)
) {
return true;
}
}
}
return false;
}
/**
* Check the raw transaction has a valid format in the blockchain context, throw otherwise.
*
* @param {string} rawTransaction - Transaction in base64 string format
*/
export function validateRawTransaction(
rawTransaction: string,
requireAllSignatures = false,
verifySignatures = false
): void {
if (!rawTransaction) {
throw new ParseTransactionError('Invalid raw transaction: Undefined');
}
if (!isValidRawTransaction(rawTransaction, requireAllSignatures, verifySignatures)) {
throw new ParseTransactionError('Invalid raw transaction');
}
}
/**
* Validates address to check if it exists and is a valid Solana public key
*
* @param {string} address The address to be validated
* @param {string} fieldName Name of the field to validate, its needed to return which field is failing on case of error.
*/
export function validateAddress(address: string, fieldName: string): void {
if (!address || !isValidPublicKey(address)) {
throw new BuildTransactionError(`Invalid or missing ${fieldName}, got: ${address}`);
}
}
/**
* Get the statics coin object matching a given Solana token address if it exists
*
* @param tokenAddress The token address to match against
* @param network Solana Mainnet or Testnet
* @returns statics BaseCoin object for the matching token
*/
export function getSolTokenFromAddress(tokenAddress: string, network: BaseNetwork): Readonly<BaseCoin> | undefined {
const tokens = coins.filter((coin) => {
if (coin instanceof SolCoin) {
return coin.network.type === network.type && coin.tokenAddress.toLowerCase() === tokenAddress.toLowerCase();
}
return false;
});
const tokensArray = tokens.map((token) => token);
if (tokensArray.length >= 1) {
// there should never be two tokens with the same contract address, so we assert that here
assert(tokensArray.length === 1);
return tokensArray[0];
}
return undefined;
}
/**
* Get the solana token object from token name
* @param tokenName The token name to match against
* */
export function getSolTokenFromTokenName(tokenName: string): Readonly<SolCoin> | undefined {
try {
const token = coins.get(tokenName);
if (!(token.isToken && token instanceof SolCoin)) {
return undefined;
}
return token;
} catch (e) {
if (!(e instanceof CoinNotDefinedError)) {
throw e;
}
return undefined;
}
}
/**
* Get the solana associated token account address
* @param tokenAddress token mint address
* @param ownerAddress The owner of the associated token account
* @returns The associated token account address
* */
export async function getAssociatedTokenAccountAddress(
tokenMintAddress: string,
ownerAddress: string,
allowOwnerOffCurve = false
): Promise<string> {
const ownerPublicKey = new PublicKey(ownerAddress);
// tokenAddress are not on ed25519 curve, so they can't be used as ownerAddress
if (!allowOwnerOffCurve && !PublicKey.isOnCurve(ownerPublicKey.toBuffer())) {
throw new UtilsError('Invalid ownerAddress - address off ed25519 curve, got: ' + ownerAddress);
}
const ataAddress = await getAssociatedTokenAddress(
new PublicKey(tokenMintAddress),
ownerPublicKey,
allowOwnerOffCurve
);
return ataAddress.toString();
}
export function validateMintAddress(mintAddress: string) {
if (!mintAddress || !isValidAddress(mintAddress)) {
throw new BuildTransactionError('Invalid or missing mintAddress, got: ' + mintAddress);
}
}
export function validateOwnerAddress(ownerAddress: string) {
if (!ownerAddress || !isValidAddress(ownerAddress)) {
throw new BuildTransactionError('Invalid or missing ownerAddress, got: ' + ownerAddress);
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!