Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blend-capital/blend-sdk",
"version": "3.0.1",
"version": "3.1.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is mostly for the minor version change in stellar-sdk

"description": "Javascript SDK for the Blend Protocol",
"type": "module",
"scripts": {
Expand All @@ -18,7 +18,7 @@
],
"publishConfig": {
"access": "public",
"tag": "beta"
"tag": "latest"
},
"repository": {
"type": "git",
Expand All @@ -43,7 +43,7 @@
},
"dependencies": {
"buffer": "6.0.3",
"@stellar/stellar-sdk": "13.2.0",
"@stellar/stellar-sdk": "13.3.0",
"follow-redirects": ">=1.15.6"
}
}
138 changes: 138 additions & 0 deletions src/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,41 @@ import {
Account,
Address,
Contract,
FeeBumpTransaction,
Networks,
rpc,
scValToNative,
Transaction,
TransactionBuilder,
xdr,
} from '@stellar/stellar-sdk';
import { Network } from './index.js';

const REFLECTOR_ORACLE_ADDRESSES = [
'CBKGPWGKSKZF52CFHMTRR23TBWTPMRDIYZ4O2P5VS65BMHYH4DXMCJZC',
'CALI2BYU2JE6WVRUFYTS6MSBNEHGJ35P4AVCZYF3B6QOE3QKOB2PLE6M',
'CAFJZQWSED6YAWZU3GWRTOCNPPCGBN32L7QV43XX5LZLFTK6JLN34DLN',
];

export interface PriceData {
/**
* The price as a fixed point number with the oracle's decimals.
*/
price: bigint;
/**
* The timestamp of the price in seconds
*/
timestamp: number;
}

/**
* Fetch the `lastprice` from an oracle contract for the given token.
* @param network - The network to use
* @param oracle_id - The oracle contract ID
* @param token_id - The token contract ID to fetch the price for
* @returns The PriceData
* @throws Will throw an error if `None` is returned or if the simulation fails.
*/
export async function getOraclePrice(
network: Network,
oracle_id: string,
Expand Down Expand Up @@ -52,6 +75,13 @@ export async function getOraclePrice(
}
}

/**
* Fetch the `decimals` from an oracle contract.
* @param network - The network to use
* @param oracle_id - The oracle contract ID
* @returns The decimals and latest ledger number
* @throws Will throw an error if the simulation fails.
*/
export async function getOracleDecimals(
network: Network,
oracle_id: string
Expand All @@ -76,3 +106,111 @@ export async function getOracleDecimals(
throw new Error(`Failed to fetch oralce decimals: ${result.error}`);
}
}

/**
* Add future Reflector oracle entries to the read-only footprint of a transaction.
* This ensures that if a future oracle round occurs before the transaction is executed,
* the future oracle round will still be included in the footprint.
*
* This only works for Reflector based oracles as it makes assumptions based on how the
* oracle contracts keys are structured.
*
* If more than 100 entries are added to the read-only footprint, it will stop adding. The priority
* is given to the oracle contracts seen first, and the indexes for the contract that are seen first.
*
* @param tx - The transaction XDR to add the reflector entries to.
* @returns A base-64 transaction XDR string with the reflector entries added to the read-only footprint.
*/
export function addReflectorEntries(txXdr: string): string {
// network passphrase not relevant as TX XDR is returned, which
// does not include a network passphrase.
const tx = TransactionBuilder.fromXDR(txXdr, Networks.PUBLIC);
if (tx instanceof FeeBumpTransaction) {
return tx.toXDR();
}

const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData();
const readEntries = sorobanData.resources().footprint().readOnly();
const readWriteEntries = sorobanData.resources().footprint().readWrite();
let bytes_read = sorobanData.resources().readBytes();
// Key: the reflector oracle contract address
// Value: a map of index to the most recent timestamp for that index
const mostRecentEntries: Map<string, Map<bigint, bigint>> = new Map();
const newReadEntries = [];
for (const entry of readEntries) {
switch (entry.switch()) {
case xdr.LedgerEntryType.contractData(): {
const contractData = entry.contractData();
const address = Address.fromScAddress(contractData.contract()).toString();

if (REFLECTOR_ORACLE_ADDRESSES.includes(address)) {
switch (contractData.key().switch()) {
case xdr.ScValType.scvU128(): {
const u128Key = contractData.key().u128();
const roundTimestamp = u128Key.hi().toBigInt();
const index = u128Key.lo().toBigInt();
if (
!mostRecentEntries.has(Address.fromScAddress(contractData.contract()).toString())
) {
mostRecentEntries.set(
Address.fromScAddress(contractData.contract()).toString(),
new Map()
);
}
const contractEntries = mostRecentEntries.get(
Address.fromScAddress(contractData.contract()).toString()
);

if (contractEntries.has(index)) {
const mostRecentTimestamp = contractEntries.get(index);
if (mostRecentTimestamp < roundTimestamp) {
contractEntries.set(index, roundTimestamp);
}
} else {
contractEntries.set(index, roundTimestamp);
}
}
}
}
break;
}
}
}

for (const [contract, entries] of mostRecentEntries.entries()) {
for (const [index, roundTimestamp] of entries.entries()) {
if (readWriteEntries.length + readEntries.length + newReadEntries.length + 1 > 100) {
break;
}
// Create a new entry for the reflector oracle
const newRoundTimestamp = roundTimestamp + 300_000n;
newReadEntries.push(
xdr.LedgerKey.contractData(
new xdr.LedgerKeyContractData({
contract: Address.fromString(contract).toScAddress(),
key: xdr.ScVal.scvU128(
new xdr.UInt128Parts({
hi: xdr.Uint64.fromString(newRoundTimestamp.toString()),
lo: xdr.Uint64.fromString(index.toString()),
})
),
durability: xdr.ContractDataDurability.temporary(),
})
)
);
// Reading the additional ledger key+entry adds 96 bytes to the bytes_read count.
// Add 100 bytes to be safe.
bytes_read += 100;
}
}

sorobanData
.resources()
.footprint()
.readOnly([...readEntries, ...newReadEntries]);
sorobanData.resources().readBytes(bytes_read);
return TransactionBuilder.cloneFrom(tx, {
sorobanData: sorobanData,
fee: tx.fee,
}).build().toXDR();
}
Loading