mango-v4-ui/utils/governance/tools.ts

243 lines
6.2 KiB
TypeScript

import {
getGovernanceAccounts,
getRealm,
Governance,
ProgramAccount,
pubkeyFilter,
} from '@solana/spl-governance'
import {
Connection,
PublicKey,
RpcResponseAndContext,
SignatureResult,
Transaction,
VersionedTransaction,
} from '@solana/web3.js'
import { TokenProgramAccount } from './accounts/vsrAccounts'
import { MintLayout, RawMint } from '@solana/spl-token'
import { BN } from '@coral-xyz/anchor'
import { awaitTransactionSignatureConfirmation } from '@blockworks-foundation/mangolana/lib/transactions'
import { MangoError, tryStringify } from '@blockworks-foundation/mango-v4'
export async function fetchRealm({
connection,
realmId,
}: {
connection: Connection
realmId: PublicKey
}) {
const realm = await getRealm(connection, realmId)
return realm
}
export async function fetchGovernances({
connection,
realmId,
programId,
}: {
connection: Connection
realmId: PublicKey
programId: PublicKey
}) {
const governances = await getGovernanceAccounts(
connection,
programId,
Governance,
[pubkeyFilter(1, realmId)!],
)
const governancesMap = accountsToPubkeyMap(governances)
return governancesMap
}
export function accountsToPubkeyMap<T>(accounts: ProgramAccount<T>[]) {
return arrayToRecord(accounts, (a) => a.pubkey.toBase58())
}
export function arrayToRecord<T>(
source: readonly T[],
getKey: (item: T) => string,
) {
return source.reduce((all, a) => ({ ...all, [getKey(a)]: a }), {}) as Record<
string,
T
>
}
export async function tryGetMint(
connection: Connection,
publicKey: PublicKey,
): Promise<TokenProgramAccount<RawMint> | undefined> {
try {
const result = await connection.getAccountInfo(publicKey)
const data = Buffer.from(result!.data)
const account = parseMintAccountData(data)
return {
publicKey,
account,
}
} catch (ex) {
console.error(
`Can't fetch mint ${publicKey?.toBase58()} @ ${connection.rpcEndpoint}`,
ex,
)
return undefined
}
}
export function parseMintAccountData(data: Buffer): RawMint {
const mintInfo = MintLayout.decode(data)
return mintInfo
}
export const fmtTokenAmount = (c: BN, decimals?: number) =>
c?.div(new BN(10).pow(new BN(decimals ?? 0))).toNumber() || 0
export const tryGetPubKey = (pubkey: string) => {
try {
return new PublicKey(pubkey)
} catch (e) {
return null
}
}
const urlRegex =
// eslint-disable-next-line
/(https:\/\/)(gist\.github.com\/)([\w\/]{1,39}\/)([\w]{1,32})/
export async function fetchGistFile(gistUrl: string) {
const controller = new AbortController()
const pieces = gistUrl.match(urlRegex)
if (pieces) {
const justIdWithoutUser = pieces[4]
if (justIdWithoutUser) {
const apiUrl = 'https://api.github.com/gists/' + justIdWithoutUser
const apiResponse = await fetch(apiUrl, {
signal: controller.signal,
})
const jsonContent = await apiResponse.json()
if (apiResponse.status === 200) {
const nextUrlFileName = Object.keys(jsonContent['files'])[0]
const nextUrl = jsonContent['files'][nextUrlFileName]['raw_url']
if (nextUrl.startsWith('https://gist.githubusercontent.com/')) {
const fileResponse = await fetch(nextUrl, {
signal: controller.signal,
})
const body = await fileResponse.json()
//console.log('fetchGistFile file', gistUrl, fileResponse)
return body
}
return undefined
} else {
console.warn('could not fetchGistFile', {
gistUrl,
apiResponse: jsonContent,
})
}
}
}
return undefined
}
export async function resolveProposalDescription(descriptionLink: string) {
try {
const url = new URL(descriptionLink)
const desc = (await fetchGistFile(url.toString())) ?? descriptionLink
return desc
} catch {
return descriptionLink
}
}
export const compareObjectsAndGetDifferentKeys = <T extends object>(
object1: T,
object2: T,
): (keyof T)[] => {
const diffKeys: string[] = []
Object.keys(object1).forEach((key) => {
if (
object1[key as keyof typeof object1] !==
object2[key as keyof typeof object2]
) {
diffKeys.push(key)
}
})
return diffKeys as (keyof T)[]
}
export const sendTxAndConfirm = async (
multipleConnections: Connection[] = [],
connection: Connection,
tx: Transaction | VersionedTransaction,
latestBlockhash: {
lastValidBlockHeight: number
blockhash: string
},
) => {
let signature = ''
const abortController = new AbortController()
try {
const allConnections = [connection, ...multipleConnections]
const rawTransaction = tx.serialize()
signature = await Promise.any(
allConnections.map((c) => {
return c.sendRawTransaction(rawTransaction, {
skipPreflight: true,
})
}),
)
await Promise.any(
allConnections.map((c) =>
awaitTransactionSignatureConfirmation({
txid: signature,
confirmLevel: 'processed',
connection: c,
timeoutStrategy: {
block: latestBlockhash,
},
abortSignal: abortController.signal,
}),
),
)
abortController.abort()
return signature
} catch (e) {
abortController.abort()
if (e instanceof AggregateError) {
for (const individualError of e.errors) {
const stringifiedError = tryStringify(individualError)
throw new MangoError({
txid: signature,
message: `${
stringifiedError
? stringifiedError
: individualError
? individualError
: 'Unknown error'
}`,
})
}
}
if (isErrorWithSignatureResult(e)) {
const stringifiedError = tryStringify(e?.value?.err)
throw new MangoError({
txid: signature,
message: `${stringifiedError ? stringifiedError : e?.value?.err}`,
})
}
const stringifiedError = tryStringify(e)
throw new MangoError({
txid: signature,
message: `${stringifiedError ? stringifiedError : e}`,
})
}
}
function isErrorWithSignatureResult(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
err: any,
): err is RpcResponseAndContext<SignatureResult> {
return err && typeof err.value !== 'undefined'
}