Skip to content

Support DEX orders on stellar client #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
May 4, 2025
Merged
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5f75e28
WIP: support creating && cancelling orders
AlaaElattar Dec 2, 2024
163bee4
Merge branch 'development' of https://github.com/codescalers/tfgrid-s…
AlaaElattar Dec 4, 2024
0f603ab
support creating && cancelling orders
AlaaElattar Dec 5, 2024
3d6c320
WIP: trying to create orders with TF service
AlaaElattar Dec 8, 2024
3b94cab
support native asset && comment orders with TF service
AlaaElattar Dec 8, 2024
8bdabab
remova commented code
AlaaElattar Dec 22, 2024
ce14d21
Merge branch 'development' into development_order_book
AlaaElattar Dec 22, 2024
3f4b8d1
fix workflow
AlaaElattar Dec 25, 2024
5c44c9c
added native asset in currencies list && remove all prints
AlaaElattar Dec 25, 2024
1b7492b
WIP: fix logger && trustlines issue
AlaaElattar Jan 12, 2025
ccae64c
fix balance checking issue && update renaming
AlaaElattar Jan 12, 2025
3e8d0b5
support updating an offer
AlaaElattar Jan 12, 2025
12c2650
upgrade http version
AlaaElattar Feb 2, 2025
ac4edb2
Merge branch 'development' into development_order_book
AlaaElattar Feb 2, 2025
71697b7
Merge branch 'development' of https://github.com/codescalers/tfgrid-s…
AlaaElattar Feb 2, 2025
bb5eaac
Merge branch 'development_order_book' of https://github.com/codescale…
AlaaElattar Feb 2, 2025
b5beea7
added documentation && removed redundant code for getting asset
AlaaElattar Feb 10, 2025
6b2baa0
update docs
AlaaElattar Feb 18, 2025
20bbbef
added function for getting trading history
AlaaElattar Mar 10, 2025
c86f2f9
update return of create, cancel and update orders to be bool
AlaaElattar Mar 12, 2025
a33b6f6
fix bug in create order
AlaaElattar Mar 23, 2025
10c6448
apply pr comments && refactor code
AlaaElattar Apr 28, 2025
50d12b7
fix workflow
AlaaElattar Apr 28, 2025
0a890f9
handle 2 loops in creating order && remove memo from cancel order
AlaaElattar Apr 30, 2025
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
265 changes: 247 additions & 18 deletions packages/stellar_client/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Client {
late KeyPair _keyPair;
late currency.Currencies _currencies;
late Map<String, String> _serviceUrls;
late Map<String, String> _horizonServerUrls;
late Network _stellarNetwork;

String get accountId => _keyPair.accountId;
Expand Down Expand Up @@ -41,10 +42,16 @@ class Client {

void _initialize() {
late final currency.Currency tft;
late final currency.Currency usdc;
_serviceUrls = {
'PUBLIC': 'https://tokenservices.threefold.io/threefoldfoundation',
'TESTNET': 'https://testnet.threefold.io/threefoldfoundation'
};
_horizonServerUrls = {
'PUBLIC': 'https://horizon.stellar.org/',
'TESTNET': 'https://horizon-testnet.stellar.org/'
};

switch (_network) {
case NetworkType.TESTNET:
_sdk = StellarSDK.TESTNET;
Expand All @@ -53,6 +60,9 @@ class Client {
assetCode: 'TFT',
issuer: "GA47YZA3PKFUZMPLQ3B5F2E3CJIB57TGGU7SPCQT2WAEYKN766PWIMB3",
);
usdc = currency.Currency(
assetCode: 'USDC',
issuer: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5');
break;
case NetworkType.PUBLIC:
_sdk = StellarSDK.PUBLIC;
Expand All @@ -61,12 +71,15 @@ class Client {
assetCode: 'TFT',
issuer: "GBOVQKJYHXRR3DX6NOX2RRYFRCUMSADGDESTDNBDS6CDVLGVESRTAC47",
);
usdc = currency.Currency(
assetCode: 'USDC',
issuer: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN');
break;
default:
throw Exception('Unsupported network type');
}

_currencies = currency.Currencies({'TFT': tft});
_currencies = currency.Currencies({'TFT': tft, 'USDC': usdc});
}

Future<bool> activateThroughThreefoldService() async {
Expand Down Expand Up @@ -125,26 +138,27 @@ class Client {
}

Future<bool> addTrustLine() async {
for (var entry in _currencies.currencies.entries) {
String currencyCode = entry.key;
currency.Currency currentCurrency = entry.value;
try {
for (var entry in _currencies.currencies.entries) {
String currencyCode = entry.key;
currency.Currency currentCurrency = entry.value;

String issuerAccountId = currentCurrency.issuer;
Asset currencyAsset =
AssetTypeCreditAlphaNum4(currentCurrency.assetCode, issuerAccountId);
String issuerAccountId = currentCurrency.issuer;
Asset currencyAsset = AssetTypeCreditAlphaNum4(
currentCurrency.assetCode, issuerAccountId);

ChangeTrustOperationBuilder changeTrustOperation =
ChangeTrustOperationBuilder(currencyAsset, "300000");
ChangeTrustOperationBuilder changeTrustOperation =
ChangeTrustOperationBuilder(currencyAsset, "300000");

final account = await _sdk.accounts.account(accountId);
final account = await _sdk.accounts.account(accountId);

Transaction transaction = TransactionBuilder(account)
.addOperation(changeTrustOperation.build())
.build();
transaction.sign(_keyPair, _stellarNetwork);
Transaction transaction = TransactionBuilder(account)
.addOperation(changeTrustOperation.build())
.build();
transaction.sign(_keyPair, _stellarNetwork);

SubmitTransactionResponse response =
await _sdk.submitTransaction(transaction);
SubmitTransactionResponse response =
await _sdk.submitTransaction(transaction);

if (!response.success) {
logger.e("Failed to add trustline for $currencyCode");
Expand All @@ -153,7 +167,6 @@ class Client {
logger.i("trustline for $currencyCode was added successfully");
return true;
}
}

logger.i("No trustlines were processed");
return false;
Expand Down Expand Up @@ -222,7 +235,6 @@ class Client {
);

final data = jsonDecode(response.body);

String trustlineTransaction = data['addtrustline_transaction'];
XdrTransactionEnvelope xdrTxEnvelope =
XdrTransactionEnvelope.fromEnvelopeXdrString(trustlineTransaction);
Expand Down Expand Up @@ -473,4 +485,221 @@ class Client {
throw Exception("Couldn't get memo text due to ${e}");
}
}

Future<SubmitTransactionResponse> createOrder(
{required String sellingAsset,
required String buyingAsset,
required String amount,
required String price,
String? memo}) async {
if (!_currencies.currencies.containsKey(sellingAsset) ||
sellingAsset == 'XLM') {
throw Exception('Sell asset $sellingAsset is not available.');
}
if (!_currencies.currencies.containsKey(buyingAsset) ||
buyingAsset == 'XLM') {
throw Exception('Buy asset $buyingAsset is not available.');
}

late final Asset sellAsset;
late final Asset buyAsset;

if (sellingAsset == 'XLM') {
sellAsset = AssetTypeNative();
} else {
sellAsset = AssetTypeCreditAlphaNum4(
_currencies.currencies[sellingAsset]!.assetCode,
_currencies.currencies[sellingAsset]!.issuer);
}
if (sellingAsset == 'XLM') {
buyAsset = AssetTypeNative();
} else {
buyAsset = AssetTypeCreditAlphaNum4(
_currencies.currencies[buyingAsset]!.assetCode,
_currencies.currencies[buyingAsset]!.issuer);
}

final ManageBuyOfferOperation buyOfferOperation =
ManageBuyOfferOperationBuilder(sellAsset, buyAsset, amount, price)
.build();

final account = await _sdk.accounts.account(accountId);

final balances = account.balances;
final sellAssetBalance = balances.firstWhere(
(balance) => balance.assetCode == sellingAsset,
orElse: () => throw Exception('Insufficient balance in $sellingAsset'),
);

final double sellAmount = double.parse(amount);
final double availableBalance = double.parse(sellAssetBalance.balance);
if (sellAmount > availableBalance) {
throw Exception(
'Insufficient balance in $sellingAsset. Available: $availableBalance');
}
final Transaction transaction = TransactionBuilder(account)
.addOperation(buyOfferOperation)
.addMemo(memo != null ? Memo.text(memo) : Memo.none())
.build();
print('Transaction XDR: ${transaction.toEnvelopeXdrBase64()}');

transaction.sign(_keyPair, _stellarNetwork);
try {
final SubmitTransactionResponse response =
await _sdk.submitTransaction(transaction);
if (!response.success) {
print('Transaction failed with result: ${response.resultXdr}');
}
return response;
} catch (error) {
throw Exception('Transaction failed due to: ${error.toString()}');
}
}

Future<SubmitTransactionResponse> cancelOrder(
{required String sellingAsset,
required String buyingAsset,
required String offerId,
String? memo}) async {
if (!_currencies.currencies.containsKey(sellingAsset) ||
sellingAsset == 'XLM') {
throw Exception('Sell asset $sellingAsset is not available.');
}
if (!_currencies.currencies.containsKey(buyingAsset) ||
buyingAsset == 'XLM') {
throw Exception('Buy asset $buyingAsset is not available.');
}

final offers = (await _sdk.offers.forAccount(accountId).execute()).records;
final OfferResponse? targetOffer = offers.firstWhere(
(offer) => offer.id == offerId,
orElse: () => throw Exception(
'Offer with ID $offerId not found in user\'s account.'),
);

late final Asset sellAsset;
late final Asset buyAsset;

if (sellingAsset == 'XLM') {
sellAsset = AssetTypeNative();
} else {
sellAsset = AssetTypeCreditAlphaNum4(
_currencies.currencies[sellingAsset]!.assetCode,
_currencies.currencies[sellingAsset]!.issuer);
}
if (sellingAsset == 'XLM') {
buyAsset = AssetTypeNative();
} else {
buyAsset = AssetTypeCreditAlphaNum4(
_currencies.currencies[buyingAsset]!.assetCode,
_currencies.currencies[buyingAsset]!.issuer);
}

final ManageBuyOfferOperation cancelOfferOperation =
ManageBuyOfferOperationBuilder(sellAsset, buyAsset, '0', '1')
.setOfferId(offerId)
.build();

final account = await _sdk.accounts.account(accountId);
final Transaction transaction = TransactionBuilder(account)
.addOperation(cancelOfferOperation)
.addMemo(memo != null ? Memo.text(memo) : Memo.none())
.build();
transaction.sign(_keyPair, _stellarNetwork);
try {
final SubmitTransactionResponse response =
await _sdk.submitTransaction(transaction);
if (!response.success) {
print('Transaction failed with result: ${response.resultXdr}');
}
return response;
} catch (error) {
throw Exception('Transaction failed due to: ${error.toString()}');
}
}

Future<void> getOrderBook(
{required String sellingAssetCode,
required String buyingAssetCode}) async {
if (!_currencies.currencies.containsKey(sellingAssetCode) ||
sellingAssetCode == 'XLM') {
throw Exception('Sell asset $sellingAssetCode is not available.');
}
if (!_currencies.currencies.containsKey(buyingAssetCode) ||
buyingAssetCode == 'XLM') {
throw Exception('Buy asset $buyingAssetCode is not available.');
}
http.Client httpClient = http.Client();
Uri serverURI = Uri.parse(_horizonServerUrls[_network.toString()]!);
late final Asset sellingAsset;
late final Asset buyingAsset;

if (sellingAssetCode == 'XLM') {
sellingAsset = AssetTypeNative();
} else {
sellingAsset = AssetTypeCreditAlphaNum4(
_currencies.currencies[sellingAssetCode]!.assetCode,
_currencies.currencies[sellingAssetCode]!.issuer);
}
if (buyingAssetCode == 'XLM') {
buyingAsset = AssetTypeNative();
} else {
buyingAsset = AssetTypeCreditAlphaNum4(
_currencies.currencies[buyingAssetCode]!.assetCode,
_currencies.currencies[buyingAssetCode]!.issuer);
}

OrderBookRequestBuilder orderBookRequest =
OrderBookRequestBuilder(httpClient, serverURI)
..sellingAsset(sellingAsset)
..buyingAsset(buyingAsset);

Stream<OrderBookResponse> orderBookStream = orderBookRequest.stream();
orderBookStream.listen((orderBookResponse) {
print("Received OrderBookResponse:");
print("Base: ${orderBookResponse.base}");
print("Counter: ${orderBookResponse.counter}");

print("\nBids:");
for (var offer in orderBookResponse.bids) {
// priceR numerator/denominator
print(
'Bid - Amount: ${offer.amount}, Price: ${offer.price}, PriceR: ${offer.priceR}');
}

print("\nAsks:");
for (var offer in orderBookResponse.asks) {
print(
'Ask - Amount: ${offer.amount}, Price: ${offer.price}, PriceR: ${offer.priceR}');
}
}, onError: (error) {
print("Error while listening to order book stream: $error");
}, onDone: () {
print("Order book stream is closed.");
});
}

Future<List<OfferResponse>> listMyOffers() async {
try {
final offers = await _sdk.offers.forAccount(accountId).execute();
print(offers);

if (offers.records.isEmpty) {
print('No offers found for account: $accountId');
return [];
}

for (var offer in offers.records) {
print('Offer ID: ${offer.id}');
print('Selling Asset: ${offer.selling}');
print('Buying Asset: ${offer.buying}');
print('Amount: ${offer.amount}');
print('Price: ${offer.price}');
print('-----------------------------------');
}
return offers.records;
} catch (error) {
throw Exception('Error listing offers for account $accountId: $error');
}
}
}
Loading