Skip to content
Open
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
98 changes: 97 additions & 1 deletion src/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* StreamsModule - all DripStream + DripFactory operations.
*/

import { SorobanRpc, nativeToScVal, xdr, Address, Transaction } from '@stellar/stellar-sdk';
import { SorobanRpc, nativeToScVal, xdr, Address, Transaction, BASE_FEE } from '@stellar/stellar-sdk';
import type { Signer } from './signer.js';
import type {
ConduitConfig,
Expand All @@ -15,6 +15,8 @@ import type {
Subscription,
BatchWithdrawItem,
BatchWithdrawResult,
StreamOperation,
FeeEstimate,
} from './types/index.js';
import type { WalletAdapter } from './adapters/types.js';
import { KeypairWalletAdapter } from './adapters/keypair.js';
Expand Down Expand Up @@ -417,6 +419,100 @@ export class StreamsModule {
};
}

/**
* Estimate the network fee for a given stream operation by running a
* Soroban simulation. Returns the resource fee (CPU/RAM), base fee, and
* total estimated fee in stroops.
*/
async estimateFee(operation: StreamOperation): Promise<FeeEstimate> {
const callerAddr = await this._resolveCallerAddress();
const server = this._server();

let tx: Transaction;

switch (operation.type) {
case 'create': {
const decimals = await getTokenDecimals(this.rpcUrl, this.passphrase, callerAddr, operation.token);
const depositStroops = toStroops(operation.depositAmount, decimals);
const rateStroops = operation.ratePerSecond
? BigInt(operation.ratePerSecond)
: calculateRate(operation.depositAmount, operation.durationSeconds!, decimals);
const start = operation.startTime ?? Math.floor(Date.now() / 1000);
const end = operation.durationSeconds ? start + operation.durationSeconds : 0;

const args = [
new Address(callerAddr).toScVal(),
new Address(operation.recipient).toScVal(),
new Address(operation.token).toScVal(),
nativeToScVal(depositStroops, { type: 'i128' }),
nativeToScVal(rateStroops, { type: 'i128' }),
nativeToScVal(start, { type: 'u64' }),
nativeToScVal(end, { type: 'u64' }),
boolToScVal(operation.clawbackEnabled ?? false),
];

tx = await buildContractCallTx(this.rpcUrl, this.passphrase, callerAddr, this.config.factoryAddress ?? '', 'create_stream', args);
break;
}
case 'withdraw': {
const addr = await this._resolveAddr(BigInt(operation.streamId));
const qty = operation.amount ?? 0n;
tx = await buildContractCallTx(this.rpcUrl, this.passphrase, callerAddr, addr, 'withdraw', [
nativeToScVal(qty, { type: 'i128' }),
]);
break;
}
case 'cancel': {
const addr = await this._resolveAddr(BigInt(operation.streamId));
tx = await buildContractCallTx(this.rpcUrl, this.passphrase, callerAddr, addr, 'cancel', []);
break;
}
case 'pause': {
const addr = await this._resolveAddr(BigInt(operation.streamId));
tx = await buildContractCallTx(this.rpcUrl, this.passphrase, callerAddr, addr, 'pause', []);
break;
}
case 'resume': {
const addr = await this._resolveAddr(BigInt(operation.streamId));
tx = await buildContractCallTx(this.rpcUrl, this.passphrase, callerAddr, addr, 'resume', []);
break;
}
case 'topUp': {
const addr = await this._resolveAddr(BigInt(operation.streamId));
tx = await buildContractCallTx(this.rpcUrl, this.passphrase, callerAddr, addr, 'top_up', [
nativeToScVal(operation.amount, { type: 'i128' }),
]);
break;
}
case 'clawback': {
const addr = await this._resolveAddr(BigInt(operation.streamId));
tx = await buildContractCallTx(this.rpcUrl, this.passphrase, callerAddr, addr, 'clawback', []);
break;
}
}

let simResult;
try {
simResult = await server.simulateTransaction(tx);
} catch (err) {
throw RateLimitError.fromRpcError(err) ?? err;
}

if (SorobanRpc.Api.isSimulationError(simResult)) {
throw new Error(`Simulation failed: ${simResult.error}`);
}

const resourceFee = Number(simResult.minResourceFee);
const cpuInstructions = Number(simResult.cost.cpuInsns);

return {
totalFee: Number(BASE_FEE) + resourceFee,
resourceFee,
baseFee: Number(BASE_FEE),
instructions: cpuInstructions,
};
}

// Private helpers

private _ensureCanMutate(): void {
Expand Down
52 changes: 52 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,55 @@ export interface BatchWithdrawResult {
txHash?: string;
error?: string;
}

// -- Fee estimation ----------------------------------------------------------

export type StreamOperation =
| {
type: 'create';
token: string;
sender: string;
recipient: string;
depositAmount: string;
durationSeconds?: number;
ratePerSecond?: string;
startTime?: number;
clawbackEnabled?: boolean;
}
| {
type: 'withdraw';
streamId: bigint | string;
amount?: bigint;
}
| {
type: 'cancel';
streamId: bigint | string;
}
| {
type: 'pause';
streamId: bigint | string;
}
| {
type: 'resume';
streamId: bigint | string;
}
| {
type: 'topUp';
streamId: bigint | string;
amount: bigint;
}
| {
type: 'clawback';
streamId: bigint | string;
};

export interface FeeEstimate {
/** Total estimated fee in stroops (base + resource) */
totalFee: number;
/** Resource fee component (CPU/RAM) in stroops */
resourceFee: number;
/** Base (inclusion) fee component in stroops */
baseFee: number;
/** Estimated CPU instructions */
instructions: number;
}