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
Show file tree
Hide file tree
Changes from 16 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
301 changes: 294 additions & 7 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 @@ -49,10 +50,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 @@ -61,6 +68,9 @@ class Client {
assetCode: 'TFT',
issuer: "GA47YZA3PKFUZMPLQ3B5F2E3CJIB57TGGU7SPCQT2WAEYKN766PWIMB3",
);
usdc = currency.Currency(
assetCode: 'USDC',
issuer: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5');
break;
case NetworkType.PUBLIC:
_sdk = StellarSDK.PUBLIC;
Expand All @@ -69,10 +79,17 @@ class Client {
assetCode: 'TFT',
issuer: "GBOVQKJYHXRR3DX6NOX2RRYFRCUMSADGDESTDNBDS6CDVLGVESRTAC47",
);
usdc = currency.Currency(
assetCode: 'USDC',
issuer: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN');
break;
}

_currencies = currency.Currencies({'TFT': tft});
_currencies = currency.Currencies({
'TFT': tft,
'USDC': usdc,
'XLM': currency.Currency(assetCode: 'XLM', issuer: "")
});
}

Future<bool> activateThroughThreefoldService() async {
Expand Down Expand Up @@ -131,9 +148,17 @@ class Client {
}

Future<bool> addTrustLine() async {
bool allTrustlinesAdded = true;

for (var entry in _currencies.currencies.entries) {
String currencyCode = entry.key;
currency.Currency currentCurrency = entry.value;
if (currencyCode == 'XLM') {
logger.i("Skipping trustline for native asset $currencyCode");
continue;
}
logger.i(
"Processing trustline for ${entry.key} with issuer ${entry.value.issuer}");

String issuerAccountId = currentCurrency.issuer;
Asset currencyAsset =
Expand All @@ -154,15 +179,19 @@ class Client {

if (!response.success) {
logger.e("Failed to add trustline for $currencyCode");
return false;
allTrustlinesAdded = false;
} else {
logger.i("trustline for $currencyCode was added successfully");
return true;
logger.i("Trustline for $currencyCode was added successfully");
}
}

logger.i("No trustlines were processed");
return false;
if (allTrustlinesAdded) {
logger.i("All trustlines were added successfully");
return true;
} else {
logger.e("One or more trustlines failed to be added");
return false;
}
}

Future<bool> transfer(
Expand Down Expand Up @@ -231,7 +260,6 @@ class Client {
);

final data = jsonDecode(response.body);

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

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

late final Asset sellAsset;
late final Asset buyAsset;

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

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

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

try {
final sellAssetBalance = balances.firstWhere(
(balance) {
if (sellingAssetCode == 'XLM' && balance.assetCode == null) {
// Special case for XLM
return true;
} else {
return balance.assetCode == sellingAssetCode;
}
},
orElse: () {
logger.e("Sell asset $sellingAssetCode not found in balances.");
throw Exception('Insufficient balance in $sellingAssetCode');
},
);

final double sellAmount = double.parse(amount);
final double availableBalance = double.parse(sellAssetBalance.balance);

if (sellAmount > availableBalance) {
throw Exception(
'Insufficient balance in $sellingAssetCode. Available: $availableBalance');
}
} catch (e) {
logger.e("Error: ${e.toString()}");
rethrow;
}

final Transaction transaction = TransactionBuilder(account)
.addOperation(buyOfferOperation)
.addMemo(memo != null ? Memo.text(memo) : Memo.none())
.build();

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

Future<SubmitTransactionResponse> cancelOrder(
{required String sellingAssetCode,
required String buyingAssetCode,
required String offerId,
String? memo}) async {
if (!_currencies.currencies.containsKey(sellingAssetCode)) {
throw Exception('Sell asset $sellingAssetCode is not available.');
}
if (!_currencies.currencies.containsKey(buyingAssetCode)) {
throw Exception('Buy asset $buyingAssetCode 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 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);
}

final ManageBuyOfferOperation cancelOfferOperation =
ManageBuyOfferOperationBuilder(sellingAsset, buyingAsset, '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) {
logger.e('Transaction failed with result: ${response.resultXdr}');
}
return response;
} catch (error) {
throw Exception('Transaction failed due to: ${error.toString()}');
}
}

Future<SubmitTransactionResponse> updateOrder(
{required String sellingAssetCode,
required String buyingAssetCode,
required String amount,
required String price,
required String offerId,
String? memo}) async {
if (!_currencies.currencies.containsKey(sellingAssetCode)) {
throw Exception('Sell asset $sellingAssetCode is not available.');
}
if (!_currencies.currencies.containsKey(buyingAssetCode)) {
throw Exception('Buy asset $buyingAssetCode 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 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);
}

ManageBuyOfferOperation updateOfferOperation = ManageBuyOfferOperationBuilder(
sellingAsset,
buyingAsset,
amount,
price,
).setOfferId(offerId).build();

final account = await _sdk.accounts.account(accountId);
final Transaction transaction = TransactionBuilder(account)
.addOperation(updateOfferOperation)
.build();
transaction.sign(_keyPair, _stellarNetwork);
try {
final SubmitTransactionResponse response =
await _sdk.submitTransaction(transaction);
if (!response.success) {
logger.e('Transaction failed with result: ${response.resultXdr}');
}
return response;
} catch (error) {
throw Exception('Transaction failed due to: ${error.toString()}');
}
}

Future<Stream<OrderBookResponse>> getOrderBook(
{required String sellingAssetCode,
required String buyingAssetCode}) async {
if (!_currencies.currencies.containsKey(sellingAssetCode)) {
throw Exception('Sell asset $sellingAssetCode is not available.');
}
if (!_currencies.currencies.containsKey(buyingAssetCode)) {
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);

return await orderBookRequest.stream();
}

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

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

return offers.records;
} catch (error) {
throw Exception('Error listing offers for account $accountId: $error');
}
}
}
4 changes: 2 additions & 2 deletions packages/stellar_client/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.2.2"
version: "1.3.0"
http_multi_server:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion packages/stellar_client/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ environment:

# Add regular dependencies here.
dependencies:
http: ^1.2.2
http: 1.3.0
stellar_flutter_sdk: ^1.9.2

dev_dependencies:
Expand Down