Atomic oracle transactions
Construct the Ed25519 verification and oracle-refresh instructions required by borrowing transactions.
Minting nUSD and withdrawing collateral with debt require a fresh oracle update in the same transaction. The instruction order is a security boundary:
optional prefix instructions
compute budget
Ed25519 signature verification
Nest oracle refresh
mint or withdrawThe refresh instruction records the exact Ed25519 instruction index. If a wallet or middleware inserts or reorders instructions, the transaction must be rejected and rebuilt with corrected indexes.
Oracle helper
First create src/nest/nest-client.ts from Project setup.
That local module exports NEST_API_ORIGIN, CollateralMarket, NestClient,
and oraclePda; those names do not come from an npm package.
Then create src/nest/nest-oracle.ts beside it. It supports both Pyth Lazer
and Nest-signed markets returned by the public Nest API.
import {
ComputeBudgetProgram,
Ed25519Program,
PublicKey,
SYSVAR_INSTRUCTIONS_PUBKEY,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import {
NEST_API_ORIGIN,
type CollateralMarket,
type NestClient,
oraclePda,
} from "./nest-client";
const REFRESH_LAZER = Buffer.from([34, 110, 193, 85, 108, 145, 68, 174]);
const REFRESH_SIGNED = Buffer.from([14, 131, 238, 44, 16, 211, 43, 143]);
const CURRENT_INSTRUCTION = 0xffff;
const LAZER_ANCHOR_MESSAGE_OFFSET = 12;
const SIGNED_PAYLOAD_BYTES = 73;
type OracleUpdate = {
schema: "nest-oracle-update-v1";
oracleProvider: "pyth-lazer" | "jupiter-signed";
symbol: string;
feedId: string;
priceE8: string;
confidenceE8: string;
publishTime: number;
sourceAgeAtFetch: number;
maxSourceAgeSeconds: number;
expiresAt: number | null;
proof: {
format: "pyth-lazer" | "ed25519";
encoding: "base64";
payload: string;
signature: string | null;
signer: string | null;
};
accounts: {
program: string;
storage: string;
treasury: string;
} | null;
};
export type CurrentOraclePrice = {
schema: "nest-oracle-price-v1";
oracleProvider: string;
symbol: string;
feedId: string;
priceE8: string;
confidenceE8: string;
safePriceE8: string;
publishTime: number;
sourceAgeAtFetch: number;
cacheAgeSeconds: number;
cacheMaxAgeSeconds: number;
cacheStale: boolean;
oracleUpdate: OracleUpdate;
};
type PriceSummary = {
clusterUnixTimestamp: number;
requestedSymbols: string[];
prices: CurrentOraclePrice[];
errors: Array<{ symbol: string; error: string }>;
};
function validateCurrentOraclePrice(
market: CollateralMarket,
price: CurrentOraclePrice,
) {
if (
price.schema !== "nest-oracle-price-v1"
|| price.symbol !== market.symbol
|| price.oracleProvider !== market.oracleProvider
) throw new Error("Price API returned the wrong market or provider");
const expectedFeed = market.pythLazerFeedId !== null
? String(market.pythLazerFeedId)
: market.signedOracleFeedId;
if (!expectedFeed || price.feedId !== expectedFeed) {
throw new Error("Price API returned the wrong oracle feed");
}
if (price.cacheStale) throw new Error(`${market.symbol} price cache is stale`);
const age = price.sourceAgeAtFetch + price.cacheAgeSeconds;
if (age > market.maxStalenessSeconds || age > price.cacheMaxAgeSeconds) {
throw new Error(`${market.symbol} price is ${age}s old`);
}
const priceE8 = BigInt(price.priceE8);
const confidenceE8 = BigInt(price.confidenceE8);
const safePriceE8 = BigInt(price.safePriceE8);
if (
priceE8 <= 0n
|| confidenceE8 < 0n
|| safePriceE8 !== priceE8 - confidenceE8
|| safePriceE8 <= 0n
|| confidenceE8 * 10_000n > priceE8 * BigInt(market.maxConfidenceBps)
) throw new Error("Price API returned an invalid price or confidence interval");
if (
price.oracleUpdate.schema !== "nest-oracle-update-v1"
|| price.oracleUpdate.symbol !== price.symbol
|| price.oracleUpdate.feedId !== price.feedId
|| price.oracleUpdate.oracleProvider !== price.oracleProvider
|| price.oracleUpdate.priceE8 !== price.priceE8
|| price.oracleUpdate.confidenceE8 !== price.confidenceE8
|| price.oracleUpdate.publishTime !== price.publishTime
|| price.oracleUpdate.sourceAgeAtFetch > price.oracleUpdate.maxSourceAgeSeconds
|| price.oracleUpdate.proof.encoding !== "base64"
) throw new Error("Price API returned an invalid oracle update");
}
export async function fetchCurrentOraclePrices(
markets: CollateralMarket[],
apiKey?: string,
): Promise<Map<string, CurrentOraclePrice>> {
const response = await fetch(
`${NEST_API_ORIGIN}/api/prices`,
{
cache: "no-store",
headers: apiKey ? { "x-api-key": apiKey } : undefined,
},
);
if (!response.ok) throw new Error(`Price API returned HTTP ${response.status}`);
const summary = await response.json() as PriceSummary;
if (!Array.isArray(summary.prices) || !Array.isArray(summary.errors)) {
throw new Error("Price API returned an invalid batch response");
}
if (summary.errors.length > 0) {
throw new Error(`Some prices are unavailable: ${JSON.stringify(summary.errors)}`);
}
const marketsBySymbol = new Map(markets.map((market) => [market.symbol, market]));
const prices = new Map<string, CurrentOraclePrice>();
for (const price of summary.prices) {
const market = marketsBySymbol.get(price.symbol);
if (!market) continue;
validateCurrentOraclePrice(market, price);
prices.set(price.symbol, price);
}
for (const market of markets) {
if (!prices.has(market.symbol)) {
throw new Error(`Price API omitted ${market.symbol}`);
}
}
return prices;
}
function lazerEd25519Instruction(messageData: Buffer, refreshIndex: number) {
const signatureOffset = LAZER_ANCHOR_MESSAGE_OFFSET + 4;
const publicKeyOffset = signatureOffset + 64;
const messageSizeOffset = publicKeyOffset + 32;
const messageOffset = messageSizeOffset + 2;
const relativeSizeOffset = messageSizeOffset - LAZER_ANCHOR_MESSAGE_OFFSET;
const relativeMessageOffset = messageOffset - LAZER_ANCHOR_MESSAGE_OFFSET;
if (messageData.length < relativeMessageOffset) throw new Error("Malformed Lazer message");
const messageSize = messageData.readUInt16LE(relativeSizeOffset);
if (relativeMessageOffset + messageSize > messageData.length) {
throw new Error("Lazer signed data exceeds message length");
}
// Pyth keeps signature/public key/message bytes in the following refresh
// instruction, so the offsets point at refreshIndex rather than this one.
const data = Buffer.alloc(16);
data.writeUInt8(1, 0);
data.writeUInt8(0, 1);
[
signatureOffset,
refreshIndex,
publicKeyOffset,
refreshIndex,
messageOffset,
messageSize,
refreshIndex,
].forEach((value, index) => data.writeUInt16LE(value, 2 + index * 2));
return new TransactionInstruction({
programId: Ed25519Program.programId,
keys: [],
data,
});
}
function signedEd25519Instruction(message: Buffer, signature: Buffer, signer: PublicKey) {
if (message.length !== SIGNED_PAYLOAD_BYTES) {
throw new Error(`Signed payload is ${message.length} bytes; expected ${SIGNED_PAYLOAD_BYTES}`);
}
if (signature.length !== 64) throw new Error("Signed oracle signature must be 64 bytes");
return Ed25519Program.createInstructionWithPublicKey({
publicKey: signer.toBytes(),
message,
signature,
});
}
function lazerRefreshData(messageData: Buffer, ed25519Index: number) {
const data = Buffer.alloc(8 + 4 + messageData.length + 2 + 1);
REFRESH_LAZER.copy(data, 0);
data.writeUInt32LE(messageData.length, 8);
messageData.copy(data, 12);
data.writeUInt16LE(ed25519Index, 12 + messageData.length);
data.writeUInt8(0, 14 + messageData.length); // signature index
return data;
}
function signedRefreshData(message: Buffer, ed25519Index: number) {
if (message.length !== SIGNED_PAYLOAD_BYTES) throw new Error("Invalid signed payload length");
const data = Buffer.alloc(8 + message.length + 2);
REFRESH_SIGNED.copy(data, 0);
message.copy(data, 8);
data.writeUInt16LE(ed25519Index, 8 + message.length);
return data;
}
export function buildOracleRefreshInstructions(
client: NestClient,
market: CollateralMarket,
prefixInstructionCount: number,
update: OracleUpdate,
) {
if (
update.symbol !== market.symbol
|| update.oracleProvider !== market.oracleProvider
) throw new Error("Oracle update does not match the selected market");
const owner = client.wallet.publicKey;
const coreProgram = new PublicKey(client.deployment.programs.nestCore);
const protocol = new PublicKey(client.deployment.accounts.protocol);
const collateralConfig = new PublicKey(market.collateralConfig);
const oracle = oraclePda(coreProgram, collateralConfig);
const computeInstructions = [
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 150_000 }),
ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 }),
];
const ed25519Index = prefixInstructionCount + computeInstructions.length;
const refreshIndex = ed25519Index + 1;
if (update.oracleProvider === "pyth-lazer") {
if (update.proof.format !== "pyth-lazer" || !update.accounts) {
throw new Error("Incomplete Pyth Lazer oracle update");
}
if (
market.pythLazerFeedId !== null
&& market.pythLazerFeedId !== undefined
&& update.feedId !== String(market.pythLazerFeedId)
) throw new Error("Pyth Lazer feed mismatch");
const messageData = Buffer.from(update.proof.payload, "base64");
const verify = lazerEd25519Instruction(messageData, refreshIndex);
const refresh = new TransactionInstruction({
programId: coreProgram,
keys: [
{ pubkey: protocol, isSigner: false, isWritable: false },
{ pubkey: collateralConfig, isSigner: false, isWritable: false },
{ pubkey: oracle, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: true },
{
pubkey: new PublicKey(update.accounts.program),
isSigner: false,
isWritable: false,
},
{
pubkey: new PublicKey(update.accounts.storage),
isSigner: false,
isWritable: false,
},
{ pubkey: new PublicKey(update.accounts.treasury), isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
],
data: lazerRefreshData(messageData, ed25519Index),
});
return [...computeInstructions, verify, refresh];
}
if (
update.proof.format !== "ed25519"
|| !update.proof.signature
|| !update.proof.signer
) {
throw new Error("Incomplete Nest-signed oracle update");
}
if (market.signedOracleFeedId && update.feedId !== market.signedOracleFeedId) {
throw new Error("Signed oracle feed mismatch");
}
const message = Buffer.from(update.proof.payload, "base64");
const signature = Buffer.from(update.proof.signature, "base64");
const signer = new PublicKey(update.proof.signer);
const [nestPriceSigner] = PublicKey.findProgramAddressSync(
[Buffer.from("nest_price_signer"), protocol.toBuffer()],
coreProgram,
);
const verify = signedEd25519Instruction(message, signature, signer);
const refresh = new TransactionInstruction({
programId: coreProgram,
keys: [
{ pubkey: protocol, isSigner: false, isWritable: false },
{ pubkey: collateralConfig, isSigner: false, isWritable: false },
{ pubkey: nestPriceSigner, isSigner: false, isWritable: false },
{ pubkey: oracle, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
],
data: signedRefreshData(message, ed25519Index),
});
return [...computeInstructions, verify, refresh];
}The apiKey parameter is optional. Omit it for public browser requests. When
this helper runs on your server, read the key from a private environment
variable and pass it into the helper:
import type { NestClient } from "../nest-client";
import { fetchCurrentOraclePrices } from "../nest-oracle";
export async function loadOraclePrices(client: NestClient) {
const apiKey = process.env.NEST_API_KEY;
if (!apiKey) throw new Error("NEST_API_KEY is not configured");
return fetchCurrentOraclePrices(
client.deployment.collateral,
apiKey,
);
}Never pass an API key from a NEXT_PUBLIC_ variable or serialize it into a
browser response. Contact the Nest team on Discord
to request a key or a higher rate limit.
Keep the refresh and action atomic
A refresh transaction followed by a mint or withdrawal transaction creates a race and may use stale state. Put verification, refresh, and the protected action in the same transaction and simulate that final transaction.
Wallet mutation protection
Some wallets or middleware can alter a transaction before returning it. Compare the signed transaction's instruction program IDs, account metas, and data byte for byte with the transaction you presented. Reject a mutation instead of submitting an oracle transaction whose verifier indexes no longer match.