@@ -49,10 +49,12 @@ class Client {
4949
5050 void _initialize () {
5151 late final currency.Currency tft;
52+ late final currency.Currency usdc;
5253 _serviceUrls = {
5354 'PUBLIC' : 'https://tokenservices.threefold.io/threefoldfoundation' ,
5455 'TESTNET' : 'https://testnet.threefold.io/threefoldfoundation'
5556 };
57+
5658 switch (_network) {
5759 case NetworkType .TESTNET :
5860 _sdk = StellarSDK .TESTNET ;
@@ -61,6 +63,9 @@ class Client {
6163 assetCode: 'TFT' ,
6264 issuer: "GA47YZA3PKFUZMPLQ3B5F2E3CJIB57TGGU7SPCQT2WAEYKN766PWIMB3" ,
6365 );
66+ usdc = currency.Currency (
67+ assetCode: 'USDC' ,
68+ issuer: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5' );
6469 break ;
6570 case NetworkType .PUBLIC :
6671 _sdk = StellarSDK .PUBLIC ;
@@ -69,10 +74,17 @@ class Client {
6974 assetCode: 'TFT' ,
7075 issuer: "GBOVQKJYHXRR3DX6NOX2RRYFRCUMSADGDESTDNBDS6CDVLGVESRTAC47" ,
7176 );
77+ usdc = currency.Currency (
78+ assetCode: 'USDC' ,
79+ issuer: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' );
7280 break ;
7381 }
7482
75- _currencies = currency.Currencies ({'TFT' : tft});
83+ _currencies = currency.Currencies ({
84+ 'TFT' : tft,
85+ 'USDC' : usdc,
86+ 'XLM' : currency.Currency (assetCode: 'XLM' , issuer: "" )
87+ });
7688 }
7789
7890 Future <bool > activateThroughThreefoldService () async {
@@ -130,10 +142,29 @@ class Client {
130142 }
131143 }
132144
133- Future <bool > addTrustLine () async {
145+ /// Adds trustline for all non-native assets in the `_currencies.currencies` map.
146+ ///
147+ /// Trustlines are required to hold non-native assets on a Stellar account.
148+ /// This function iterates over all available currencies and attempts to
149+ /// establish trustlines for each, except for the native asset (`XLM` ).
150+ ///
151+ /// **Note:** Adding trustline requires having XLM in the account
152+ ///
153+ /// ### Returns:
154+ /// - `true` if all trustlines were successfully added.
155+ /// - `false` if one or more trustlines failed.
156+ Future <bool > addConfiguredTrustlines () async {
157+ bool allTrustlinesAdded = true ;
158+
134159 for (var entry in _currencies.currencies.entries) {
135160 String currencyCode = entry.key;
136161 currency.Currency currentCurrency = entry.value;
162+ if (currencyCode == 'XLM' ) {
163+ logger.i ("Skipping trustline for native asset $currencyCode " );
164+ continue ;
165+ }
166+ logger.i (
167+ "Processing trustline for ${entry .key } with issuer ${entry .value .issuer }" );
137168
138169 String issuerAccountId = currentCurrency.issuer;
139170 Asset currencyAsset =
@@ -154,17 +185,26 @@ class Client {
154185
155186 if (! response.success) {
156187 logger.e ("Failed to add trustline for $currencyCode " );
157- return false ;
188+ allTrustlinesAdded = false ;
158189 } else {
159- logger.i ("trustline for $currencyCode was added successfully" );
160- return true ;
190+ logger.i ("Trustline for $currencyCode was added successfully" );
161191 }
162192 }
163193
164- logger.i ("No trustlines were processed" );
165- return false ;
194+ if (allTrustlinesAdded) {
195+ logger.i ("All trustlines were added successfully" );
196+ return true ;
197+ } else {
198+ logger.e ("One or more trustlines failed to be added" );
199+ return false ;
200+ }
166201 }
167202
203+ /// Transfers a specified amount of currency to a destination address.
204+ ///
205+ /// This function builds a Stellar transaction to send funds from the current account
206+ /// to a given recipient. It supports optional memo fields for additional transaction details.
207+ /// **Note:** Transfer requires having XLM in the account
168208 Future <bool > transfer (
169209 {required String destinationAddress,
170210 required String amount,
@@ -231,7 +271,6 @@ class Client {
231271 );
232272
233273 final data = jsonDecode (response.body);
234-
235274 String trustlineTransaction = data['addtrustline_transaction' ];
236275 XdrTransactionEnvelope xdrTxEnvelope =
237276 XdrTransactionEnvelope .fromEnvelopeXdrString (trustlineTransaction);
@@ -519,4 +558,269 @@ class Client {
519558 throw Exception ("Couldn't get memo text due to ${e }" );
520559 }
521560 }
561+
562+ Asset _getAsset (String assetCode) {
563+ if (assetCode == 'XLM' ) {
564+ return AssetTypeNative ();
565+ }
566+
567+ final asset = _currencies.currencies[assetCode];
568+ if (asset == null ) {
569+ throw Exception ('Asset $assetCode is not available' );
570+ }
571+
572+ return AssetTypeCreditAlphaNum4 (asset.assetCode, asset.issuer);
573+ }
574+
575+ /// Creates a DEX order by submitting a `ManageBuyOfferOperation` transaction.
576+ ///
577+ /// This function allows user to create an order to buy a specified asset
578+ /// using another asset on Stellar network.
579+ ///
580+ /// **Note:** Creating an order requires having XLM in the account
581+ /// to cover transaction fees and reserve requirements.
582+ ///
583+ /// **Price Format:**
584+ /// - The `price` should always include a leading zero for decimal values.
585+ /// - For example, instead of writing `.1` , the price should be written as `0.1` .
586+ /// - **Correct format**: `0.1`
587+ /// - **Incorrect format**: `.1`
588+ Future <bool > createOrder ({
589+ required String sellingAssetCode,
590+ required String buyingAssetCode,
591+ required String amount,
592+ required String price,
593+ String ? memo,
594+ }) async {
595+ if (! _currencies.currencies.containsKey (sellingAssetCode)) {
596+ throw Exception ('Sell asset $sellingAssetCode is not available.' );
597+ }
598+ if (! _currencies.currencies.containsKey (buyingAssetCode)) {
599+ throw Exception ('Buy asset $buyingAssetCode is not available.' );
600+ }
601+
602+ final Asset sellingAsset = _getAsset (sellingAssetCode);
603+ final Asset buyingAsset = _getAsset (buyingAssetCode);
604+
605+ final ManageSellOfferOperation sellOfferOperation =
606+ ManageSellOfferOperationBuilder (
607+ sellingAsset, buyingAsset, amount, price)
608+ .build ();
609+
610+ final account = await _sdk.accounts.account (accountId);
611+ final balances = account.balances;
612+
613+ try {
614+ Balance ? sellAssetBalance;
615+ Balance ? buyAssetBalance;
616+
617+ for (final balance in balances) {
618+ if (sellAssetBalance == null ) {
619+ if (sellingAssetCode == 'XLM' && balance.assetCode == null ) {
620+ sellAssetBalance = balance;
621+ } else if (balance.assetCode == sellingAssetCode) {
622+ sellAssetBalance = balance;
623+ }
624+ }
625+
626+ if (buyingAssetCode != 'XLM' && buyAssetBalance == null ) {
627+ if (balance.assetCode == buyingAssetCode) {
628+ buyAssetBalance = balance;
629+ }
630+ }
631+
632+ if (sellAssetBalance != null &&
633+ (buyingAssetCode == 'XLM' || buyAssetBalance != null )) {
634+ break ;
635+ }
636+ }
637+
638+ if (sellAssetBalance == null ) {
639+ logger.e ("Sell asset $sellingAssetCode not found in balances." );
640+ throw Exception ('Insufficient balance in $sellingAssetCode ' );
641+ }
642+
643+ if (buyingAssetCode != 'XLM' && buyAssetBalance == null ) {
644+ logger.e ("Buy asset $buyingAssetCode not found in balances." );
645+ throw Exception ('No trustline for $buyingAssetCode ' );
646+ }
647+
648+ final double sellAmount = double .parse (amount);
649+ final double availableBalance = double .parse (sellAssetBalance.balance);
650+
651+ if (sellAmount > availableBalance) {
652+ throw Exception (
653+ 'Insufficient balance in $sellingAssetCode . Available: $availableBalance ' );
654+ }
655+ } catch (e) {
656+ logger.e ("Error: ${e .toString ()}" );
657+ rethrow ;
658+ }
659+
660+ final Transaction transaction = TransactionBuilder (account)
661+ .addOperation (sellOfferOperation)
662+ .addMemo (memo != null ? Memo .text (memo) : Memo .none ())
663+ .build ();
664+
665+ transaction.sign (_keyPair, _stellarNetwork);
666+ try {
667+ final SubmitTransactionResponse response =
668+ await _sdk.submitTransaction (transaction);
669+ if (! response.success) {
670+ logger.e ('Transaction failed with result: ${response .resultXdr }' );
671+ return false ;
672+ }
673+ return true ;
674+ } catch (error) {
675+ throw Exception ('Transaction failed due to: ${error .toString ()}' );
676+ }
677+ }
678+
679+ /// Cancels a DEX order by submitting a `ManageBuyOfferOperation` transaction with zero amount.
680+ ///
681+ /// This function allows user to cancel previously created order with its offerId.
682+ ///
683+ /// **Note:** Cancelling an order requires having XLM in the account
684+ /// to cover transaction fees and reserve requirements.
685+ Future <bool > cancelOrder ({required String offerId}) async {
686+ final offers = (await _sdk.offers.forAccount (accountId).execute ()).records;
687+ final OfferResponse targetOffer = offers.firstWhere (
688+ (offer) => offer.id == offerId,
689+ orElse: () => throw Exception (
690+ 'Offer with ID $offerId not found in user\' s account.' ),
691+ );
692+
693+ final Asset sellingAsset = targetOffer.selling;
694+ final Asset buyingAsset = targetOffer.buying;
695+
696+ final ManageBuyOfferOperation cancelOfferOperation =
697+ ManageBuyOfferOperationBuilder (sellingAsset, buyingAsset, '0' , '1' )
698+ .setOfferId (offerId)
699+ .build ();
700+
701+ final account = await _sdk.accounts.account (accountId);
702+ final Transaction transaction =
703+ TransactionBuilder (account).addOperation (cancelOfferOperation).build ();
704+ transaction.sign (_keyPair, _stellarNetwork);
705+ try {
706+ final SubmitTransactionResponse response =
707+ await _sdk.submitTransaction (transaction);
708+ if (! response.success) {
709+ logger.e ('Transaction failed with result: ${response .resultXdr }' );
710+ return false ;
711+ }
712+ return true ;
713+ } catch (error) {
714+ throw Exception ('Transaction failed due to: ${error .toString ()}' );
715+ }
716+ }
717+
718+ /// Updating a DEX order by submitting a `ManageBuyOfferOperation` transaction.
719+ ///
720+ /// This function allows user to update previously created order by its offerId.
721+ ///
722+ /// **Note:** Updating an order requires having XLM in the account
723+ /// to cover transaction fees and reserve requirements.
724+ ///
725+ /// **Price Format:**
726+ /// - The `price` should always include a leading zero for decimal values.
727+ /// - For example, instead of writing `.1` , the price should be written as `0.1` .
728+ /// - **Correct format**: `0.1`
729+ /// - **Incorrect format**: `.1`
730+ Future <bool > updateOrder (
731+ {required String amount,
732+ required String price,
733+ required String offerId,
734+ String ? memo}) async {
735+ final offers = (await _sdk.offers.forAccount (accountId).execute ()).records;
736+ final OfferResponse ? targetOffer = offers.firstWhere (
737+ (offer) => offer.id == offerId,
738+ orElse: () => throw Exception (
739+ 'Offer with ID $offerId not found in user\' s account.' ),
740+ );
741+
742+ ManageBuyOfferOperation updateOfferOperation =
743+ ManageBuyOfferOperationBuilder (
744+ targetOffer! .selling,
745+ targetOffer.buying,
746+ amount,
747+ price,
748+ ).setOfferId (offerId).build ();
749+
750+ final account = await _sdk.accounts.account (accountId);
751+ final Transaction transaction =
752+ TransactionBuilder (account).addOperation (updateOfferOperation).build ();
753+ transaction.sign (_keyPair, _stellarNetwork);
754+ try {
755+ final SubmitTransactionResponse response =
756+ await _sdk.submitTransaction (transaction);
757+ if (! response.success) {
758+ logger.e ('Transaction failed with result: ${response .resultXdr }' );
759+ return false ;
760+ }
761+ return true ;
762+ } catch (error) {
763+ throw Exception ('Transaction failed due to: ${error .toString ()}' );
764+ }
765+ }
766+
767+ /// Lists all active offers created by the current account.
768+ ///
769+ /// This function fetches a list of `OfferResponse` objects representing
770+ /// open orders created by the account.
771+ ///
772+ /// ### Understanding Stellar Order Representation:
773+ /// - **Price (`OfferResponse.price` )**: Stellar stores price as `buying / selling` ,
774+ /// meaning the displayed price is the **inverse** of the price provided
775+ /// when creating an order.
776+ /// - **Amount (`OfferResponse.amount` )**: This represents the amount of the
777+ /// **buying asset** still available for trade, not the original amount
778+ /// of the selling asset.
779+ ///
780+ /// ### Conversion Formula:
781+ /// When placing an order:
782+ /// ```
783+ /// Total selling amount = Buying amount * Price
784+ /// ```
785+ ///
786+ /// Stellar inverts the price when storing the offer:
787+ /// ```
788+ /// Stored price = 1 / Provided price
789+ /// ```
790+ ///
791+ /// ### Example:
792+ /// #### **Creating an Order**
793+ /// ```dart
794+ /// await stellarClient.createOrder(
795+ /// sellingAssetCode: 'USDC',
796+ /// buyingAssetCode: 'TFT',
797+ /// amount: '5', // Buying 5 TFT
798+ /// price: '0.02'); // 1 USDC = 0.02 TFT
799+ /// ```
800+ ///
801+ /// #### **Retrieved Offer (from Stellar Order Book)**
802+ /// ```dart
803+ /// OfferResponse {
804+ /// amount: "0.2", // Total selling amount = 5 * 0.02 = 0.2 USDC
805+ /// price: "50.0" // Inverted: 1 / 0.02 = 50 USDC per TFT
806+ /// }
807+ /// ```
808+ ///
809+ /// **Key Takeaways:**
810+ /// - `OfferResponse.amount` = **Total amount of the selling asset left**.
811+ /// - `OfferResponse.price` = **Inverse of the provided price**.
812+ Future <List <OfferResponse >> listMyOffers () async {
813+ try {
814+ final offers = await _sdk.offers.forAccount (accountId).execute ();
815+
816+ if (offers.records.isEmpty) {
817+ logger.i ('No offers found for account: $accountId ' );
818+ return [];
819+ }
820+
821+ return offers.records;
822+ } catch (error) {
823+ throw Exception ('Error listing offers for account $accountId : $error ' );
824+ }
825+ }
522826}
0 commit comments