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 all 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
320 changes: 312 additions & 8 deletions packages/stellar_client/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ 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'
};

switch (_network) {
case NetworkType.TESTNET:
_sdk = StellarSDK.TESTNET;
Expand All @@ -61,6 +63,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 +74,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 @@ -130,10 +142,29 @@ class Client {
}
}

Future<bool> addTrustLine() async {
/// Adds trustline for all non-native assets in the `_currencies.currencies` map.
///
/// Trustlines are required to hold non-native assets on a Stellar account.
/// This function iterates over all available currencies and attempts to
/// establish trustlines for each, except for the native asset (`XLM`).
///
/// **Note:** Adding trustline requires having XLM in the account
///
/// ### Returns:
/// - `true` if all trustlines were successfully added.
/// - `false` if one or more trustlines failed.
Future<bool> addConfiguredTrustlines() 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,17 +185,26 @@ 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;
}
}

/// Transfers a specified amount of currency to a destination address.
///
/// This function builds a Stellar transaction to send funds from the current account
/// to a given recipient. It supports optional memo fields for additional transaction details.
/// **Note:** Transfer requires having XLM in the account
Future<bool> transfer(
{required String destinationAddress,
required String amount,
Expand Down Expand Up @@ -231,7 +271,6 @@ class Client {
);

final data = jsonDecode(response.body);

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

Asset _getAsset(String assetCode) {
if (assetCode == 'XLM') {
return AssetTypeNative();
}

final asset = _currencies.currencies[assetCode];
if (asset == null) {
throw Exception('Asset $assetCode is not available');
}

return AssetTypeCreditAlphaNum4(asset.assetCode, asset.issuer);
}

/// Creates a DEX order by submitting a `ManageBuyOfferOperation` transaction.
///
/// This function allows user to create an order to buy a specified asset
/// using another asset on Stellar network.
///
/// **Note:** Creating an order requires having XLM in the account
/// to cover transaction fees and reserve requirements.
///
/// **Price Format:**
/// - The `price` should always include a leading zero for decimal values.
/// - For example, instead of writing `.1`, the price should be written as `0.1`.
/// - **Correct format**: `0.1`
/// - **Incorrect format**: `.1`
Future<bool> 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.');
}

final Asset sellingAsset = _getAsset(sellingAssetCode);
final Asset buyingAsset = _getAsset(buyingAssetCode);

final ManageSellOfferOperation sellOfferOperation =
ManageSellOfferOperationBuilder(
sellingAsset, buyingAsset, amount, price)
.build();

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

try {
Balance? sellAssetBalance;
Balance? buyAssetBalance;

for (final balance in balances) {
if (sellAssetBalance == null) {
if (sellingAssetCode == 'XLM' && balance.assetCode == null) {
sellAssetBalance = balance;
} else if (balance.assetCode == sellingAssetCode) {
sellAssetBalance = balance;
}
}

if (buyingAssetCode != 'XLM' && buyAssetBalance == null) {
if (balance.assetCode == buyingAssetCode) {
buyAssetBalance = balance;
}
}

if (sellAssetBalance != null &&
(buyingAssetCode == 'XLM' || buyAssetBalance != null)) {
break;
}
}

if (sellAssetBalance == null) {
logger.e("Sell asset $sellingAssetCode not found in balances.");
throw Exception('Insufficient balance in $sellingAssetCode');
}

if (buyingAssetCode != 'XLM' && buyAssetBalance == null) {
logger.e("Buy asset $buyingAssetCode not found in balances.");
throw Exception('No trustline for $buyingAssetCode');
}

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(sellOfferOperation)
.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 false;
}
return true;
} catch (error) {
throw Exception('Transaction failed due to: ${error.toString()}');
}
}

/// Cancels a DEX order by submitting a `ManageBuyOfferOperation` transaction with zero amount.
///
/// This function allows user to cancel previously created order with its offerId.
///
/// **Note:** Cancelling an order requires having XLM in the account
/// to cover transaction fees and reserve requirements.
Future<bool> cancelOrder({required String offerId}) async {
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.'),
);

final Asset sellingAsset = targetOffer.selling;
final Asset buyingAsset = targetOffer.buying;

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).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 false;
}
return true;
} catch (error) {
throw Exception('Transaction failed due to: ${error.toString()}');
}
}

/// Updating a DEX order by submitting a `ManageBuyOfferOperation` transaction.
///
/// This function allows user to update previously created order by its offerId.
///
/// **Note:** Updating an order requires having XLM in the account
/// to cover transaction fees and reserve requirements.
///
/// **Price Format:**
/// - The `price` should always include a leading zero for decimal values.
/// - For example, instead of writing `.1`, the price should be written as `0.1`.
/// - **Correct format**: `0.1`
/// - **Incorrect format**: `.1`
Future<bool> updateOrder(
{required String amount,
required String price,
required String offerId,
String? memo}) async {
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.'),
);

ManageBuyOfferOperation updateOfferOperation =
ManageBuyOfferOperationBuilder(
targetOffer!.selling,
targetOffer.buying,
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 false;
}
return true;
} catch (error) {
throw Exception('Transaction failed due to: ${error.toString()}');
}
}

/// Lists all active offers created by the current account.
///
/// This function fetches a list of `OfferResponse` objects representing
/// open orders created by the account.
///
/// ### Understanding Stellar Order Representation:
/// - **Price (`OfferResponse.price`)**: Stellar stores price as `buying / selling`,
/// meaning the displayed price is the **inverse** of the price provided
/// when creating an order.
/// - **Amount (`OfferResponse.amount`)**: This represents the amount of the
/// **buying asset** still available for trade, not the original amount
/// of the selling asset.
///
/// ### Conversion Formula:
/// When placing an order:
/// ```
/// Total selling amount = Buying amount * Price
/// ```
///
/// Stellar inverts the price when storing the offer:
/// ```
/// Stored price = 1 / Provided price
/// ```
///
/// ### Example:
/// #### **Creating an Order**
/// ```dart
/// await stellarClient.createOrder(
/// sellingAssetCode: 'USDC',
/// buyingAssetCode: 'TFT',
/// amount: '5', // Buying 5 TFT
/// price: '0.02'); // 1 USDC = 0.02 TFT
/// ```
///
/// #### **Retrieved Offer (from Stellar Order Book)**
/// ```dart
/// OfferResponse {
/// amount: "0.2", // Total selling amount = 5 * 0.02 = 0.2 USDC
/// price: "50.0" // Inverted: 1 / 0.02 = 50 USDC per TFT
/// }
/// ```
///
/// **Key Takeaways:**
/// - `OfferResponse.amount` = **Total amount of the selling asset left**.
/// - `OfferResponse.price` = **Inverse of the provided price**.
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');
}
}
}
Loading