diff --git a/integration-tests/snapshots/budget_utilization.snap b/integration-tests/snapshots/budget_utilization.snap new file mode 100644 index 00000000..88f29625 --- /dev/null +++ b/integration-tests/snapshots/budget_utilization.snap @@ -0,0 +1,12 @@ +{ + "borrow": { + "cpuInsns": "37580030", + "memBytes": "4027722" + } +} +{ + "withdraw": { + "cpuInsns": "37578751", + "memBytes": "4031271" + } +} diff --git a/integration-tests/tests/pool.sut.ts b/integration-tests/tests/pool.sut.ts index 46bcc832..0c7f85ee 100644 --- a/integration-tests/tests/pool.sut.ts +++ b/integration-tests/tests/pool.sut.ts @@ -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, @@ -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"; @@ -89,15 +92,15 @@ export async function mintBurn( mintsBurns: Array ): Promise { 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!"); } } @@ -161,9 +164,9 @@ export async function accountPosition( export async function setPrice( client: SorobanClient, asset: SlenderAsset, - amount: bigint -): Promise { - await client.sendTransaction( + amount: bigint, +): Promise { + return client.sendTransaction( process.env.SLENDER_POOL, "set_price", adminKeys, @@ -213,9 +216,9 @@ export async function borrow( client: SorobanClient, signer: Keypair, asset: SlenderAsset, - amount: bigint -): Promise { - const response = await client.sendTransaction( + amount: bigint, +): Promise { + const txResult = await client.sendTransaction( process.env.SLENDER_POOL, "borrow", signer, @@ -223,21 +226,22 @@ export async function borrow( convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvI128(amount) ); - - const result = parseMetaXdrToJs>( - response.resultMetaXdr - ); + + const result = parseMetaXdrToJs>(txResult.response.resultMetaXdr); await mintBurn(client, result); + + return txResult; } export async function deposit( client: SorobanClient, signer: Keypair, asset: SlenderAsset, - amount: bigint -): Promise { - const response = await client.sendTransaction( + amount: bigint, + withBudget = false, +): Promise { + const txResult = await client.sendTransaction( process.env.SLENDER_POOL, "deposit", signer, @@ -246,20 +250,21 @@ export async function deposit( convertToScvI128(amount) ); - const result = parseMetaXdrToJs>( - response.resultMetaXdr - ); + const result = parseMetaXdrToJs>(txResult.response.resultMetaXdr); await mintBurn(client, result); + + return txResult; } export async function repay( client: SorobanClient, signer: Keypair, asset: SlenderAsset, - amount: bigint -): Promise { - const response = await client.sendTransaction( + amount: bigint, + withBudget = false, +): Promise { + const txResult = await client.sendTransaction( process.env.SLENDER_POOL, "repay", signer, @@ -269,19 +274,22 @@ export async function repay( ); const result = parseMetaXdrToJs>( - response.resultMetaXdr + txResult.response.resultMetaXdr ); await mintBurn(client, result); + + return txResult; } export async function withdraw( client: SorobanClient, signer: Keypair, asset: SlenderAsset, - amount: bigint -): Promise { - const response = await client.sendTransaction( + amount: bigint, + withBudget = false, +): Promise { + const txResult = await client.sendTransaction( process.env.SLENDER_POOL, "withdraw", signer, @@ -292,19 +300,22 @@ export async function withdraw( ); const result = parseMetaXdrToJs>( - response.resultMetaXdr + txResult.response.resultMetaXdr ); await mintBurn(client, result); + + return txResult; } export async function liquidate( client: SorobanClient, signer: Keypair, who: string, - receiveStoken: boolean -): Promise { - const response = await client.sendTransaction( + receiveStoken: boolean, + withBudget = false, +): Promise { + const txResult = await client.sendTransaction( process.env.SLENDER_POOL, "liquidate", signer, @@ -314,10 +325,12 @@ export async function liquidate( ); const result = parseMetaXdrToJs>( - response.resultMetaXdr + txResult.response.resultMetaXdr ); await mintBurn(client, result); + + return txResult; } export async function collatCoeff( @@ -338,9 +351,10 @@ export async function transferStoken( asset: SlenderAsset, signer: Keypair, to: string, - amount: bigint -): Promise { - await client.sendTransaction( + amount: bigint, + withBudget = false, +): Promise { + return client.sendTransaction( process.env[`SLENDER_S_TOKEN_${asset}`], "transfer", signer, @@ -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( name: string, - callback: () => Promise, + callback: () => Promise, success: (result: T) => string = undefined ): Promise { name = `SLENDER_${name}`; @@ -410,8 +430,8 @@ async function initContract( 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)}`); } diff --git a/integration-tests/tests/pool/6.tx.budget.spec.ts b/integration-tests/tests/pool/6.tx.budget.spec.ts index 898fd049..e1e5b64f 100644 --- a/integration-tests/tests/pool/6.tx.budget.spec.ts +++ b/integration-tests/tests/pool/6.tx.budget.spec.ts @@ -1,5 +1,6 @@ import { SorobanClient } from "../soroban.client"; import { + BUDGET_SNAPSHOT_FILE, borrow, cleanSlenderEnvKeys, deploy, @@ -7,6 +8,7 @@ import { init, mintUnderlyingTo, withdraw, + writeBudgetSnapshot, } from "../pool.sut"; import { borrower1Keys, @@ -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 () { @@ -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; }); }); diff --git a/integration-tests/tests/soroban.client.ts b/integration-tests/tests/soroban.client.ts index 377472f3..871dc6e5 100644 --- a/integration-tests/tests/soroban.client.ts +++ b/integration-tests/tests/soroban.client.ts @@ -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; @@ -24,21 +34,27 @@ export class SorobanClient { method: string, signer: Keypair, ...args: xdr.ScVal[] - ): Promise { + ): Promise { 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); @@ -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})`);