Merge branch 'litescribe' into origin/ordinals

This commit is contained in:
sneurlax 2023-07-21 16:35:25 -05:00
commit cdddf5adc5
13 changed files with 400 additions and 106 deletions

View file

@ -0,0 +1,39 @@
import 'package:stackwallet/dto/ordinals/litescribe_response.dart';
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
class AddressInscriptionResponse extends LitescribeResponse<AddressInscriptionResponse> {
final int status;
final String message;
final AddressInscriptionResult result;
AddressInscriptionResponse({
required this.status,
required this.message,
required this.result,
});
factory AddressInscriptionResponse.fromJson(Map<String, dynamic> json) {
return AddressInscriptionResponse(
status: json['status'] as int,
message: json['message'] as String,
result: AddressInscriptionResult.fromJson(json['result'] as Map<String, dynamic>),
);
}
}
class AddressInscriptionResult {
final List<InscriptionData> list;
final int total;
AddressInscriptionResult({
required this.list,
required this.total,
});
factory AddressInscriptionResult.fromJson(Map<String, dynamic> json) {
return AddressInscriptionResult(
list: (json['list'] as List).map((item) => InscriptionData.fromJson(item as Map<String, dynamic>)).toList(),
total: json['total'] as int,
);
}
}

View file

@ -0,0 +1,53 @@
// inscription data from litescribe /address/inscriptions endpoint
class InscriptionData {
final String inscriptionId;
final int inscriptionNumber;
final String address;
final String preview;
final String content;
final int contentLength;
final String contentType;
final String contentBody;
final int timestamp;
final String genesisTransaction;
final String location;
final String output;
final int outputValue;
final int offset;
InscriptionData({
required this.inscriptionId,
required this.inscriptionNumber,
required this.address,
required this.preview,
required this.content,
required this.contentLength,
required this.contentType,
required this.contentBody,
required this.timestamp,
required this.genesisTransaction,
required this.location,
required this.output,
required this.outputValue,
required this.offset,
});
factory InscriptionData.fromJson(Map<String, dynamic> json) {
return InscriptionData(
inscriptionId: json['inscriptionId'] as String,
inscriptionNumber: json['inscriptionNumber'] as int,
address: json['address'] as String,
preview: json['preview'] as String,
content: json['content'] as String,
contentLength: json['contentLength'] as int,
contentType: json['contentType'] as String,
contentBody: json['contentBody'] as String,
timestamp: json['timestamp'] as int,
genesisTransaction: json['genesisTransaction'] as String,
location: json['location'] as String,
output: json['output'] as String,
outputValue: json['outputValue'] as int,
offset: json['offset'] as int,
);
}
}

View file

@ -0,0 +1,6 @@
class LitescribeResponse<T> {
final T? data;
final String? error;
LitescribeResponse({this.data, this.error});
}

View file

