Skip to content

Commit a12a176

Browse files
cuongph87Cuong Phamjopmiddelkamp
authored
chore: add Soroban examples (#120)
Co-authored-by: Cuong Pham <cuong.pham@beansapp.com> Co-authored-by: Jop Middelkamp <middelkamp@live.nl>
1 parent 200d6a3 commit a12a176

27 files changed

Lines changed: 1414 additions & 545 deletions

Examples/Horizon/StellarDotnetSdk.Examples.Horizon.csproj

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
@@ -15,4 +15,9 @@
1515
<ProjectReference Include="..\..\StellarDotnetSdk\StellarDotnetSdk.csproj"/>
1616
</ItemGroup>
1717

18+
<ItemGroup>
19+
<PackageReference Include="System.Net.Http" Version="4.3.4"/>
20+
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/>
21+
</ItemGroup>
22+
1823
</Project>
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
using StellarDotnetSdk.Accounts;
2+
using StellarDotnetSdk.Operations;
3+
using StellarDotnetSdk.Soroban;
4+
using StellarDotnetSdk.Transactions;
5+
using StellarDotnetSdk.Examples.Soroban.Helpers;
6+
using SCString = StellarDotnetSdk.Soroban.SCString;
7+
using SCVal = StellarDotnetSdk.Soroban.SCVal;
8+
9+
namespace StellarDotnetSdk.Examples.Soroban.Examples;
10+
11+
/// <summary>
12+
/// Demonstrates atomic swap between two parties:
13+
/// - Deploy atomic swap contract
14+
/// - Deploy two token contracts (Token A and Token B)
15+
/// - Mint tokens to both parties
16+
/// - Execute the atomic swap
17+
/// - The swap happens atomically (both succeed or both fail)
18+
/// </summary>
19+
internal static class AtomicSwapExample
20+
{
21+
private const long AmountA = 1000000L; // 0.1 tokens with 7 decimals
22+
private const long AmountB = 2000000L; // 0.2 tokens with 7 decimals
23+
24+
public static async Task Run(IAccountId partyA, IAccountId partyB, IAccountId tokenIssuer)
25+
{
26+
Console.WriteLine("=== Atomic Swap Example ===");
27+
28+
var server = SorobanHelpers.CreateServer();
29+
30+
Console.WriteLine("\n--- Step 1: Deploy Token A (Party A will swap this) ---");
31+
var tokenAContractId = await DeployTokenContract(
32+
partyA,
33+
"Token A",
34+
"TKNA",
35+
7);
36+
Console.WriteLine($"Token A contract ID: {tokenAContractId}");
37+
38+
Console.WriteLine("\n--- Step 2: Deploy Token B (Party B will swap this) ---");
39+
var tokenBContractId = await DeployTokenContract(
40+
partyB,
41+
"Token B",
42+
"TKNB",
43+
7);
44+
Console.WriteLine($"Token B contract ID: {tokenBContractId}");
45+
46+
Console.WriteLine("\n--- Step 3: Mint Token A to Party A ---");
47+
await MintTokens(server, partyA, partyA, tokenAContractId, AmountA);
48+
49+
Console.WriteLine("\n--- Step 4: Mint Token B to Party B ---");
50+
await MintTokens(server, partyB, partyB, tokenBContractId, AmountB);
51+
52+
Console.WriteLine("\n--- Step 5: Deploy Atomic Swap Contract ---");
53+
var swapWasmId = await UploadContractExample.Run(partyA, SorobanWasms.AtomicSwapWasmPath);
54+
var swapContractId = await CreateContractExample.Run(partyA, swapWasmId);
55+
Console.WriteLine($"Atomic swap contract ID: {swapContractId}");
56+
57+
Console.WriteLine("\n--- Step 6: Party A Approves Swap Contract to Spend Token A ---");
58+
await ApproveSwapContract(server, partyA, tokenAContractId, swapContractId, AmountA);
59+
60+
Console.WriteLine("\n--- Step 7: Party B Approves Swap Contract to Spend Token B ---");
61+
await ApproveSwapContract(server, partyB, tokenBContractId, swapContractId, AmountB);
62+
63+
Console.WriteLine("\n--- Step 8: Execute Atomic Swap ---");
64+
await ExecuteSwap(server, partyA, partyB, tokenAContractId, tokenBContractId, swapContractId);
65+
66+
Console.WriteLine("\n--- Step 9: Verify Swap Results ---");
67+
await VerifySwapResults(server, partyA, partyB, tokenAContractId, tokenBContractId);
68+
69+
Console.WriteLine("\n✓ Atomic swap completed successfully!");
70+
Console.WriteLine("Key takeaways:");
71+
Console.WriteLine(" • Both transfers succeeded together (atomicity)");
72+
Console.WriteLine(" • No intermediary could steal funds");
73+
Console.WriteLine(" • No trust required between parties");
74+
Console.WriteLine(" • Smart contract enforced fair exchange");
75+
}
76+
77+
private static async Task<string> DeployTokenContract(
78+
IAccountId admin,
79+
string name,
80+
string symbol,
81+
uint decimals)
82+
{
83+
var tokenWasmId = await UploadContractExample.Run(admin, SorobanWasms.TokenWasmPath);
84+
85+
var constructorArgs = new SCVal[]
86+
{
87+
new ScAccountId(admin.AccountId),
88+
new SCUint32(decimals),
89+
new SCString(name),
90+
new SCString(symbol),
91+
};
92+
93+
var contractId = await CreateContractExample.Run(admin, tokenWasmId, constructorArgs);
94+
return contractId;
95+
}
96+
97+
private static async Task MintTokens(
98+
SorobanServer server,
99+
IAccountId minter,
100+
IAccountId recipient,
101+
string tokenContractId,
102+
long amount)
103+
{
104+
var minterAccount = await server.GetAccount(minter.AccountId);
105+
106+
var mintArgs = new SCVal[]
107+
{
108+
new ScAccountId(recipient.AccountId),
109+
new SCInt128(amount.ToString()),
110+
};
111+
112+
var mintOp = new InvokeContractOperation(tokenContractId, "mint", mintArgs, minter);
113+
var mintTx = new TransactionBuilder(minterAccount).AddOperation(mintOp).Build();
114+
await SorobanHelpers.SimulateAndUpdateTransaction(mintTx, minter);
115+
116+
var mintResponse = await server.SendTransaction(mintTx);
117+
ArgumentNullException.ThrowIfNull(mintResponse.Hash);
118+
await SorobanHelpers.PollTransaction(mintResponse.Hash);
119+
Console.WriteLine($"Minted {amount} tokens to {recipient.AccountId}");
120+
}
121+
122+
private static async Task ApproveSwapContract(
123+
SorobanServer server,
124+
IAccountId owner,
125+
string tokenContractId,
126+
string swapContractId,
127+
long amount)
128+
{
129+
var ownerAccount = await server.GetAccount(owner.AccountId);
130+
var latestLedger = (await server.GetLatestLedger()).Sequence;
131+
132+
var approveArgs = new SCVal[]
133+
{
134+
new ScAccountId(owner.AccountId),
135+
new ScContractId(swapContractId),
136+
new SCInt128(amount.ToString()),
137+
new SCUint32((uint)latestLedger + 200000),
138+
};
139+
140+
var approveOp = new InvokeContractOperation(tokenContractId, "approve", approveArgs, owner);
141+
var approveTx = new TransactionBuilder(ownerAccount).AddOperation(approveOp).Build();
142+
await SorobanHelpers.SimulateAndUpdateTransaction(approveTx, owner);
143+
144+
var approveResponse = await server.SendTransaction(approveTx);
145+
ArgumentNullException.ThrowIfNull(approveResponse.Hash);
146+
await SorobanHelpers.PollTransaction(approveResponse.Hash);
147+
Console.WriteLine($"Approved swap contract to spend {amount} tokens");
148+
}
149+
150+
private static async Task ExecuteSwap(
151+
SorobanServer server,
152+
IAccountId partyA,
153+
IAccountId partyB,
154+
string tokenAContractId,
155+
string tokenBContractId,
156+
string swapContractId)
157+
{
158+
Console.WriteLine("Party A initiates the swap:");
159+
Console.WriteLine($" • Party A gives {AmountA} of Token A");
160+
Console.WriteLine($" • Party A receives {AmountB} of Token B");
161+
Console.WriteLine($" • Party B gives {AmountB} of Token B");
162+
Console.WriteLine($" • Party B receives {AmountA} of Token A");
163+
164+
var partyAAccount = await server.GetAccount(partyA.AccountId);
165+
166+
var swapArgs = new SCVal[]
167+
{
168+
new ScAccountId(partyA.AccountId),
169+
new ScAccountId(partyB.AccountId),
170+
new ScContractId(tokenAContractId),
171+
new ScContractId(tokenBContractId),
172+
new SCInt128(AmountA.ToString()),
173+
new SCInt128(AmountB.ToString()),
174+
new SCInt128(AmountB.ToString()),
175+
new SCInt128(AmountA.ToString()),
176+
};
177+
178+
var swapOp = new InvokeContractOperation(swapContractId, "swap", swapArgs, partyA);
179+
var swapTx = new TransactionBuilder(partyAAccount).AddOperation(swapOp).Build();
180+
await SorobanHelpers.SimulateAndUpdateTransaction(swapTx, partyA);
181+
182+
Console.WriteLine("Sending atomic swap transaction...");
183+
var swapResponse = await server.SendTransaction(swapTx);
184+
ArgumentNullException.ThrowIfNull(swapResponse.Hash);
185+
await SorobanHelpers.PollTransaction(swapResponse.Hash);
186+
}
187+
188+
private static async Task VerifySwapResults(
189+
SorobanServer server,
190+
IAccountId partyA,
191+
IAccountId partyB,
192+
string tokenAContractId,
193+
string tokenBContractId)
194+
{
195+
// Check Party A's Token B balance
196+
var partyAAccount = await server.GetAccount(partyA.AccountId);
197+
var balanceABArgs = new SCVal[] { new ScAccountId(partyA.AccountId) };
198+
var balanceABOp = new InvokeContractOperation(tokenBContractId, "balance", balanceABArgs, partyA);
199+
var balanceABTx = new TransactionBuilder(partyAAccount).AddOperation(balanceABOp).Build();
200+
var balanceABSim = await server.SimulateTransaction(balanceABTx);
201+
202+
if (balanceABSim.Results != null && balanceABSim.Results.Length > 0)
203+
{
204+
var balanceXdr = balanceABSim.Results[0].Xdr;
205+
ArgumentNullException.ThrowIfNull(balanceXdr);
206+
var balance = (SCInt128)SCVal.FromXdrBase64(balanceXdr);
207+
Console.WriteLine($"Party A now has {balance.Lo} of Token B (expected {AmountB})");
208+
}
209+
210+
// Check Party B's Token A balance
211+
var partyBAccount = await server.GetAccount(partyB.AccountId);
212+
var balanceBAArgs = new SCVal[] { new ScAccountId(partyB.AccountId) };
213+
var balanceBAOp = new InvokeContractOperation(tokenAContractId, "balance", balanceBAArgs, partyB);
214+
var balanceBATx = new TransactionBuilder(partyBAccount).AddOperation(balanceBAOp).Build();
215+
var balanceBASim = await server.SimulateTransaction(balanceBATx);
216+
217+
if (balanceBASim.Results != null && balanceBASim.Results.Length > 0)
218+
{
219+
var balanceXdr = balanceBASim.Results[0].Xdr;
220+
ArgumentNullException.ThrowIfNull(balanceXdr);
221+
var balance = (SCInt128)SCVal.FromXdrBase64(balanceXdr);
222+
Console.WriteLine($"Party B now has {balance.Lo} of Token A (expected {AmountA})");
223+
}
224+
}
225+
}
226+
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using StellarDotnetSdk.Accounts;
2+
using StellarDotnetSdk.Operations;
3+
using StellarDotnetSdk.Transactions;
4+
using StellarDotnetSdk.Examples.Soroban.Helpers;
5+
using SCVal = StellarDotnetSdk.Soroban.SCVal;
6+
7+
namespace StellarDotnetSdk.Examples.Soroban.Examples;
8+
9+
/// <summary>
10+
/// Demonstrates how to create a new contract instance from an uploaded WASM.
11+
/// </summary>
12+
internal static class CreateContractExample
13+
{
14+
/// <summary>
15+
/// Creates a new contract instance from an uploaded WASM.
16+
/// </summary>
17+
/// <param name="keyPair">The source account key pair.</param>
18+
/// <param name="wasmId">The WASM ID (hash) of the uploaded contract.</param>
19+
/// <param name="args">Optional constructor arguments for the contract.</param>
20+
/// <returns>The created contract ID.</returns>
21+
public static async Task<string> Run(IAccountId keyPair, string wasmId, SCVal[]? args = null)
22+
{
23+
Console.WriteLine($"=== Create Contract from WASM {wasmId} ===");
24+
25+
var server = SorobanHelpers.CreateServer();
26+
27+
// Load the account with the updated sequence number from Soroban server
28+
var account = await server.GetAccount(keyPair.AccountId);
29+
30+
var operation = CreateContractOperation.FromAddress(wasmId, account.AccountId, args);
31+
32+
var tx = new TransactionBuilder(account).AddOperation(operation).Build();
33+
await SorobanHelpers.SimulateAndUpdateTransaction(tx, keyPair);
34+
35+
Console.WriteLine($"Sending 'Create contract' transaction with xdr: {tx.ToEnvelopeXdrBase64()}");
36+
var sendResponse = await server.SendTransaction(tx);
37+
38+
var txHash = sendResponse.Hash;
39+
Console.WriteLine($"`Create contract` transaction hash: {txHash}");
40+
41+
if (sendResponse.ErrorResultXdr != null)
42+
{
43+
Console.WriteLine(
44+
$"Sending 'Create contract' transaction failed with error: {sendResponse.ErrorResultXdr}");
45+
}
46+
47+
ArgumentNullException.ThrowIfNull(txHash);
48+
49+
var response = await SorobanHelpers.PollTransaction(txHash);
50+
51+
var createdContractId = response.CreatedContractId;
52+
ArgumentNullException.ThrowIfNull(createdContractId);
53+
Console.WriteLine($"Created contract ID: {createdContractId}");
54+
55+
return createdContractId;
56+
}
57+
}
58+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using StellarDotnetSdk.Accounts;
2+
using StellarDotnetSdk.Operations;
3+
using StellarDotnetSdk.Transactions;
4+
using StellarDotnetSdk.Examples.Soroban.Helpers;
5+
6+
namespace StellarDotnetSdk.Examples.Soroban.Examples;
7+
8+
/// <summary>
9+
/// Demonstrates contract events:
10+
/// - Deploy an events contract
11+
/// - Invoke functions that publish events
12+
/// - Read and parse the published events from transaction results
13+
/// </summary>
14+
internal static class EventsContractExample
15+
{
16+
public static async Task Run(IAccountId keyPair)
17+
{
18+
Console.WriteLine("=== Events Contract Example ===");
19+
20+
var server = SorobanHelpers.CreateServer();
21+
22+
Console.WriteLine("\n--- Step 1: Deploy Events Contract ---");
23+
var wasmId = await UploadContractExample.Run(keyPair, SorobanWasms.EventsWasmPath);
24+
var contractId = await CreateContractExample.Run(keyPair, wasmId);
25+
Console.WriteLine($"Events contract ID: {contractId}");
26+
27+
Console.WriteLine("\n--- Step 2: Invoke Contract to Publish Events ---");
28+
var account = await server.GetAccount(keyPair.AccountId);
29+
30+
var eventOp = new InvokeContractOperation(contractId, "increment", null, keyPair);
31+
var eventTx = new TransactionBuilder(account).AddOperation(eventOp).Build();
32+
await SorobanHelpers.SimulateAndUpdateTransaction(eventTx, keyPair);
33+
34+
var eventResponse = await server.SendTransaction(eventTx);
35+
ArgumentNullException.ThrowIfNull(eventResponse.Hash);
36+
var eventResult = await SorobanHelpers.PollTransaction(eventResponse.Hash);
37+
38+
Console.WriteLine("\n--- Step 3: Read Published Events ---");
39+
if (eventResult.ResultMetaXdr != null)
40+
{
41+
Console.WriteLine("Transaction produced events (check ResultMetaXdr for details)");
42+
Console.WriteLine("Events are embedded in the transaction metadata");
43+
Console.WriteLine($"Number of ContractEventsXdr: {eventResult.Events?.ContractEventsXdr?.Length ?? 0}");
44+
Console.WriteLine($"Number of DiagnosticEventsXdr: {eventResult.Events?.DiagnosticEventsXdr?.Length ?? 0}");
45+
Console.WriteLine(
46+
$"Number of TransactionEventsXdr: {eventResult.Events?.TransactionEventsXdr?.Length ?? 0}");
47+
}
48+
49+
Console.WriteLine("\n✓ Events contract example completed successfully!");
50+
}
51+
}
52+

0 commit comments

Comments
 (0)