Skip to content

Commit 0ea8159

Browse files
authored
Merge branch 'main' into main
2 parents 6ff0f08 + 56739e7 commit 0ea8159

20 files changed

Lines changed: 1009 additions & 99 deletions

File tree

.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

soroban-client/sdk/README.md

Lines changed: 44 additions & 2 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
@@ -54,7 +85,7 @@ const result = await sdk.eventManager.createEvent(
5485
{
5586
source: walletAddress,
5687
signTransaction,
57-
}
88+
},
5889
);
5990
```
6091

@@ -63,6 +94,7 @@ const result = await sdk.eventManager.createEvent(
6394
The SDK automatically retries failed RPC calls with exponential backoff and jitter. This handles transient network failures, rate limiting, and temporary service unavailability.
6495

6596
**Key Features:**
97+
6698
- Automatic retries for transient errors (network issues, timeouts, 5xx errors)
6799
- Exponential backoff with configurable parameters
68100
- Jitter to prevent thundering herd problems
@@ -82,7 +114,14 @@ npm run sdk:generate-types
82114
The SDK provides typed decoder utilities for safely parsing contract responses:
83115

84116
```ts
85-
import { ContractDecoder, decodeArray, decodeStruct, decodeU32, decodeString, decodeI128 } from "./src";
117+
import {
118+
ContractDecoder,
119+
decodeArray,
120+
decodeStruct,
121+
decodeU32,
122+
decodeString,
123+
decodeI128,
124+
} from "./src";
86125

87126
// Decode event response
88127
const event = ContractDecoder.event()(rawResponse);
@@ -99,13 +138,15 @@ const decodeCustom = decodeStruct({
99138
```
100139

101140
**Key Features:**
141+
102142
- Type-safe contract response parsing
103143
- Composable decoders for complex structures
104144
- Clear error messages with context
105145
- Built-in Soroban type support (u32, u64, u128, i128, etc.)
106146
- Pre-built decoders for contract types
107147

108148
See [DECODERS.md](./DECODERS.md) for detailed documentation.
149+
109150
### Batch ledger-entry fetch
110151

111152
`batchGetLedgerEntries` wraps `rpc.Server.getLedgerEntries` so callers can
@@ -134,6 +175,7 @@ for (let i = 0; i < ledgerKeys.length; i += 1) {
134175
```
135176

136177
`chunkSize`, `concurrency`, and `keyId` are all overridable.
178+
137179
### Caching contract schemas at runtime
138180

139181
Soroban contracts in this repo follow the upgradeable pattern, so each
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { nativeToScVal } from "@stellar/stellar-base";
2+
import { TransactionBuilder } from "@stellar/stellar-sdk";
3+
4+
import { SorobanSdkCore } from "../core";
5+
import type { InvocationAfterContext, InvocationBeforeContext } from "../types";
6+
7+
describe("invocation middleware", () => {
8+
it("runs before/after hooks for read and simulate lifecycle", async () => {
9+
const before: InvocationBeforeContext[] = [];
10+
const after: InvocationAfterContext[] = [];
11+
12+
const core = new SorobanSdkCore({
13+
horizonUrl: "https://horizon-testnet.stellar.org",
14+
sorobanRpcUrl: "https://soroban-testnet.stellar.org",
15+
networkPassphrase: "Test SDF Network ; September 2015",
16+
simulationSource:
17+
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
18+
middleware: [
19+
{
20+
before: (ctx) => before.push(ctx),
21+
after: (ctx) => after.push(ctx),
22+
},
23+
],
24+
});
25+
26+
const artifact = {
27+
contractId: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
28+
method: "get_event_count",
29+
args: [],
30+
};
31+
32+
(core as any).buildInvokeTransaction = jest.fn().mockResolvedValue({});
33+
(core as any).retryPolicy.execute = jest.fn(
34+
async (fn: () => Promise<unknown>) => fn(),
35+
);
36+
(core as any).rpcServer.simulateTransaction = jest.fn().mockResolvedValue({
37+
result: { retval: nativeToScVal(12, { type: "u32" }) },
38+
});
39+
40+
const value = await core.read<number>("eventManager", artifact, {});
41+
42+
expect(value).toBe(12);
43+
expect(before.map((ctx) => ctx.stage)).toEqual(["read", "simulate"]);
44+
expect(after.map((ctx) => ctx.stage)).toEqual(["simulate", "read"]);
45+
expect(after.every((ctx) => ctx.success)).toBe(true);
46+
});
47+
48+
it("runs write/send/wait hooks and captures errors", async () => {
49+
const before: InvocationBeforeContext[] = [];
50+
const after: InvocationAfterContext[] = [];
51+
52+
const core = new SorobanSdkCore({
53+
horizonUrl: "https://horizon-testnet.stellar.org",
54+
sorobanRpcUrl: "https://soroban-testnet.stellar.org",
55+
networkPassphrase: "Test SDF Network ; September 2015",
56+
middleware: [
57+
{
58+
before: (ctx) => before.push(ctx),
59+
after: (ctx) => after.push(ctx),
60+
},
61+
],
62+
});
63+
64+
const artifact = {
65+
contractId: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
66+
method: "purchase_ticket",
67+
args: [],
68+
};
69+
70+
(core as any).prepareWrite = jest.fn().mockResolvedValue({
71+
xdr: "AAAA",
72+
networkPassphrase: "Test SDF Network ; September 2015",
73+
source: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
74+
});
75+
const fromXdrSpy = jest
76+
.spyOn(TransactionBuilder, "fromXDR")
77+
.mockReturnValue({} as never);
78+
(core as any).rpcServer.sendTransaction = jest
79+
.fn()
80+
.mockResolvedValue({ status: "PENDING", hash: "abc123" });
81+
(core as any).retryPolicy.execute = jest.fn(
82+
async (fn: () => Promise<unknown>) => fn(),
83+
);
84+
(core as any).waitForTransaction = jest
85+
.fn()
86+
.mockRejectedValue(new Error("boom from confirmation"));
87+
88+
await expect(
89+
core.write("eventManager", artifact, {
90+
source: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
91+
signTransaction: async () => "AAAA",
92+
}),
93+
).rejects.toThrow("boom from confirmation");
94+
95+
fromXdrSpy.mockRestore();
96+
97+
expect(before.map((ctx) => ctx.stage)).toEqual([
98+
"write",
99+
"sendTransaction",
100+
"waitForTransaction",
101+
]);
102+
const writeAfter = after.find((ctx) => ctx.stage === "write");
103+
expect(writeAfter?.success).toBe(false);
104+
expect(writeAfter?.error).toBeDefined();
105+
});
106+
});

0 commit comments

Comments
 (0)