From 69acf809a453f4788c803af939391b4c65b219f3 Mon Sep 17 00:00:00 2001 From: Maks Nabokov Date: Thu, 21 Sep 2023 09:57:29 +0300 Subject: [PATCH] budget from preflight --- .../artifacts/budget_utilization.snap | 12 ++ integration-tests/tests/pool.sut.ts | 114 ++++++++++++------ .../tests/pool/6.tx.budget.spec.ts | 16 ++- integration-tests/tests/soroban.client.ts | 27 +++-- 4 files changed, 125 insertions(+), 44 deletions(-) create mode 100644 integration-tests/artifacts/budget_utilization.snap diff --git a/integration-tests/artifacts/budget_utilization.snap b/integration-tests/artifacts/budget_utilization.snap new file mode 100644 index 00000000..cf3ec8f5 --- /dev/null +++ b/integration-tests/artifacts/budget_utilization.snap @@ -0,0 +1,12 @@ +{ + "borrow": { + "cpuInsns": "37577012", + "memBytes": "4027722" + } +} +{ + "withdraw": { + "cpuInsns": "37631489", + "memBytes": "4031271" + } +} diff --git a/integration-tests/tests/pool.sut.ts b/integration-tests/tests/pool.sut.ts index 46bcc832..5ca74f14 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 = 'artifacts/budget_utilization.snap'; export type SlenderAsset = "XLM" | "XRP" | "USDC"; @@ -78,9 +81,10 @@ export async function mintUnderlyingTo( process.env[`SLENDER_TOKEN_${asset}`], "mint", adminKeys, + false, convertToScvAddress(to), convertToScvI128(amount) - ) + ) as Promise ); } @@ -93,9 +97,10 @@ export async function mintBurn( mintsBurns[i].asset_balance.get("asset"), mintsBurns[i].mint ? "mint" : "clawback", adminKeys, + false, convertToScvAddress(mintsBurns[i].who.toString()), convertToScvI128(mintsBurns[i].asset_balance.get("balance")) - ); + ) as SorobanRpc.GetTransactionResponse; if (response.status != "SUCCESS") { throw Error("Failed to transfer tokens!"); @@ -161,12 +166,14 @@ export async function accountPosition( export async function setPrice( client: SorobanClient, asset: SlenderAsset, - amount: bigint -): Promise { - await client.sendTransaction( + amount: bigint, + withBudget = false +): Promise { + return client.sendTransaction( process.env.SLENDER_POOL, "set_price", adminKeys, + withBudget, convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvI128(amount) ); @@ -213,78 +220,92 @@ export async function borrow( client: SorobanClient, signer: Keypair, asset: SlenderAsset, - amount: bigint -): Promise { + amount: bigint, + withBudget = false, +): Promise { const response = await client.sendTransaction( process.env.SLENDER_POOL, "borrow", signer, + withBudget, convertToScvAddress(signer.publicKey()), convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvI128(amount) ); - + const result = parseMetaXdrToJs>( - response.resultMetaXdr + withBudget ? response[0].resultMetaXdr : (response as SorobanRpc.GetSuccessfulTransactionResponse).resultMetaXdr ); await mintBurn(client, result); + + return response; } export async function deposit( client: SorobanClient, signer: Keypair, asset: SlenderAsset, - amount: bigint -): Promise { + amount: bigint, + withBudget = false, +): Promise { const response = await client.sendTransaction( process.env.SLENDER_POOL, "deposit", signer, + withBudget, convertToScvAddress(signer.publicKey()), convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvI128(amount) ); const result = parseMetaXdrToJs>( - response.resultMetaXdr + withBudget ? response[0].resultMetaXdr : (response as SorobanRpc.GetSuccessfulTransactionResponse).resultMetaXdr ); await mintBurn(client, result); + + return response; } export async function repay( client: SorobanClient, signer: Keypair, asset: SlenderAsset, - amount: bigint -): Promise { + amount: bigint, + withBudget = false, +): Promise { const response = await client.sendTransaction( process.env.SLENDER_POOL, "repay", signer, + withBudget, convertToScvAddress(signer.publicKey()), convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvI128(amount) ); const result = parseMetaXdrToJs>( - response.resultMetaXdr + withBudget ? response[0].resultMetaXdr : (response as SorobanRpc.GetSuccessfulTransactionResponse).resultMetaXdr ); await mintBurn(client, result); + + return response; } export async function withdraw( client: SorobanClient, signer: Keypair, asset: SlenderAsset, - amount: bigint -): Promise { + amount: bigint, + withBudget = false, +): Promise { const response = await client.sendTransaction( process.env.SLENDER_POOL, "withdraw", signer, + withBudget, convertToScvAddress(signer.publicKey()), convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvI128(amount), @@ -292,32 +313,38 @@ export async function withdraw( ); const result = parseMetaXdrToJs>( - response.resultMetaXdr + withBudget ? response[0].resultMetaXdr : (response as SorobanRpc.GetSuccessfulTransactionResponse).resultMetaXdr ); await mintBurn(client, result); + + return response; } export async function liquidate( client: SorobanClient, signer: Keypair, who: string, - receiveStoken: boolean -): Promise { + receiveStoken: boolean, + withBudget = false, +): Promise { const response = await client.sendTransaction( process.env.SLENDER_POOL, "liquidate", signer, + withBudget, convertToScvAddress(signer.publicKey()), convertToScvAddress(who), convertToScvBool(receiveStoken) ); const result = parseMetaXdrToJs>( - response.resultMetaXdr + withBudget ? response[0].resultMetaXdr : (response as SorobanRpc.GetSuccessfulTransactionResponse).resultMetaXdr ); await mintBurn(client, result); + + return response; } export async function collatCoeff( @@ -338,12 +365,14 @@ 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, + withBudget, convertToScvAddress(signer.publicKey()), convertToScvAddress(to), convertToScvI128(amount) @@ -388,6 +417,7 @@ export async function finalizeTransfer( process.env["SLENDER_POOL"], "finalize_transfer", signer, + false, convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvAddress(signer.publicKey()), convertToScvAddress(to), @@ -398,6 +428,13 @@ export async function finalizeTransfer( ); } +export function writeBudgetSnapshot(name: string, transactionResult: SendTransactionResult) { + if (Array.isArray(transactionResult)) { + const budget = transactionResult[1]; + fs.writeFileSync(BUDGET_SNAPSHOT_FILE, `${JSON.stringify({ [name]: budget }, null, 2)}\n`, { flag: 'a' }); + } +} + async function initContract( name: string, callback: () => Promise, @@ -424,11 +461,12 @@ async function initToken(client: SorobanClient, asset: SlenderAsset, name: strin process.env[`SLENDER_TOKEN_${asset}`], "initialize", adminKeys, + false, convertToScvAddress(adminKeys.publicKey()), convertToScvU32(9), convertToScvString(name), convertToScvString(asset) - ) + ) as Promise ); } @@ -439,13 +477,14 @@ async function initSToken(client: SorobanClient, asset: SlenderAsset, salt: stri process.env.SLENDER_DEPLOYER, "deploy_s_token", adminKeys, + false, convertToScvBytes(salt, "hex"), convertToScvBytes(process.env.SLENDER_S_TOKEN_HASH, "hex"), convertToScvString(`SToken ${asset}`), convertToScvString(`S${asset}`), convertToScvAddress(process.env.SLENDER_POOL), convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), - ), + ) as Promise, result => result[0] ); } @@ -457,13 +496,14 @@ async function initDToken(client: SorobanClient, asset: SlenderAsset, salt: stri process.env.SLENDER_DEPLOYER, "deploy_debt_token", adminKeys, + false, convertToScvBytes(salt, "hex"), convertToScvBytes(process.env.SLENDER_DEBT_TOKEN_HASH, "hex"), convertToScvString(`DToken ${asset}`), convertToScvString(`D${asset}`), convertToScvAddress(process.env.SLENDER_POOL), convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), - ), + ) as Promise, result => result[0]); } @@ -474,6 +514,7 @@ async function initPool(client: SorobanClient, salt: string): Promise { process.env.SLENDER_DEPLOYER, "deploy_pool", adminKeys, + false, convertToScvBytes(salt, "hex"), convertToScvBytes(process.env.SLENDER_POOL_HASH, "hex"), convertToScvAddress(adminKeys.publicKey()), @@ -485,7 +526,7 @@ async function initPool(client: SorobanClient, salt: string): Promise { "max_rate": convertToScvU32(50_000), "scaling_coeff": convertToScvU32(9_000) }) - ), + ) as Promise, result => result[0] ); } @@ -497,13 +538,14 @@ async function initPoolReserve(client: SorobanClient, asset: SlenderAsset, decim process.env.SLENDER_POOL, "init_reserve", adminKeys, + false, convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvMap({ "debt_token_address": convertToScvAddress(process.env[`SLENDER_DEBT_TOKEN_${asset}`]), // "decimals": convertToScvU32(9), "s_token_address": convertToScvAddress(process.env[`SLENDER_S_TOKEN_${asset}`]), }) - ) + ) as Promise ); } @@ -514,6 +556,7 @@ async function initPoolCollateral(client: SorobanClient, asset: SlenderAsset): P process.env.SLENDER_POOL, "configure_as_collateral", adminKeys, + false, convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvMap({ "discount": convertToScvU32(6000), @@ -521,7 +564,7 @@ async function initPoolCollateral(client: SorobanClient, asset: SlenderAsset): P "liq_cap": convertToScvI128(1000000000000000n), "util_cap": convertToScvU32(9000) }) - ) + ) as Promise ); } @@ -532,9 +575,10 @@ async function initPoolPriceFeed(client: SorobanClient, feed: string, assets: st process.env.SLENDER_POOL, "set_price_feed", adminKeys, + false, convertToScvAddress(feed), convertToScvVec(assets.map(asset => convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]))) - ) + ) as Promise ); } @@ -545,9 +589,10 @@ async function initPoolBorrowing(client: SorobanClient, asset: SlenderAsset): Pr process.env.SLENDER_POOL, "enable_borrowing_on_reserve", adminKeys, + false, convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvBool(true) - ) + ) as Promise ); } @@ -558,8 +603,9 @@ async function initBaseAsset(client: SorobanClient, asset: SlenderAsset): Promis process.env.SLENDER_POOL, "set_base_asset", adminKeys, + false, convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), convertToScvBool(true) - ) + ) as Promise ); } diff --git a/integration-tests/tests/pool/6.tx.budget.spec.ts b/integration-tests/tests/pool/6.tx.budget.spec.ts index 898fd049..e8151a5f 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, true) + .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, true) // 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..07591df8 100644 --- a/integration-tests/tests/soroban.client.ts +++ b/integration-tests/tests/soroban.client.ts @@ -1,8 +1,12 @@ -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 type SendTransactionResult = + SorobanRpc.GetSuccessfulTransactionResponse | + [SorobanRpc.GetSuccessfulTransactionResponse, SorobanRpc.Cost]; + export class SorobanClient { client: Server; @@ -23,22 +27,29 @@ export class SorobanClient { contractId: string, method: string, signer: Keypair, + withBudget: boolean, ...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 +75,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 withBudget ? [result, simulated.cost] : result; } - return result; + return withBudget ? [result, simulated.cost] : result; } throw Error(`Transaction failed (method: ${method})`);