@ -1,26 +1,29 @@
enum OrdCollection {
punks,
moonbirds,
}
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
class Ordinal {
final String name;
final String inscription;
final String rank;
final OrdCollection collection;
final String inscriptionId;
final int inscriptionNumber;
final String content;
// following two are used to look up the UTXO object in isar combined w/ walletId
final String utxoTXID;
final int utxoVOUT;
// TODO: make a proper Isar class instead of this placeholder
Ordinal({
required this.name,
required this.inscription,
required this.rank,
required this.collection,
required this.inscriptionId,
required this.inscriptionNumber,
required this.content,
required this.utxoTXID,
required this.utxoVOUT,
});
}
factory Ordinal.fromInscriptionData(InscriptionData data) {
return Ordinal(
inscriptionId: data.inscriptionId,
inscriptionNumber: data.inscriptionNumber,
content: data.content,
utxoTXID: data.output.split(':')[0], // "output": "062f32e21aa04246b8873b5d9a929576addd0339881e1ea478b406795d6b6c47:0"
utxoVOUT: int.parse(data.output.split(':')[1]),
);
}
}

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
import 'package:stackwallet/models/ordinal.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart';
@ -17,10 +18,10 @@ import 'package:stackwallet/widgets/rounded_white_container.dart';
class OrdinalDetailsView extends StatefulWidget {
const OrdinalDetailsView({
super.key,
Key? key,
required this.walletId,
required this.ordinal,
});
}) : super(key: key);
final String walletId;
final Ordinal ordinal;
@ -28,7 +29,7 @@ class OrdinalDetailsView extends StatefulWidget {
static const routeName = "/ordinalDetailsView";
@override
State<OrdinalDetailsView> createState() => _OrdinalDetailsViewState();
_OrdinalDetailsViewState createState() => _OrdinalDetailsViewState();
}
class _OrdinalDetailsViewState extends State<OrdinalDetailsView> {
@ -40,10 +41,10 @@ class _OrdinalDetailsViewState extends State<OrdinalDetailsView> {
child: SafeArea(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
Theme.of(context).extension<StackColors>()!.background,
leading: const AppBarBackButton(),
title: Text(
"Ordinal details",
@ -67,14 +68,14 @@ class _OrdinalDetailsViewState extends State<OrdinalDetailsView> {
),
_DetailsItemWCopy(
title: "Inscription number",
data: widget.ordinal.inscription,
data: widget.ordinal.inscriptionNumber.toString(),
),
const SizedBox(
height: _spacing,
),
_DetailsItemWCopy(
title: "Rank",
data: widget.ordinal.rank,
title: "ID",
data: widget.ordinal.inscriptionId,
),
const SizedBox(
height: _spacing,
@ -83,23 +84,23 @@ class _OrdinalDetailsViewState extends State<OrdinalDetailsView> {
const SizedBox(
height: _spacing,
),
_DetailsItemWCopy(
const _DetailsItemWCopy(
title: "Amount",
data: "FIXME",
data: "TODO", // TODO infer from utxo utxoTXID:utxoVOUT
),
const SizedBox(
height: _spacing,
),
_DetailsItemWCopy(
const _DetailsItemWCopy(
title: "Owner address",
data: "FIXME",
data: "TODO", // infer from address associated w utxoTXID
),
const SizedBox(
height: _spacing,
),
_DetailsItemWCopy(
title: "Transaction ID",
data: "FIXME",
data: widget.ordinal.utxoTXID,
),
const SizedBox(
height: _spacing,
@ -116,10 +117,10 @@ class _OrdinalDetailsViewState extends State<OrdinalDetailsView> {
class _DetailsItemWCopy extends StatelessWidget {
const _DetailsItemWCopy({
super.key,
Key? key,
required this.title,
required this.data,
});
}) : super(key: key);
final String title;
final String data;
@ -153,7 +154,7 @@ class _DetailsItemWCopy extends StatelessWidget {
child: SvgPicture.asset(
Assets.svg.copy,
color:
Theme.of(context).extension<StackColors>()!.infoItemIcons,
Theme.of(context).extension<StackColors>()!.infoItemIcons,
width: 12,
),
),
@ -174,10 +175,10 @@ class _DetailsItemWCopy extends StatelessWidget {
class _OrdinalImageGroup extends StatelessWidget {
const _OrdinalImageGroup({
super.key,
Key? key,
required this.walletId,
required this.ordinal,
});
}) : super(key: key);
final String walletId;
final Ordinal ordinal;
@ -190,17 +191,25 @@ class _OrdinalImageGroup extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
ordinal.name,
style: STextStyles.w600_16(context),
),
const SizedBox(
height: _spacing,
),
// Text(
// "${ordinal.inscriptionId}", // Use any other property you want
// style: STextStyles.w600_16(context),
// ),
// const SizedBox(
// height: _spacing,
// ),
AspectRatio(
aspectRatio: 1,
child: Container(
color: Colors.red,
child: AspectRatio(
aspectRatio: 1,
child: Container(
color: Colors.red,
child: Image.network(
ordinal.content, // Use the preview URL as the image source
fit: BoxFit.cover,
filterQuality: FilterQuality.none, // Set the filter mode to nearest
),
),
),
),
const SizedBox(

View file

@ -14,6 +14,8 @@ import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/ordinal.dart';
import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart';
import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/mixins/ordinals_interface.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -44,14 +46,24 @@ class _OrdinalsViewState extends ConsumerState<OrdinalsView> {
late final FocusNode searchFocus;
String _searchTerm = "";
dynamic _manager;
@override
void initState() {
searchController = TextEditingController();
searchFocus = FocusNode();
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Set _manager here when the widget's dependencies change
_manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId)));
}
@override
void dispose() {
searchController.dispose();
@ -87,8 +99,8 @@ class _OrdinalsViewState extends ConsumerState<OrdinalsView> {
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: () {
// todo refresh
onPressed: () async {
(_manager.wallet as OrdinalsInterface).refreshInscriptions();
},
),
),
@ -181,14 +193,7 @@ class _OrdinalsViewState extends ConsumerState<OrdinalsView> {
Expanded(
child: OrdinalsList(
walletId: widget.walletId,
ordinals: [
for (int i = 0; i < 13; i++)
Ordinal(
name: "dummy name $i",
inscription: "insc$i",
rank: "r$i",
),
],
ordinalsFuture: (_manager.wallet as OrdinalsInterface).getOrdinals(),
),
),
],

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/models/ordinal.dart';
import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -6,10 +7,10 @@ import 'package:stackwallet/widgets/rounded_white_container.dart';
class OrdinalCard extends StatelessWidget {
const OrdinalCard({
super.key,
Key? key,
required this.walletId,
required this.ordinal,
});
}) : super(key: key);
final String walletId;
final Ordinal ordinal;
@ -27,27 +28,27 @@ class OrdinalCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AspectRatio(
aspectRatio: 1,
child: Container(
AspectRatio(
aspectRatio: 1,
child: Container(
color: Colors.red,
child: const Center(
child: Text(
"replace red container with image",
),
child: Image.network(
ordinal.content, // Use the preview URL as the image source
fit: BoxFit.cover,
filterQuality: FilterQuality.none, // Set the filter mode to nearest
),
),
),
const Spacer(),
Text(
ordinal.name,
'INSC. ${ordinal.inscriptionNumber}', // infer from address associated with utxoTXID
style: STextStyles.w500_12(context),
),
const Spacer(),
Text(
"INSC. ${ordinal.inscription} RANK ${ordinal.rank}",
style: STextStyles.w500_8(context),
),
// const Spacer(),
// Text(
// "ID ${ordinal.inscriptionId}",
// style: STextStyles.w500_8(context),
// ),
],
),
);

View file

@ -1,39 +1,49 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/models/ordinal.dart';
import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart';
class OrdinalsList extends StatefulWidget {
class OrdinalsList extends StatelessWidget {
const OrdinalsList({
super.key,
Key? key,
required this.walletId,
required this.ordinals,
});
required this.ordinalsFuture,
}) : super(key: key);
final String walletId;
final List<Ordinal> ordinals;
final Future<List<Ordinal>> ordinalsFuture;
@override
State<OrdinalsList> createState() => _OrdinalsListState();
}
class _OrdinalsListState extends State<OrdinalsList> {
static const spacing = 10.0;
get spacing => 2.0;
@override
Widget build(BuildContext context) {
return GridView.builder(
shrinkWrap: true,
itemCount: widget.ordinals.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
crossAxisCount: 2,
childAspectRatio: 3 / 4,
),
itemBuilder: (_, i) => OrdinalCard(
walletId: widget.walletId,
ordinal: widget.ordinals[i],
),
return FutureBuilder<List<Ordinal>>(
future: ordinalsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
final List<Ordinal> ordinals = snapshot.data!;
return GridView.builder(
shrinkWrap: true,
itemCount: ordinals.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: spacing as double,
mainAxisSpacing: spacing as double,
crossAxisCount: 2,
childAspectRatio: 6 / 7, // was 3/4, less data displayed now
),
itemBuilder: (_, i) => OrdinalCard(
walletId: walletId,
ordinal: ordinals[i],
),
);
} else {
return const Text('No data found.');
}
},
);
}
}
}

View file

@ -12,13 +12,15 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart';
import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart';
import 'package:stackwallet/models/buy/response_objects/quote.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/isar/models/contact_entry.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/ordinal.dart';
import 'package:stackwallet/models/ordinal.dart'; // TODO generalize InscriptionData -> Ordinal
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart';
@ -168,8 +170,6 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/widgets/choose_coin_view.dart';
import 'package:tuple/tuple.dart';
import 'models/isar/models/contact_entry.dart';
/*
* This file contains all the routes for the app.
* To add a new route, add it to the switch statement in the generateRoute method.

View file

@ -136,6 +136,7 @@ class LitecoinWallet extends CoinServiceAPI
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initOrdinalsInterface(walletId:walletId, coin: coin, db: db);
initCoinControlInterface(
walletId: walletId,
walletName: walletName,

View file

@ -0,0 +1,68 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
import 'package:stackwallet/dto/ordinals/litescribe_response.dart';
class LitescribeAPI {
static final LitescribeAPI _instance = LitescribeAPI._internal();
factory LitescribeAPI({required String baseUrl}) {
_instance.baseUrl = baseUrl;
return _instance;
}
LitescribeAPI._internal();
late String baseUrl;
Future<LitescribeResponse> _getResponse(String endpoint) async {
final response = await http.get(Uri.parse('$baseUrl$endpoint'));
if (response.statusCode == 200) {
return LitescribeResponse(data: _validateJson(response.body));
} else {
throw Exception('LitescribeAPI _getResponse exception: Failed to load data');
}
}
Map<String, dynamic> _validateJson(String responseBody) {
final parsed = jsonDecode(responseBody);
if (parsed is Map<String, dynamic>) {
return parsed;
} else {
throw const FormatException('LitescribeAPI _validateJson exception: Invalid JSON format');
}
}
Future<List<InscriptionData>> getInscriptionsByAddress(String address, {int cursor = 0, int size = 1000}) async {
// size param determines how many inscriptions are returned per response
// default of 1000 is used to cover most addresses (I assume)
// if the total number of inscriptions at the address exceeds the length of the list of inscriptions returned, another call with a higher size is made
final int defaultLimit = 1000;
final response = await _getResponse('/address/inscriptions?address=$address&cursor=$cursor&size=$size');
// Check if the number of returned inscriptions equals the limit
final list = response.data['result']['list'];
final int total = response.data['result']['total'] as int;
final int currentSize = list.length as int;
if (currentSize == size && currentSize < total) {
// If the number of returned inscriptions equals the limit and there are more inscriptions available,
// increment the cursor and make the next API call to fetch the remaining inscriptions.
final int newCursor = cursor + size;
return getInscriptionsByAddress(address, cursor: newCursor, size: size);
} else {
try {
// Iterate through the list and create InscriptionData objects from each element
final List<InscriptionData> inscriptions = (list as List<dynamic>)
.map((json) => InscriptionData.fromJson(json as Map<String, dynamic>))
.toList();
return inscriptions;
} catch (e) {
throw const FormatException('LitescribeAPI getInscriptionsByAddress exception: AddressInscriptionResponse.fromJson failure');
}
}
}
}

View file

@ -1,3 +1,111 @@
import 'dart:async';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/ordinal.dart';
import 'package:stackwallet/services/litescribe_api.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
mixin OrdinalsInterface {
// TODO wallet ordinals functionality
}
late final String _walletId;
late final Coin _coin;
late final MainDB _db;
void initOrdinalsInterface({
required String walletId,
required Coin coin,
required MainDB db,
}) {
print('init');
_walletId = walletId;
_coin = coin;
_db = db;
}
final LitescribeAPI litescribeAPI = LitescribeAPI(baseUrl: 'https://litescribe.io/api');
void refreshInscriptions() async {
List<dynamic> _inscriptions;
final utxos = await _db.getUTXOs(_walletId).findAll();
final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos);
_inscriptions = await getInscriptionDataFromAddresses(uniqueAddresses);
// TODO save inscriptions to isar which gets watched by a FutureBuilder/StreamBuilder
}
Future<List<InscriptionData>> getInscriptionData() async {
try {
final utxos = await _db.getUTXOs(_walletId).findAll();
final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos);
return await getInscriptionDataFromAddresses(uniqueAddresses);
} catch (e) {
throw Exception('Error in OrdinalsInterface getInscriptions: $e');
}
}
Future<List<Ordinal>> getOrdinals() async {
try {
final utxos = await _db.getUTXOs(_walletId).findAll();
final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos);
return await getOrdinalsFromAddresses(uniqueAddresses);
} catch (e) {
throw Exception('Error in OrdinalsInterface getOrdinals: $e');
}
}
List<String> getUniqueAddressesFromUTXOs(List<UTXO> utxos) {
final Set<String> uniqueAddresses = <String>{};
for (var utxo in utxos) {
if (utxo.address != null) {
uniqueAddresses.add(utxo.address!);
}
}
return uniqueAddresses.toList();
}
Future<List<InscriptionData>> getInscriptionDataFromAddress(String address) async {
List<InscriptionData> allInscriptions = [];
try {
var inscriptions = await litescribeAPI.getInscriptionsByAddress(address);
allInscriptions.addAll(inscriptions);
} catch (e) {
throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e');
}
return allInscriptions;
}
Future<List<InscriptionData>> getInscriptionDataFromAddresses(List<String> addresses) async {
List<InscriptionData> allInscriptions = [];
for (String address in addresses) {
try {
var inscriptions = await litescribeAPI.getInscriptionsByAddress(address);
allInscriptions.addAll(inscriptions);
} catch (e) {
print("Error fetching inscriptions for address $address: $e");
}
}
return allInscriptions;
}
Future<List<Ordinal>> getOrdinalsFromAddress(String address) async {
try {
var inscriptions = await litescribeAPI.getInscriptionsByAddress(address);
return inscriptions.map((data) => Ordinal.fromInscriptionData(data)).toList();
} catch (e) {
throw Exception('Error in OrdinalsInterface getOrdinalsFromAddress: $e');
}
}
Future<List<Ordinal>> getOrdinalsFromAddresses(List<String> addresses) async {
List<Ordinal> allOrdinals = [];
for (String address in addresses) {
try {
var inscriptions = await litescribeAPI.getInscriptionsByAddress(address);
allOrdinals.addAll(inscriptions.map((data) => Ordinal.fromInscriptionData(data)));
} catch (e) {
print("Error fetching inscriptions for address $address: $e");
}
}
return allOrdinals;
}
}

View file

@ -1,9 +0,0 @@
import 'package:stackwallet/models/ordinal.dart';
class OrdinalsAPI {
// dummy class with sample functions to be changed / filled out
static Future<List<Ordinal>> fetch() async {
return [];
}
}