Skip to content

Commit 042d420

Browse files
authored
Merge branch 'main' into issue181
2 parents 7b1f134 + 2bbcfdc commit 042d420

42 files changed

Lines changed: 8056 additions & 95 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,15 @@ name: Soroban CI
22

33
on:
44
push:
5-
branches:
6-
- main
7-
- develop
85
paths:
96
- "soroban-client/**"
107
- "soroban-contract/**"
118
- ".github/workflows/**"
129
pull_request:
13-
branches:
14-
- main
15-
- develop
1610
paths:
1711
- "soroban-client/**"
1812
- "soroban-contract/**"
13+
- ".github/workflows/**"
1914

2015
jobs:
2116
client:
@@ -170,7 +165,6 @@ jobs:
170165
- name: Check contract formatting
171166
working-directory: soroban-contract
172167
run: cargo fmt --all -- --check
173-
continue-on-error: true
174168

175169
- name: Build contracts
176170
working-directory: soroban-contract
@@ -179,15 +173,17 @@ jobs:
179173
cargo build --release --target wasm32-unknown-unknown -p tba_account
180174
cargo build --release --target wasm32-unknown-unknown
181175
176+
- name: Run contract tests
177+
working-directory: soroban-contract
178+
run: cargo test --workspace --all-targets
179+
182180
- name: Optimize contracts
183181
working-directory: soroban-contract
184182
run: soroban contract optimize --wasm target/wasm32-unknown-unknown/release/*.wasm
185-
continue-on-error: true
186183

187184
- name: Run clippy
188185
working-directory: soroban-contract
189186
run: cargo clippy --all-targets -- -D warnings
190-
continue-on-error: true
191187

192188
- name: Run contract tests with coverage
193189
working-directory: soroban-contract
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# Gas Estimation for Soroban Operations
2+
3+
This SDK provides comprehensive gas estimation capabilities, allowing users to preview transaction costs before executing complex contract operations.
4+
5+
## Features
6+
7+
- **Accurate Simulation**: Uses Soroban RPC simulation to get precise resource measurements
8+
- **Cost Breakdown**: Detailed breakdown of base fees, resource fees, and refundable portions
9+
- **Resource Metrics**: CPU instructions, read/write bytes, and storage operations
10+
- **Safety Buffer**: Configurable fee buffer multiplier to ensure transactions succeed
11+
- **Offline Estimation**: Rough estimates for air-gapped or hardware wallet flows
12+
- **Formatted Display**: Human-readable gas reports for UI display
13+
14+
## Quick Start
15+
16+
### Basic Gas Estimation
17+
18+
```typescript
19+
import { createTokenboundSdk } from '@crowdpass/tokenbound-sdk';
20+
21+
const sdk = createTokenboundSdk({
22+
horizonUrl: 'https://horizon-testnet.stellar.org',
23+
sorobanRpcUrl: 'https://soroban-testnet.stellar.org',
24+
networkPassphrase: 'Test SDF Network ; September 2015',
25+
simulationSource: 'GABC...', // Default source for simulations
26+
});
27+
28+
// Estimate gas for a contract call
29+
const gas = await sdk.estimateGas('eventManager', {
30+
contractId: 'CDP...',
31+
method: 'create_event',
32+
args: [/* ... */]
33+
}, {
34+
source: 'GABC...', // Optional: override simulation source
35+
feeBufferMultiplier: 1.3, // 30% buffer for safety
36+
});
37+
38+
console.log(gas.summary);
39+
// "Estimated gas: 0.0001234 XLM (max: 0.0001481 XLM). Resources: 50000 instructions, 1024 bytes I/O."
40+
```
41+
42+
### Formatted Display
43+
44+
```typescript
45+
import { formatGasDisplay } from '@crowdpass/tokenbound-sdk';
46+
47+
// Print detailed gas report
48+
console.log(formatGasDisplay(gas, {
49+
currency: 'XLM',
50+
decimals: 7,
51+
showResources: true,
52+
showCosts: true,
53+
}));
54+
55+
// Output:
56+
// ═══════════════════════════════════════════
57+
// GAS ESTIMATION REPORT
58+
// ═══════════════════════════════════════════
59+
//
60+
// 💰 ESTIMATED COSTS
61+
// ───────────────────────────────────────────
62+
// Base Fee: 0.0000100 XLM
63+
// Resource Fee: 0.0001134 XLM
64+
// Refundable: 0.0000100 XLM
65+
// ─────────────────────────────────────────
66+
// TOTAL: 0.0001234 XLM
67+
// Max (buffered): 0.0001481 XLM
68+
//
69+
// ⚡ RESOURCE USAGE
70+
// ───────────────────────────────────────────
71+
// Instructions: 50,000
72+
// Read Bytes: 1,024
73+
// Write Bytes: 512
74+
// ...
75+
//
76+
// ═══════════════════════════════════════════
77+
// Estimated gas: 0.0001234 XLM (max: 0.0001481 XLM). Resources: 50000 instructions, 1024 bytes I/O.
78+
// ═══════════════════════════════════════════
79+
```
80+
81+
## API Reference
82+
83+
### `sdk.estimateGas(contract, artifact, options)`
84+
85+
Estimates gas costs by simulating a transaction against the actual contract state.
86+
87+
**Parameters:**
88+
- `contract`: Contract name ('eventManager', 'ticketNft', etc.)
89+
- `artifact`: Contract call details including contractId, method, and args
90+
- `options`: Gas estimation options
91+
- `source`: Source account for simulation
92+
- `fee`: Base fee override (in stroops)
93+
- `feeBufferMultiplier`: Safety buffer multiplier (default: 1.2)
94+
- `includeRawSimulation`: Include raw RPC response (default: false)
95+
- `correlationId`: Tracing correlation ID
96+
97+
**Returns:** `GasEstimation` object with:
98+
- `costs`: Detailed cost breakdown
99+
- `baseFee`: Base transaction fee
100+
- `resourceFee`: Fee for resource consumption
101+
- `refundableFee`: Portion refunded on success
102+
- `totalFee`: Total estimated fee
103+
- `maxFee`: Maximum fee with buffer applied
104+
- `resources`: Resource usage metrics
105+
- `instructions`: CPU instructions executed
106+
- `readBytes`: Bytes read from storage
107+
- `writeBytes`: Bytes written to storage
108+
- `entryReads`: Contract entries read
109+
- `entryWrites`: Contract entries written
110+
- `transactionSizeBytes`: Transaction XDR size
111+
- `metadataSizeBytes`: Soroban metadata size
112+
- `success`: Whether simulation succeeded
113+
- `summary`: Human-readable summary string
114+
- `rawSimulation`: Raw RPC response (if requested)
115+
116+
### `formatGasDisplay(estimation, options)`
117+
118+
Formats a gas estimation into a multi-line human-readable report.
119+
120+
```typescript
121+
import { formatGasDisplay } from '@crowdpass/tokenbound-sdk';
122+
123+
const report = formatGasDisplay(gas, {
124+
currency: 'XLM', // 'XLM' or 'stroops'
125+
decimals: 7, // Decimal places for XLM
126+
showResources: true, // Include resource breakdown
127+
showCosts: true, // Include cost breakdown
128+
});
129+
```
130+
131+
### `formatGasCost(stroops, currency, decimals)`
132+
133+
Formats a single gas cost value.
134+
135+
```typescript
136+
import { formatGasCost } from '@crowdpass/tokenbound-sdk';
137+
138+
formatGasCost(1234000, 'XLM', 7); // "0.1234000 XLM"
139+
formatGasCost(1234000, 'stroops'); // "1,234,000 stroops"
140+
```
141+
142+
### `calculateRecommendedFee(gasEstimation, priority)`
143+
144+
Calculates a recommended fee based on priority level.
145+
146+
```typescript
147+
import { calculateRecommendedFee } from '@crowdpass/tokenbound-sdk';
148+
149+
const recommendedFee = calculateRecommendedFee(gas, 'high');
150+
// Returns fee with 1.5x multiplier for high priority
151+
```
152+
153+
### `checkResourceWarnings(estimation)`
154+
155+
Checks for potentially problematic resource usage.
156+
157+
```typescript
158+
import { checkResourceWarnings } from '@crowdpass/tokenbound-sdk';
159+
160+
const { hasWarnings, warnings } = checkResourceWarnings(gas);
161+
if (hasWarnings) {
162+
console.warn('Resource concerns:', warnings);
163+
// ["High instruction count (150,000,000). May exceed network limits."]
164+
}
165+
```
166+
167+
### `compareGasEstimations(before, after)`
168+
169+
Compares two gas estimations to show the impact of changes.
170+
171+
```typescript
172+
import { compareGasEstimations } from '@crowdpass/tokenbound-sdk';
173+
174+
const comparison = compareGasEstimations(oldGas, newGas);
175+
console.log(comparison.summary);
176+
// "Gas cost increase: 0.0000500 XLM (500000 stroops)"
177+
```
178+
179+
## Offline Gas Estimation
180+
181+
For air-gapped or hardware wallet flows where network access isn't available:
182+
183+
```typescript
184+
import { OfflineTransactionBuilder } from '@crowdpass/tokenbound-sdk';
185+
186+
const builder = new OfflineTransactionBuilder('Test SDF Network ; September 2015');
187+
188+
const account = {
189+
accountId: 'GABC...',
190+
sequenceNumber: '123456789',
191+
};
192+
193+
const artifact = {
194+
contractId: 'CDP...',
195+
method: 'create_event',
196+
args: [/* ... */],
197+
};
198+
199+
// Rough estimate without network
200+
const estimate = builder.estimateGasOffline(account, artifact, 100, 1.2);
201+
202+
console.log(estimate.summary);
203+
// "Estimated gas: 0.0000150 XLM (max: 0.0000180 XLM). Tx size: 450 bytes."
204+
```
205+
206+
**Note:** Offline estimates are less accurate than online simulation as they cannot account for current contract state or resource contention.
207+
208+
## Integration with Contract Calls
209+
210+
### Estimate Before Write
211+
212+
```typescript
213+
// First, estimate the gas
214+
const gas = await sdk.estimateGas('eventManager', artifact, {
215+
source: organizerAddress,
216+
});
217+
218+
// Check if user can afford it
219+
if (gas.costs.maxFee > userBalance) {
220+
throw new Error(`Insufficient balance. Need ${formatGasCost(gas.costs.maxFee)}`);
221+
}
222+
223+
// Show confirmation with gas estimate
224+
if (confirm(`This will cost approximately ${gas.summary}. Proceed?`)) {
225+
// Execute the actual transaction
226+
const result = await sdk.write('eventManager', artifact, {
227+
source: organizerAddress,
228+
signTransaction: wallet.sign,
229+
});
230+
}
231+
```
232+
233+
### Batch Operations
234+
235+
```typescript
236+
const operations = [artifact1, artifact2, artifact3];
237+
238+
// Estimate all operations
239+
const estimates = await Promise.all(
240+
operations.map(op => sdk.estimateGas('eventManager', op, { source }))
241+
);
242+
243+
// Calculate total cost
244+
const totalMaxFee = estimates.reduce((sum, e) => sum + e.costs.maxFee, 0);
245+
246+
console.log(`Total estimated cost for ${operations.length} operations:`);
247+
console.log(` Max: ${formatGasCost(totalMaxFee)}`);
248+
```
249+
250+
## Best Practices
251+
252+
1. **Always Use Buffer**: Set `feeBufferMultiplier` to at least 1.2 (20%) to handle network congestion
253+
2. **Check Success**: Verify `gas.success` before proceeding with actual transactions
254+
3. **Handle Failures**: If simulation fails, the error is in `gas.summary`
255+
4. **Cache Estimates**: Gas estimates can be cached for similar operations within a short time window
256+
5. **Offline Fallback**: Use `estimateGasOffline` for hardware wallet flows where online simulation isn't possible
257+
258+
## Troubleshooting
259+
260+
### "Simulation failed" errors
261+
- Check that the contract ID is correct
262+
- Verify the method name and arguments match the contract spec
263+
- Ensure the source account exists on the network
264+
265+
### High gas costs
266+
- Large argument values increase transaction size
267+
- Complex operations with many storage reads/writes cost more
268+
- Consider batching multiple operations into a single transaction
269+
270+
### Fee buffer exceeded
271+
- Network congestion can cause actual fees to exceed estimates
272+
- Increase `feeBufferMultiplier` during high-traffic periods
273+
- Monitor recent ledger fees to set appropriate buffers
274+
275+
## TypeScript Types
276+
277+
```typescript
278+
interface GasEstimation {
279+
costs: {
280+
baseFee: number; // stroops
281+
resourceFee: number; // stroops
282+
refundableFee: number; // stroops
283+
totalFee: number; // stroops
284+
maxFee: number; // stroops
285+
};
286+
resources: {
287+
instructions: number;
288+
readBytes: number;
289+
writeBytes: number;
290+
entryReads: number;
291+
entryWrites: number;
292+
transactionSizeBytes: number;
293+
metadataSizeBytes: number;
294+
};
295+
success: boolean;
296+
summary: string;
297+
rawSimulation?: unknown;
298+
}
299+
300+
interface GasEstimateOptions extends InvokeOptions {
301+
includeRawSimulation?: boolean;
302+
feeBufferMultiplier?: number;
303+
}
304+
```
305+
306+
---
307+
308+
For more information on Soroban fees, see the [Stellar Documentation](https://soroban.stellar.org/docs/fundamentals/fees-and-metering).

soroban-client/sdk/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,37 @@ const sdk = createTokenboundSdk({
3737
const events = await sdk.eventManager.getAllEvents();
3838
```
3939

