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

Add API to get last oracle value #90

Merged
merged 5 commits into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Changelog
* Implement new JSON RPC Blockchain's API (sendTx, getTransactionFee and addOriginKey methods)
* Add a method for calling smart contract functions
* Upgrade dart version to 3.1 and update dependencies
* Named actions recipients
* BREAKING-CHANGE - New transaction structure version 2 (if FeatureFlags.txVersion2 == true)
* Add API to get last oracle value

#### Version 3.2.0
* Add utility methods to handle message exchange mechanisms within the context of messaging.
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,20 @@ It supports the Archethic Cryptography rules which are:
// 00b1d3750edb9381c96b1a975a55b5b4e4fb37bfab104c10b0b6c9a00433ec4646
```

#### getOracleData(timestamp)
Fetch the OracleChain data
return a value of Oracle Uco_Price in {OracleUcoPrice} from a timestamp
If timestamp = 0 or not precised, the last price is returned
- `timestamp`: UNIX timestamp (optional)

```dart
import 'package:archethic_lib_dart/archethic_lib_dart.dart';

final oracleData =
await OracleService('https://testnet.archethic.net').getOracleData(timestamp);
final eurConversionRate = oracleUcoPrice.uco?.eur;
final usdConversionRate = oracleUcoPrice.uco?.usd;
```
#### getTransactionFee(tx)
Query a node to fetch the tx fee for a given transaction
Calls the 'estimate_transaction_fee' JSON-RPC 2.0 method
Expand All @@ -305,6 +319,29 @@ It supports the Archethic Cryptography rules which are:
TransactionFee transactionFee = await ApiService('https://testnet.archethic.net').getTransactionFee(tx);
```

#### subscribeToOracleUpdates(handler)
Subscribe to get the real time updates of the OracleChain
- `handler`: Callback to handle the new data

```dart
import 'package:archethic_lib_dart/archethic_lib_dart.dart';

await OracleService('https://testnet.archethic.net')
.subscribeToOracleUpdates((data) {
log('Oracle value: ${data.timestamp} - ${data.uco!.usd} USD');
// TODO with
{
timestamp: ...,
services: {
uco: {
eur: ...,
usd: ...
}
}
}
});
```

#### getTransactionOwnerships(addresses)
Query a node to find the ownerships (secrets and authorized keys) to given transactions addresses

Expand Down
29 changes: 29 additions & 0 deletions lib/src/services/oracle_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:archethic_lib_dart/src/model/oracle_chain/oracle_uco_price.dart'
import 'package:archethic_lib_dart/src/model/response/oracle_data_response.dart';
import 'package:archethic_lib_dart/src/model/uco.dart';
import 'package:archethic_lib_dart/src/utils/logs.dart';
import 'package:archethic_lib_dart/src/utils/oracle/archethic_oracle.dart';
import 'package:http/http.dart' as http show post;

