Skip to content
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

Support DEX orders on stellar client #119

Open
wants to merge 19 commits into
base: development
Choose a base branch
from
Open
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
311 changes: 303 additions & 8 deletions packages/stellar_client/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ 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;
String get secretSeed => _keyPair.secretSeed;
Uint8List? get privateKey => _keyPair.privateKey;

var logger = Logger(
printer: PrettyPrinter(),
printer: PrettyPrinter(
methodCount: 2,
errorMethodCount: 8,
lineLength: 120,
colors: true,
printEmojis: true,
printTime: true),
level: Level.debug,
filter: ProductionFilter(),
);

Client(this._network, String secretSeed) {
Expand Down Expand Up @@ -41,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 @@ -53,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 @@ -61,12 +79,19 @@ 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,
'XLM': currency.Currency(assetCode: 'XLM', issuer: "")
});
}

Future<bool> activateThroughThreefoldService() async {
Expand Down Expand Up @@ -125,9 +150,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 @@ -148,15 +181,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 @@ -222,7 +259,6 @@ class Client {
);

final data = jsonDecode(response.body);

String trustlineTransaction = data['addtrustline_transaction'];
XdrTransactionEnvelope xdrTxEnvelope =
XdrTransactionEnvelope.fromEnvelopeXdrString(trustlineTransaction);
Expand Down Expand Up @@ -473,4 +509,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');
}
}
}
Loading