@quartz-labs/sdk
Version:
SDK for interacting with the Quartz Protocol
303 lines • 15.3 kB
JavaScript
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