class OracleService {
Expand Down Expand Up @@ -58,6 +59,7 @@ class OracleService {
oracleDataResponse.data!.oracleData!.services != null &&
oracleDataResponse.data!.oracleData!.services!.uco != null) {
oracleUcoPrice = OracleUcoPrice(
timestamp: oracleDataResponse.data!.oracleData!.timestamp,
uco: Uco(
eur: oracleDataResponse.data!.oracleData!.services!.uco!.eur,
usd: oracleDataResponse.data!.oracleData!.services!.uco!.usd,
Expand All @@ -75,4 +77,31 @@ class OracleService {
completer.complete(oracleUcoPrice);
return completer.future;
}

/// Subscribe to be notified when a new oracle data is stored
Future<void> subscribeToOracleUpdates(
Function(OracleUcoPrice?) onUpdate,
) async {
String websocketEndpoint;
switch (endpoint) {
case 'https://mainnet.archethic.net':
case 'https://testnet.archethic.net':
websocketEndpoint =
"${endpoint!.replaceAll('https:', 'wss:').replaceAll('http:', 'wss:')}/socket/websocket";
break;
default:
websocketEndpoint =
"${endpoint!.replaceAll('https:', 'wss:').replaceAll('http:', 'ws:')}/socket/websocket";
break;
}

final oracleRepository = ArchethicOracle(
phoenixHttpEndpoint: '$endpoint/socket/websocket',
websocketEndpoint: websocketEndpoint,
);

await oracleRepository.subscribeToOracleUpdates(
onUpdate: onUpdate,
);
}
}
120 changes: 120 additions & 0 deletions lib/src/utils/oracle/archethic_oracle.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'dart:async';
import 'dart:developer';
import 'package:archethic_lib_dart/archethic_lib_dart.dart';
import 'package:graphql/client.dart';
import 'package:phoenix_socket/phoenix_socket.dart';

class ArchethicOracle {
ArchethicOracle({
required this.phoenixHttpEndpoint,
required this.websocketEndpoint,
});

final String phoenixHttpEndpoint;
final String websocketEndpoint;

PhoenixChannel? _channel;
GraphQLClient? _client;

StreamSubscription? _oracleUpdatesSubscription;
Timer? _timer;

void close() {
_timer?.cancel();
_channel?.close();
_oracleUpdatesSubscription?.cancel();
}

/// Subscribe to oracle updates
Future<void> subscribeToOracleUpdates({
Function(OracleUcoPrice?)? onUpdate,
}) async {
await _connect(
phoenixHttpEndpoint,
websocketEndpoint,
);

_listenOracleUpdates(
onUpdate!,
);
}

Future<void> _connect(
String phoenixHttpEndpoint,
String websocketEndpoint,
) async {
assert(
_client == null,
'Connection already established. That instance of [SubscriptionChannel] must not be reused.',
);

final phoenixHttpLink = HttpLink(
phoenixHttpEndpoint,
);

_channel = await PhoenixLink.createChannel(
websocketUri: websocketEndpoint,
);
final phoenixLink = PhoenixLink(
channel: _channel!,
);

final link = Link.split(
(request) => request.isSubscription,
phoenixLink,
phoenixHttpLink,
);
_client = GraphQLClient(
link: link,
cache: GraphQLCache(),
);
}

Stream<QueryResult<T>> _subscribe<T>(String operation) async* {
assert(
_client != null,
'You must call [connect] before [subscribing].',
);
final subscriptionDocument = gql(operation);
yield* _client!.subscribe(
SubscriptionOptions(
document: subscriptionDocument,
),
);
}

void _listenOracleUpdates(
Function(OracleUcoPrice?) onUpdate,
) {
_oracleUpdatesSubscription = _subscribe<OracleUcoPrice>(
'subscription { oracleUpdate { timestamp, services { uco { eur, usd } } } }',
).listen(
(result) async {
log('Oracle value: ${result.timestamp}');
final oracleUcoPrice = _oracleUcoPriceDtoToModel(data: result.data);

log(
'>>> Oracle update <<< ($oracleUcoPrice)',
);
await onUpdate(
oracleUcoPrice,
);
},
);
}

OracleUcoPrice? _oracleUcoPriceDtoToModel({
Map<String, dynamic>? data,
}) {
final oracleUpdate = data?['oracleUpdate'];
if (oracleUpdate == null) return null;

return OracleUcoPrice(
timestamp: oracleUpdate?['timestamp'],
uco: Uco(
eur: oracleUpdate?['services']['uco']['eur'],
usd: oracleUpdate?['services']['uco']['usd'],
),
);
}
}
27 changes: 27 additions & 0 deletions test/oracle_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
library test.oracle_test;

// Project imports:
import 'dart:async';
import 'dart:developer';

import 'package:archethic_lib_dart/src/services/oracle_service.dart';
// Package imports:
import 'package:test/test.dart';
Expand All @@ -21,6 +24,30 @@ void main() {
expect(oracleUcoPrice.uco!.eur, oracleUcoPrice2.uco!.eur);
expect(oracleUcoPrice.uco!.usd, oracleUcoPrice2.uco!.usd);
});

test('subscribeToOracleUpdates', () async {
final completer = Completer<void>();

await OracleService('https://mainnet.archethic.net')
.subscribeToOracleUpdates((data) {
if (data == null) {
log('Oracle value null');
} else {
log('Oracle value: ${data.timestamp} - ${data.uco!.usd} USD');
}

if (!completer.isCompleted) {
completer.complete();
}
});

await Future.delayed(const Duration(minutes: 1), () {
if (!completer.isCompleted) {
log('Timeout reached');
completer.complete();
}
});
});
},
tags: <String>['noCI'],
);
Expand Down