Skip to content

Commit

Permalink
Merge pull request #67 from eq-lab/feature/budget-from-preflight
Browse files Browse the repository at this point in the history
Feature/budget from preflight
  • Loading branch information
mn13 authored Sep 21, 2023
2 parents 38c103c + b0630ab commit 9092f6a
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 48 deletions.
12 changes: 12 additions & 0 deletions integration-tests/snapshots/budget_utilization.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"borrow": {
"cpuInsns": "37580030",
"memBytes": "4027722"
}
}
{
"withdraw": {
"cpuInsns": "37578751",
"memBytes": "4031271"
}
}
96 changes: 58 additions & 38 deletions integration-tests/tests/pool.sut.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Address, Keypair, SorobanRpc } from "soroban-client";
import { SorobanClient } from "./soroban.client";
import { SendTransactionResult, SorobanClient } from "./soroban.client";
import { adminKeys, contractsFilename, setEnv, treasuryKeys } from "./soroban.config";
import {
convertToScvAddress,
Expand All @@ -14,6 +14,9 @@ import {
parseScvToJs
} from "./soroban.converter";
import { exec } from "child_process";
import * as fs from 'fs';

export const BUDGET_SNAPSHOT_FILE = 'snapshots/budget_utilization.snap';

export type SlenderAsset = "XLM" | "XRP" | "USDC";

Expand Down Expand Up @@ -89,15 +92,15 @@ export async function mintBurn(
mintsBurns: Array<MintBurn>
): Promise<void> {
for (let i = 0; i < mintsBurns.length; i++) {
const response = await client.sendTransaction(
const txResult = await client.sendTransaction(
mintsBurns[i].asset_balance.get("asset"),
mintsBurns[i].mint ? "mint" : "clawback",
adminKeys,
convertToScvAddress(mintsBurns[i].who.toString()),
convertToScvI128(mintsBurns[i].asset_balance.get("balance"))
);
)

if (response.status != "SUCCESS") {
if (txResult.response.status != "SUCCESS") {
throw Error("Failed to transfer tokens!");
}
}
Expand Down Expand Up @@ -161,9 +164,9 @@ export async function accountPosition(
export async function setPrice(
client: SorobanClient,
asset: SlenderAsset,
amount: bigint
): Promise<void> {
await client.sendTransaction(
amount: bigint,
): Promise<SendTransactionResult> {
return client.sendTransaction(
process.env.SLENDER_POOL,
"set_price",
adminKeys,
Expand Down Expand Up @@ -213,31 +216,32 @@ export async function borrow(
client: SorobanClient,
signer: Keypair,
asset: SlenderAsset,
amount: bigint
): Promise<void> {
const response = await client.sendTransaction(
amount: bigint,
): Promise<SendTransactionResult> {
const txResult = await client.sendTransaction(
process.env.SLENDER_POOL,
"borrow",
signer,
convertToScvAddress(signer.publicKey()),
convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]),
convertToScvI128(amount)
);

const result = parseMetaXdrToJs<Array<MintBurn>>(
response.resultMetaXdr
);

const result = parseMetaXdrToJs<Array<MintBurn>>(txResult.response.resultMetaXdr);

await mintBurn(client, result);

return txResult;
}

export async function deposit(
client: SorobanClient,
signer: Keypair,
asset: SlenderAsset,
amount: bigint
): Promise<void> {
const response = await client.sendTransaction(
amount: bigint,
withBudget = false,
): Promise<SendTransactionResult> {
const txResult = await client.sendTransaction(
process.env.SLENDER_POOL,
"deposit",
signer,
Expand All @@ -246,20 +250,21 @@ export async function deposit(
convertToScvI128(amount)
);

const result = parseMetaXdrToJs<Array<MintBurn>>(
response.resultMetaXdr
);
const result = parseMetaXdrToJs<Array<MintBurn>>(txResult.response.resultMetaXdr);

await mintBurn(client, result);

return txResult;
}

export async function repay(
client: SorobanClient,
signer: Keypair,
asset: SlenderAsset,
amount: bigint
): Promise<void> {
const response = await client.sendTransaction(
amount: bigint,
withBudget = false,
): Promise<SendTransactionResult> {
const txResult = await client.sendTransaction(
process.env.SLENDER_POOL,
"repay",
signer,
Expand All @@ -269,19 +274,22 @@ export async function repay(
);

const result = parseMetaXdrToJs<Array<MintBurn>>(
response.resultMetaXdr
txResult.response.resultMetaXdr
);

await mintBurn(client, result);

return txResult;
}

export async function withdraw(
client: SorobanClient,
signer: Keypair,
asset: SlenderAsset,
amount: bigint
): Promise<void> {
const response = await client.sendTransaction(
amount: bigint,
withBudget = false,
): Promise<SendTransactionResult> {
const txResult = await client.sendTransaction(
process.env.SLENDER_POOL,
"withdraw",
signer,
Expand All @@ -292,19 +300,22 @@ export async function withdraw(
);

const result = parseMetaXdrToJs<Array<MintBurn>>(
response.resultMetaXdr
txResult.response.resultMetaXdr
);

await mintBurn(client, result);

return txResult;
}

export async function liquidate(
client: SorobanClient,
signer: Keypair,
who: string,
receiveStoken: boolean
): Promise<void> {
const response = await client.sendTransaction(
receiveStoken: boolean,
withBudget = false,
): Promise<SendTransactionResult> {
const txResult = await client.sendTransaction(
process.env.SLENDER_POOL,
"liquidate",
signer,
Expand All @@ -314,10 +325,12 @@ export async function liquidate(
);

const result = parseMetaXdrToJs<Array<MintBurn>>(
response.resultMetaXdr
txResult.response.resultMetaXdr
);

await mintBurn(client, result);

return txResult;
}

export async function collatCoeff(
Expand All @@ -338,9 +351,10 @@ export async function transferStoken(
asset: SlenderAsset,
signer: Keypair,
to: string,
amount: bigint
): Promise<void> {
await client.sendTransaction(
amount: bigint,
withBudget = false,
): Promise<SendTransactionResult> {
return client.sendTransaction(
process.env[`SLENDER_S_TOKEN_${asset}`],
"transfer",
signer,
Expand Down Expand Up @@ -398,9 +412,15 @@ export async function finalizeTransfer(
);
}

export function writeBudgetSnapshot(name: string, transactionResult: SendTransactionResult) {
if (transactionResult.cost !== null && transactionResult.cost !== undefined) {
fs.writeFileSync(BUDGET_SNAPSHOT_FILE, `${JSON.stringify({ [name]: transactionResult.cost }, null, 2)}\n`, { flag: 'a' });
}
}

async function initContract<T>(
name: string,
callback: () => Promise<SorobanRpc.GetTransactionResponse>,
callback: () => Promise<SendTransactionResult>,
success: (result: T) => string = undefined
): Promise<void> {
name = `SLENDER_${name}`;
Expand All @@ -410,8 +430,8 @@ async function initContract<T>(

const result = await callback();

if (result.status == "SUCCESS") {
setEnv(name, success && success(parseMetaXdrToJs(result.resultMetaXdr)) || "TRUE");
if (result.response.status == "SUCCESS") {
setEnv(name, success && success(parseMetaXdrToJs(result.response.resultMetaXdr)) || "TRUE");
} else {
throw Error(`Transaction failed: ${name} ${JSON.stringify(result)}`);
}
Expand Down
16 changes: 14 additions & 2 deletions integration-tests/tests/pool/6.tx.budget.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { SorobanClient } from "../soroban.client";
import {
BUDGET_SNAPSHOT_FILE,
borrow,
cleanSlenderEnvKeys,
deploy,
deposit,
init,
mintUnderlyingTo,
withdraw,
writeBudgetSnapshot,
} from "../pool.sut";
import {
borrower1Keys,
Expand All @@ -15,6 +17,8 @@ import {
} from "../soroban.config";
import { expect, use } from "chai";
import chaiAsPromised from 'chai-as-promised';
import * as fs from 'fs';

use(chaiAsPromised);

describe("LendingPool: methods must not exceed CPU/MEM limits", function () {
Expand Down Expand Up @@ -59,15 +63,23 @@ describe("LendingPool: methods must not exceed CPU/MEM limits", function () {
await deposit(client, borrower2Keys, "USDC", 20_000_000_000n);
await borrow(client, borrower2Keys, "XLM", 6_000_000_000n);
await borrow(client, borrower2Keys, "XRP", 5_900_000_000n);

fs.unlinkSync(BUDGET_SNAPSHOT_FILE);
});

it("Case 1: borrow()", async function () {
// Borrower1 borrows 20_000_000 USDC
await expect(borrow(client, borrower1Keys, "USDC", 20_000_000n)).to.not.eventually.rejected;
await expect(
borrow(client, borrower1Keys, "USDC", 20_000_000n)
.then((result) => writeBudgetSnapshot("borrow", result)) // TODO: method name
).to.not.eventually.rejected;
});

it("Case 2: withdraw full", async function () {
// Borrower1 witdraws all XLM
await expect(withdraw(client, borrower1Keys, "XLM", 170_141_183_460_469_231_731_687_303_715_884_105_727n)).to.not.eventually.rejected; // i128::MAX
await expect(
withdraw(client, borrower1Keys, "XLM", 170_141_183_460_469_231_731_687_303_715_884_105_727n) // i128::MAX
.then((result) => writeBudgetSnapshot("withdraw", result))
).to.not.eventually.rejected;
});
});
32 changes: 24 additions & 8 deletions integration-tests/tests/soroban.client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { Server, Contract, TimeoutInfinite, TransactionBuilder, Keypair, xdr, SorobanRpc } from "soroban-client";
import { Server, Contract, TimeoutInfinite, TransactionBuilder, Keypair, xdr, SorobanRpc, BASE_FEE, assembleTransaction } from "soroban-client";
import { promisify } from "util";
import "./soroban.config";
import { adminKeys } from "./soroban.config";

export class SendTransactionResult {
response: SorobanRpc.GetSuccessfulTransactionResponse;
cost?: SorobanRpc.Cost

constructor(response: SorobanRpc.GetSuccessfulTransactionResponse, cost?: SorobanRpc.Cost) {
this.response = response;
this.cost = cost;
}
}

export class SorobanClient {
client: Server;

Expand All @@ -24,21 +34,27 @@ export class SorobanClient {
method: string,
signer: Keypair,
...args: xdr.ScVal[]
): Promise<SorobanRpc.GetSuccessfulTransactionResponse> {
): Promise<SendTransactionResult> {
const source = await this.client.getAccount(signer.publicKey());
const contract = new Contract(contractId);

const operation = new TransactionBuilder(source, {
fee: "100",
fee: BASE_FEE,
networkPassphrase: process.env.PASSPHRASE,
}).addOperation(contract.call(method, ...args || []))
.setTimeout(TimeoutInfinite)
.build();

const simulated = await this.client.simulateTransaction(operation);

const transaction = await this.client.prepareTransaction(
operation,
process.env.PASSPHRASE);
if (SorobanRpc.isSimulationError(simulated)) {
throw new Error(simulated.error);
} else if (!simulated.result) {
throw new Error(`invalid simulation: no result in ${simulated}`);
}

const transaction = assembleTransaction(operation, process.env.PASSPHRASE, simulated).build()

transaction.sign(signer);

const response = await this.client.sendTransaction(transaction);
Expand All @@ -64,10 +80,10 @@ export class SorobanClient {
const getResult = result as SorobanRpc.GetTransactionResponse;
if (getResult.status !== SorobanRpc.GetTransactionStatus.SUCCESS) {
console.error('Transaction submission failed! Returning full RPC response.');
return result;
return new SendTransactionResult(result, simulated.cost);
}

return result;
return new SendTransactionResult(result, simulated.cost);
}

throw Error(`Transaction failed (method: ${method})`);
Expand Down

0 comments on commit 9092f6a

Please sign in to comment.