Skip to content
Open
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
38 changes: 38 additions & 0 deletions lib/dto/ordinals/inscription_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,44 @@ class InscriptionData {
);
}

/// Parse the response from an ord server's /inscription/{id} endpoint.
/// [contentUrl] should be pre-built as `$baseUrl/content/$inscriptionId`.
factory InscriptionData.fromOrdJson(
Map<String, dynamic> json,
String contentUrl,
) {
final inscriptionId = json['inscription_id'] as String;
final satpoint = json['satpoint'] as String? ?? '';
// satpoint format: "txid:vout:offset"
final satpointParts = satpoint.split(':');
if (satpointParts.length < 2 || satpointParts[0].isEmpty) {
throw FormatException(
'Invalid satpoint for inscription $inscriptionId: "$satpoint"',
);
}
final output = '${satpointParts[0]}:${satpointParts[1]}';
final offset = satpointParts.length >= 3
? int.tryParse(satpointParts[2]) ?? 0
: 0;

return InscriptionData(
inscriptionId: inscriptionId,
inscriptionNumber: json['inscription_number'] as int? ?? 0,
address: json['address'] as String? ?? '',
preview: contentUrl,
content: contentUrl,
contentLength: json['content_length'] as int? ?? 0,
contentType: json['content_type'] as String? ?? '',
contentBody: '',
timestamp: json['timestamp'] as int? ?? 0,
genesisTransaction: inscriptionId.split('i').first,
location: satpoint,
output: output,
outputValue: json['output_value'] as int? ?? 0,
offset: offset,
);
}

@override
String toString() {
return 'InscriptionData {'
Expand Down
164 changes: 131 additions & 33 deletions lib/pages/ordinals/ordinal_details_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import '../../models/isar/models/blockchain_data/utxo.dart';
import '../../models/isar/ordinal.dart';
import '../../networking/http.dart';
import '../../notifications/show_flush_bar.dart';
import '../../pages/send_view/confirm_transaction_view.dart';
import '../../providers/db/main_db_provider.dart';
import '../../providers/global/prefs_provider.dart';
import '../../providers/global/wallets_provider.dart';
import '../../route_generator.dart';
import '../../services/tor_service.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/amount/amount.dart';
Expand All @@ -27,10 +30,14 @@ import '../../utilities/fs.dart';
import '../../utilities/show_loading.dart';
import '../../utilities/text_styles.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/ordinal_image.dart';
import '../../widgets/rounded_white_container.dart';
import 'widgets/dialogs.dart';

class OrdinalDetailsView extends ConsumerStatefulWidget {
const OrdinalDetailsView({
Expand Down Expand Up @@ -298,12 +305,7 @@ class _OrdinalImageGroup extends ConsumerWidget {
aspectRatio: 1,
child: Container(
color: Colors.transparent,
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
),
child: OrdinalImage(url: ordinal.content),
),
),
),
Expand Down Expand Up @@ -354,33 +356,129 @@ class _OrdinalImageGroup extends ConsumerWidget {
},
),
),
// const SizedBox(
// width: _spacing,
// ),
// Expanded(
// child: PrimaryButton(
// label: "Send",
// icon: SvgPicture.asset(
// Assets.svg.send,
// width: 10,
// height: 10,
// color: Theme.of(context)
// .extension<StackColors>()!
// .buttonTextPrimary,
// ),
// buttonHeight: ButtonHeight.l,
// iconSpacing: 4,
// onPressed: () async {
// final response = await showDialog<String?>(
// context: context,
// builder: (_) => const SendOrdinalUnfreezeDialog(),
// );
// if (response == "unfreeze") {
// // TODO: unfreeze and go to send ord screen
// }
// },
// ),
// ),
const SizedBox(width: _spacing),
Expanded(
child: PrimaryButton(
label: "Send",
icon: SvgPicture.asset(
Assets.svg.send,
width: 10,
height: 10,
color: Theme.of(
context,
).extension<StackColors>()!.buttonTextPrimary,
),
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
onPressed: () async {
final utxo = ordinal.getUTXO(ref.read(mainDBProvider));
if (utxo == null) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Could not find ordinal UTXO",
context: context,
),
);
return;
}

// Step 1: Confirm unfreeze
if (utxo.isBlocked) {
final unfreezeResponse = await showDialog<String?>(
context: context,
builder: (_) => const SendOrdinalUnfreezeDialog(),
);
if (unfreezeResponse != "unfreeze") return;
}

if (!context.mounted) return;

// Step 2: Get recipient address
final address = await showDialog<String?>(
context: context,
builder: (_) => OrdinalRecipientAddressDialog(
inscriptionNumber: ordinal.inscriptionNumber,
),
);
if (address == null || address.isEmpty) return;

// Validate address
final wallet = ref.read(pWallets).getWallet(walletId);
if (!wallet.cryptoCurrency.validateAddress(address)) {
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Invalid address",
context: context,
),
);
}
return;
}

if (!context.mounted) return;

// Step 3: Prepare the transaction
final OrdinalsInterface? ordinalsWallet =
wallet is OrdinalsInterface ? wallet : null;
if (ordinalsWallet == null) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Wallet does not support ordinals",
context: context,
),
);
return;
}

bool didError = false;
final txData = await showLoading(
whileFuture: ordinalsWallet.prepareOrdinalSend(
ordinalUtxo: utxo,
recipientAddress: address,
),
context: context,
rootNavigator: true,
message: "Preparing transaction...",
onException: (e) {
didError = true;
String msg = e.toString();
while (msg.isNotEmpty && msg.startsWith("Exception:")) {
msg = msg.substring(10).trim();
}
if (context.mounted) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: msg,
context: context,
);
}
},
);

if (didError || txData == null || !context.mounted) return;

// Step 4: Navigate to confirm transaction view
await Navigator.of(context).push(
RouteGenerator.getRoute<void>(
shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute,
builder: (_) => ConfirmTransactionView(
walletId: walletId,
txData: txData,
onSuccess: () {},
),
settings: const RouteSettings(
name: ConfirmTransactionView.routeName,
),
),
);
},
),
),
],
),
],
Expand Down
Loading
Loading