40+
### Invocation middleware hooks
41+
42+
You can attach middleware to run logic before and after each invocation lifecycle stage
43+
(`simulate`, `read`, `prepareWrite`, `write`, `sendTransaction`, `waitForTransaction`).
44+
This is useful for request signing policies, logging, tracing, and metrics.
45+
46+
```ts
47+
const sdk = createTokenboundSdk({
48+
horizonUrl: process.env.NEXT_PUBLIC_HORIZON_URL!,
49+
sorobanRpcUrl: process.env.NEXT_PUBLIC_SOROBAN_RPC_URL!,
50+
networkPassphrase: process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE!,
51+
contracts: {
52+
eventManager: process.env.NEXT_PUBLIC_EVENT_MANAGER_CONTRACT,
53+
},
54+
middleware: [
55+
{
56+
before: ({ stage, contract, method }) => {
57+
console.log(`[before] ${stage} ${contract}.${method}`);
58+
},
59+
after: ({ stage, success, durationMs, error }) => {
60+
if (!success) {
61+
console.error(`[after] ${stage} failed in ${durationMs}ms`, error);
62+
return;
63+
}
64+
console.log(`[after] ${stage} success in ${durationMs}ms`);
65+
},
66+
},
67+
],
68+
});
69+
```
70+
4071
### Creating an event
4172

4273
```ts

0 commit comments

Comments
 (0)