Accounts
Create, derive, load and read marginfi accounts, and move or close them when you are done.
A marginfi account holds a wallet's positions (up to 16 balance slots) in one group. This page covers the full account lifecycle through Project0Client and MarginfiAccountWrapper. It assumes you have a client from Getting Started.
Creating accounts
client.createMarginfiAccountTx(authority, accountIndex = 0, thirdPartyId?) returns a transaction that initializes a new account PDA:
const createAccountTx = await client.createMarginfiAccountTx(
wallet.publicKey,
0 // account index (default: 0)
);
const signature = await wallet.sendTransaction(createAccountTx, connection);
await connection.confirmTransaction(signature);| Parameter | Type | Description |
|---|---|---|
authority | PublicKey | Wallet that will own the account |
accountIndex | number (u16) | Lets one wallet own several accounts; each index is a distinct PDA. Default 0 |
thirdPartyId | number (u16, optional) | Namespaces accounts for an integrator, so your index 0 never collides with another app's index 0 |
// A second account for the same wallet
const secondAccountTx = await client.createMarginfiAccountTx(wallet.publicKey, 1);
// An account namespaced under a third-party id
const thirdPartyAccountTx = await client.createMarginfiAccountTx(wallet.publicKey, 0, 12345);Composing creation with other instructions
If you want to create an account and act on it in the same transaction (or before it is confirmed), use the projection method. It returns a wrapper for the future account plus the creation instruction:
const { wrappedAccount, ix } = await client.createMarginfiAccountWithProjection(
wallet.publicKey,
0
);
// wrappedAccount can build deposit/borrow transactions now; prepend `ix`
// to the transaction you send so the account exists when they execute.Deriving and discovering accounts
Account addresses are PDAs of (programId, group, authority, accountIndex, thirdPartyId), so you can derive them offline:
import { deriveMarginfiAccount } from "@0dotxyz/p0-ts-sdk";
const [accountAddress] = deriveMarginfiAccount(
client.program.programId,
client.group.address,
wallet.publicKey,
0 // account index
// , 12345 // third-party id (defaults to 0)
);When you don't know the index, scan the chain instead. All three helpers use filtered getProgramAccounts calls and return addresses only, so they stay cheap even across hundreds of thousands of accounts:
// Every account owned by a wallet in this group
const addresses = await client.getAccountAddresses(wallet.publicKey);
// Every account holding a position in a bank (any authority)
const holders = await client.getAccountAddressesHoldingBank(usdcBank.address, {
concurrency: 4, // optional: batch the 16 slot scans to stay under RPC limits
});
// Every (account, bank) row holding a mint, with authority and UI amounts
const rows = await client.getAuthorityBalancesForMint(USDC_MINT, {
assetTag: AssetTag.DEFAULT, // optional: restrict to one bank flavour
concurrency: 4,
});
for (const row of rows) {
// row: { authority, accountAddress, bank, assets, liabilities } (BigNumber, UI units)
console.log(row.authority.toBase58(), row.assets.toFixed(2), row.liabilities.toFixed(2));
}A mint can map to several banks (for example a standard USDC bank and a Kamino USDC bank), so getAuthorityBalancesForMint resolves all of them via getBanksByMint and emits one row per (account, bank). The balances call decodes full accounts and is heavier than the address-only scans; see examples/18-mint-holders.ts.
Loading an account
The convenience path is client.fetchAccount(address, skipHealthCache?), which fetches, parses and wraps in one call:
import { PublicKey } from "@solana/web3.js";
const accountAddress = new PublicKey("YOUR_MARGINFI_ACCOUNT_ADDRESS");
// Fetch, simulate the health cache, and wrap
const wrappedAccount = await client.fetchAccount(accountAddress);
// Skip the health-cache simulation (one fewer RPC round-trip) when you
// only need the raw balances, not health-based numbers
const rawWrapped = await client.fetchAccount(accountAddress, true);fetchAccount runs simulateHealthCache by default so computeMaxBorrowForBank, computeFreeCollateralFromCache and friends are fresh. See Health & Max Amounts for what the cache contains and how to refresh it later.
If you want control over each step (for example to wrap an account you already fetched elsewhere):
import { MarginfiAccount, MarginfiAccountWrapper } from "@0dotxyz/p0-ts-sdk";
const account = await MarginfiAccount.fetch(accountAddress, client.program);
const wrappedAccount = new MarginfiAccountWrapper(account, client);The wrapper is a thin layer over MarginfiAccount that auto-injects the client's banks, oracle prices, lookup tables and integration metadata into every method, so you only pass what is specific to the action.
Reading positions
wrappedAccount.balances is the fixed 16-slot array; wrappedAccount.activeBalances filters it to slots that are in use. Each Balance stores shares, not token amounts — convert with the bank:
import { MarginRequirementType } from "@0dotxyz/p0-ts-sdk";
for (const balance of wrappedAccount.activeBalances) {
const bank = client.getBank(balance.bankPk);
const oraclePrice = client.oraclePriceByBank.get(balance.bankPk.toBase58());
if (!bank || !oraclePrice) continue;
// Integration banks (Kamino, Drift, Jupiter Lend) hold shares of a share;
// the multiplier converts them to underlying tokens. Undefined for standard banks.
const multiplier = client.assetShareValueMultiplierByBank.get(balance.bankPk.toBase58());
// Token amounts in UI units
const { assets, liabilities } = balance.computeQuantityUi(bank, multiplier);
// USD value. MarginRequirementType.Equity = unweighted; Initial/Maintenance
// apply the bank's asset/liability weights.
const usd = balance.computeUsdValue(bank, oraclePrice, MarginRequirementType.Equity, multiplier);
console.log(
`${bank.tokenSymbol}: ${assets.toFixed(4)} deposited ($${usd.assets.toFixed(2)}), ` +
`${liabilities.toFixed(4)} borrowed ($${usd.liabilities.toFixed(2)})`
);
}Other useful accessors:
| Accessor | Description |
|---|---|
wrappedAccount.getBalance(bankPk) | The Balance for one bank (an empty balance if none) |
balance.assetShares / balance.liabilityShares | Raw share amounts (BigNumber) |
bank.getAssetQuantity(shares) / bank.getLiabilityQuantity(shares) | Shares to native token amount, before decimals |
balance.computeQuantity(bank) | Native token amounts (no decimals applied) |
balance.getUsdValueWithPriceBias(bank, oraclePrice, ...) | USD value using the oracle's low/high confidence bound, as the program does for health |
wrappedAccount.authority, .group, .address | Account identity |
wrappedAccount.healthCache | The on-chain/simulated health cache; see Health & Max Amounts |
Account flags are exposed as booleans:
wrappedAccount.isDisabled— the account has been disabled (for example after it was transferred to a new account) and rejects further actions.wrappedAccount.isFlashLoanEnabled— the account is currently inside a flash loan (the flag is set betweenbeginandend; see Flash Loans).wrappedAccount.isTransferAccountAuthorityEnabled— authority transfer has been allowed for this account.
Account lifecycle
Close an account
An account with no active balances can be closed to reclaim rent. makeCloseAccountIx() returns an InstructionsWrapper ({ instructions, keys }):
import { Transaction } from "@solana/web3.js";
if (wrappedAccount.activeBalances.length > 0) {
throw new Error("Withdraw and repay everything before closing");
}
const { instructions } = await wrappedAccount.makeCloseAccountIx();
const closeTx = new Transaction().add(...instructions);
const sig = await wallet.sendTransaction(closeTx, connection);
await connection.confirmTransaction(sig);Transfer an account to a new authority
makeAccountTransferToNewAccountTx(newMarginfiAccount, newAuthority, feePayer?) migrates every position into a brand-new account owned by newAuthority and leaves the old account disabled. The new account keypair must sign (it creates itself), the current authority signs to authorize, and feePayer (a PublicKey for adapter signing or a Keypair; defaults to the current authority) pays the program's account-transfer fee and rent.
import { Keypair } from "@solana/web3.js";
const newMarginfiAccount = Keypair.generate();
const transferTx = await wrappedAccount.makeAccountTransferToNewAccountTx(
newMarginfiAccount,
newAuthority.publicKey
);
// ExtendedV0Transaction: the new account keypair is a required signer
transferTx.sign([newMarginfiAccount]);
const sig = await wallet.sendTransaction(transferTx, connection);
await connection.confirmTransaction(sig);This instruction's account layout changes with program 0.1.10 — the SDK handles it for you, see Program Upgrade 0.1.10.
Move selected positions to another account
makeTransferPositionsTx moves a chosen subset of positions (collateral or debt) from this account to a destination account atomically inside a flash loan. The side is inferred from this account's balance in each bank; debt positions are re-borrowed on the destination with a small interest padding.
const result = await wrappedAccount.makeTransferPositionsTx({
bankAddresses: [solBank.address, usdcBank.address],
// destinationAccount: otherAccount, // omit to create a fresh account in the same tx
// createDestinationOpts: { accountIndex: 1 }, // only used when destinationAccount is omitted
// borrowPaddingBps: 10, // head-room over estimated debt; default 10
// maxPositions: 5, // reject larger selections; default 5
});
// result: TransferPositionsResult
// transactions: ExtendedV0Transaction[] ordered [setup/crank txs…, flashloan tx]
// actionTxIndex: number index of the flashloan tx
// destinationAccount: MarginfiAccountType
// mustBeAtomicBundle: boolean true → send as one Jito bundleProp
Type
When mustBeAtomicBundle is true the transactions must land in one atomic bundle — see Transactions for sending bundles. Failures throw a TransactionBuildingError with a TRANSFER_POSITIONS_* code; see Errors.
Bulk withdraw and repay
For draining an account in one go, the Layer 3 actions makeBulkWithdrawTx and makeBulkRepayTx (from @0dotxyz/p0-ts-sdk, not on the wrapper) withdraw the full position of every listed bank, or repay the full debt of every listed bank, packing as many instructions per transaction as fit. Both return { transactions, actionTxIndex, mustBeAtomicBundle }; bulk withdraws must land as one bundle because the integration refreshes live in a single prelude transaction. Being Layer 3, they take the client data explicitly (program, connection, marginfiAccount, bankMap, tokenProgramsByBank, and for withdraws also oraclePrices, bankMetadataMap, assetShareValueMultiplierByBank, luts), so build them from client.* fields as described in Architecture.
Related
- Getting Started — client setup and first deposit
- Health & Max Amounts — what
fetchAccountsimulates and how to read it - Banks & Prices — the
bankandoraclePriceobjects used above - Transactions — multi-transaction results and atomic bundles
- Examples:
18-mint-holders.ts,06a-account-health-simulated.ts