UNPKG

@quartz-labs/sdk

Version:

SDK for interacting with the Quartz Protocol

303 lines 15.3 kB
import { BN } from "@drift-labs/sdk"; import { calculateBorrowRate, calculateDepositRate, DRIFT_PROGRAM_ID, fetchUserAccountsUsingKeys as fetchDriftAccountsUsingKeys } from "@drift-labs/sdk"; import { MAX_ACCOUNTS_PER_FETCH_CALL, MESSAGE_TRANSMITTER_PROGRAM_ID, QUARTZ_ADDRESS_TABLE, QUARTZ_PROGRAM_ID } from "./config/constants.js"; import { IDL } from "./types/idl/pyra.js"; import { AnchorProvider, BorshInstructionCoder, Program, setProvider } from "@coral-xyz/anchor"; import { QuartzUser } from "./QuartzUser.class.js"; import { getBridgeRentPayerPublicKey, getDepositAddressPublicKey, getDriftStatePublicKey, getDriftUserPublicKey, getDriftUserStatsPublicKey, getInitRentPayerPublicKey, getMessageTransmitter, getVaultPublicKey } from "./utils/accounts.js"; import { SystemProgram, SYSVAR_RENT_PUBKEY, } from "@solana/web3.js"; import { DummyWallet } from "./types/classes/DummyWallet.class.js"; import { retryWithBackoff } from "./utils/helpers.js"; import { DriftClientService } from "./services/DriftClientService.js"; import AdvancedConnection from "@quartz-labs/connection"; export class QuartzClient { connection; program; quartzLookupTable; driftClient; constructor(connection, program, quartzAddressTable, driftClient) { this.connection = connection; this.program = program; this.quartzLookupTable = quartzAddressTable; this.driftClient = driftClient; } static async getProgram(connection) { const wallet = new DummyWallet(); const provider = new AnchorProvider(connection, wallet, { commitment: "confirmed" }); setProvider(provider); return new Program(IDL, QUARTZ_PROGRAM_ID, provider); } /** * Fetch a QuartzClient instance. * @param config - Configuration object, you must provide either `rpcUrls` or `connection`. * @param config.rpcUrls - Array of RPC URLs. * @param config.connection - AdvancedConnection instance (from @quartz-labs/connection) * @param config.pollingFrequency - Polling frequency in milliseconds. * @returns QuartzClient instance. */ static async fetchClient(config) { if (!config.connection && !config.rpcUrls) throw Error("Either rpcUrls or connection must be provided"); const connection = config.connection ?? new AdvancedConnection(config.rpcUrls); const pollingFrequency = config.pollingFrequency ?? 1000; const program = await QuartzClient.getProgram(connection); const quartzLookupTable = await connection.getAddressLookupTable(QUARTZ_ADDRESS_TABLE).then((res) => res.value); if (!quartzLookupTable) throw Error("Address Lookup Table account not found"); const driftClient = await DriftClientService.getDriftClient(connection, pollingFrequency); return new QuartzClient(connection, program, quartzLookupTable, driftClient); } static async doesQuartzUserExist(connection, owner, attempts = 5) { const vault = getVaultPublicKey(owner); const program = await QuartzClient.getProgram(connection); try { await retryWithBackoff(async () => { await program.account.vault.fetch(vault); }, attempts); return true; } catch { return false; } } async getAllQuartzAccountOwnerPubkeys() { return (await this.program.account.vault.all()).map((vault) => vault.account.owner); } async getQuartzAccount(owner) { const vaultAddress = getVaultPublicKey(owner); const vaultAccount = await this.program.account.vault.fetch(vaultAddress); // Check account exists const [driftUserAccount] = await fetchDriftAccountsUsingKeys(this.connection, this.driftClient.program, [getDriftUserPublicKey(vaultAddress)]); if (!driftUserAccount) throw Error("Drift user not found"); return new QuartzUser(owner, this.connection, this, this.program, this.quartzLookupTable, this.driftClient, driftUserAccount, vaultAccount.spendLimitPerTransaction, vaultAccount.spendLimitPerTimeframe, vaultAccount.remainingSpendLimitPerTimeframe, vaultAccount.nextTimeframeResetTimestamp, vaultAccount.timeframeInSeconds); } async getMultipleQuartzAccounts(owners) { if (owners.length === 0) return []; const vaultAddresses = owners.map((owner) => getVaultPublicKey(owner)); const vaultChunks = Array.from({ length: Math.ceil(vaultAddresses.length / MAX_ACCOUNTS_PER_FETCH_CALL) }, (_, i) => vaultAddresses.slice(i * MAX_ACCOUNTS_PER_FETCH_CALL, (i + 1) * MAX_ACCOUNTS_PER_FETCH_CALL)); const vaultResults = await Promise.all(vaultChunks.map(async (chunk) => { const vaultAccounts = await this.program.account.vault.fetchMultiple(chunk); return vaultAccounts.map((account, index) => { if (account === null) throw Error(`Account not found for pubkey: ${vaultAddresses[index]?.toBase58()}`); return account; }); })); const vaultAccounts = vaultResults.flat(); const driftResults = await Promise.all(vaultChunks.map(async (chunk) => { return await fetchDriftAccountsUsingKeys(this.connection, this.driftClient.program, chunk.map((vault) => getDriftUserPublicKey(vault))); })); const driftUsers = driftResults.flat(); // TODO: Uncomment once Drift accounts are guaranteed // const undefinedIndex = driftUsers.findIndex(user => !user); // if (undefinedIndex !== -1) { // throw new Error(`[${this.wallet?.publicKey}] Failed to fetch drift user for vault ${vaults[undefinedIndex].toBase58()}`); // } return driftUsers.map((driftUser, index) => { if (driftUser === undefined) return null; if (owners[index] === undefined) throw Error("Missing pubkey in owners array"); const vaultAccount = vaultAccounts[index]; if (!vaultAccount) throw Error(`Vault account not found for pubkey: ${vaultAddresses[index]?.toBase58()}`); return new QuartzUser(owners[index], this.connection, this, this.program, this.quartzLookupTable, this.driftClient, driftUser, vaultAccount.spendLimitPerTransaction, vaultAccount.spendLimitPerTimeframe, vaultAccount.remainingSpendLimitPerTimeframe, vaultAccount.nextTimeframeResetTimestamp, vaultAccount.timeframeInSeconds); }); } async getDepositRate(marketIndex) { const spotMarket = await this.getSpotMarketAccount(marketIndex); const depositRate = calculateDepositRate(spotMarket); return depositRate; } async getBorrowRate(marketIndex) { const spotMarket = await this.getSpotMarketAccount(marketIndex); const borrowRate = calculateBorrowRate(spotMarket); return borrowRate; } async getCollateralWeight(marketIndex) { const spotMarket = await this.getSpotMarketAccount(marketIndex); return spotMarket.initialAssetWeight; } async getSpotMarketAccount(marketIndex) { const spotMarket = await this.driftClient.getSpotMarketAccount(marketIndex); if (!spotMarket) throw Error("Spot market not found"); return spotMarket; } async getOpenWithdrawOrders(user) { const query = user ? [{ memcmp: { offset: 8, bytes: user.toBase58() } }] : undefined; const orders = await this.program.account.withdrawOrder.all(query); const sortedOrders = orders.sort((a, b) => a.account.timeLock.releaseSlot.cmp(b.account.timeLock.releaseSlot)); return sortedOrders.map((order) => ({ publicKey: order.publicKey, account: { ...order.account, driftMarketIndex: new BN(order.account.driftMarketIndex) } })); } async getOpenSpendLimitsOrders(user) { const query = user ? [{ memcmp: { offset: 8, bytes: user.toBase58() } }] : undefined; const orders = await this.program.account.spendLimitsOrder.all(query); return orders.sort((a, b) => a.account.timeLock.releaseSlot.cmp(b.account.timeLock.releaseSlot)); } async parseOpenWithdrawOrder(order, retries = 5) { const orderAccount = await retryWithBackoff(async () => { return await this.program.account.withdrawOrder.fetch(order); }, retries); return { ...orderAccount, driftMarketIndex: new BN(orderAccount.driftMarketIndex) }; } async parseOpenSpendLimitsOrder(order, retries = 5) { const orderAccount = await retryWithBackoff(async () => { return await this.program.account.spendLimitsOrder.fetch(order); }, retries); return orderAccount; } async listenForInstruction(instructionName, onInstruction, ignoreErrors = true) { this.connection.onLogs(QUARTZ_PROGRAM_ID, async (logs) => { if (!logs.logs.some(log => log.includes(instructionName))) return; const tx = await retryWithBackoff(async () => { const tx = await this.connection.getTransaction(logs.signature, { maxSupportedTransactionVersion: 0, commitment: 'confirmed' }); if (!tx) throw new Error("Transaction not found"); return tx; }); if (tx.meta?.err && ignoreErrors) return; const encodedIxs = tx.transaction.message.compiledInstructions; const coder = new BorshInstructionCoder(IDL); for (const ix of encodedIxs) { try { const quartzIx = coder.decode(Buffer.from(ix.data), "base58"); if (quartzIx?.name.toLowerCase() === instructionName.toLowerCase()) { const accountKeys = tx.transaction.message.staticAccountKeys; onInstruction(tx, ix, accountKeys); } } catch { } } }, "confirmed"); } static async parseSpendIx(connection, signature, owner) { const INSRTUCTION_NAME = "CompleteSpend"; const ACCOUNT_INDEX_OWNER = 1; const ACCOUNT_INDEX_MESSAGE_SENT_EVENT_DATA = 12; const tx = await retryWithBackoff(async () => { const tx = await connection.getTransaction(signature, { maxSupportedTransactionVersion: 0, commitment: "finalized" }); if (!tx) throw new Error("Transaction not found"); return tx; }); const encodedIxs = tx.transaction.message.compiledInstructions; const coder = new BorshInstructionCoder(IDL); for (const ix of encodedIxs) { try { const quartzIx = coder.decode(Buffer.from(ix.data), "base58"); if (quartzIx?.name.toLowerCase() === INSRTUCTION_NAME.toLowerCase()) { const accountKeys = tx.transaction.message.staticAccountKeys; const ownerIndex = ix.accountKeyIndexes?.[ACCOUNT_INDEX_OWNER]; if (ownerIndex === undefined || accountKeys[ownerIndex] === undefined) { throw new Error("Owner not found"); } const actualOwner = accountKeys[ownerIndex]; if (!actualOwner.equals(owner)) throw new Error("Owner does not match"); const messageSentEventDataIndex = ix.accountKeyIndexes?.[ACCOUNT_INDEX_MESSAGE_SENT_EVENT_DATA]; if (messageSentEventDataIndex === undefined || accountKeys[messageSentEventDataIndex] === undefined) { throw new Error("Message sent event data not found"); } return accountKeys[messageSentEventDataIndex]; } } catch { } } throw new Error("Spend instruction not found"); } // --- Instructions --- /** * Creates instructions to initialize a new Quartz user account. * @param owner - The public key of Quartz account owner. * @param spendLimitPerTransaction - The card spend limit per transaction. * @param spendLimitPerTimeframe - The card spend limit per timeframe. * @param timeframeInSlots - The timeframe in slots (eg: 216,000 for ~1 day). * @returns {Promise<{ * ixs: TransactionInstruction[], * lookupTables: AddressLookupTableAccount[], * signers: Keypair[] * }>} Object containing: * - ixs: Array of instructions to initialize the Quartz user account. * - lookupTables: Array of lookup tables for building VersionedTransaction. * - signers: Array of signer keypairs that must sign the transaction the instructions are added to. * @throws Error if the RPC connection fails. The transaction will fail if the vault already exists, or the user does not have enough SOL. */ async makeInitQuartzUserIxs(owner, spendLimitPerTransaction, spendLimitPerTimeframe, timeframeInSeconds, nextTimeframeResetTimestamp) { const vault = getVaultPublicKey(owner); const ix_initUser = await this.program.methods .initUser(spendLimitPerTransaction, spendLimitPerTimeframe, timeframeInSeconds, nextTimeframeResetTimestamp) .accounts({ vault: vault, owner: owner, initRentPayer: getInitRentPayerPublicKey(), driftUser: getDriftUserPublicKey(vault), driftUserStats: getDriftUserStatsPublicKey(vault), driftState: getDriftStatePublicKey(), driftProgram: DRIFT_PROGRAM_ID, rent: SYSVAR_RENT_PUBKEY, systemProgram: SystemProgram.programId, depositAddress: getDepositAddressPublicKey(owner), }) .instruction(); return { ixs: [ix_initUser], lookupTables: [this.quartzLookupTable], signers: [] }; } admin = { makeReclaimBridgeRentIxs: async (messageSentEventData, attestation, rentReclaimer) => { const ix_reclaimBridgeRent = await this.program.methods .reclaimBridgeRent(attestation) .accounts({ rentReclaimer: rentReclaimer.publicKey, bridgeRentPayer: getBridgeRentPayerPublicKey(), messageTransmitter: getMessageTransmitter(), messageSentEventData: messageSentEventData, cctpMessageTransmitter: MESSAGE_TRANSMITTER_PROGRAM_ID }) .instruction(); return { ixs: [ix_reclaimBridgeRent], lookupTables: [this.quartzLookupTable], signers: [rentReclaimer] }; } }; } //# sourceMappingURL=QuartzClient.class.js.map