gs-ur-dart

This repository contains the Flutter SDK for interacting with Goldshell's GSWallet. GSWallet is a hardware wallet that securely transmits information via QR codes. This SDK allows developers to integrate GSWallet functionality into their applications.

Additional Libraries

This library offers the same functionalities as gs-ur-dart, but is implemented in JavaScript. Both libraries provide the same features for interacting with GSWallet.

Features

  • Secure QR Code Communication: Enables secure communication with GSWallet using QR codes.
  • Multi-Currency Support: Supports various cryptocurrencies (currently under development, examples below show BTC and ETH).
  • Transaction Signing: Allows parsing signature of transactions from the GSWallet device.

Getting Started

These instructions will guide you through setting up and using the gs-ur-dart SDK.

Prerequisites

  • Flutter SDK and Dart installed.

Installation

Choose your preferred package manager:

// pubspec.yaml
dependencies:
  gs_ur_dart: ^1.0.0

// bash
flutter pub get

// In your Dart file, import library
import 'package:gs_ur_dart/gs_ur_dart.dart';

QRcode Example

Generating UR QR Codes

import 'dart:async';

import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gs_ur_dart/gs_ur_dart.dart';
import 'package:qr_flutter/qr_flutter.dart';

abstract class _State {}

class _InitialState extends _State {}

class _AnimatedQRDataState extends _State {
  final String data;
  _AnimatedQRDataState(this.data);
}

class _Cubit extends Cubit<_State> {
  final UREncoder urEncoder;
  final AnimatedQRCodeStyle style;

  late String _currentQR;
  late Timer timer;

  _Cubit(this.urEncoder, this.style) : super(_InitialState());

  void initial() {
    _currentQR = urEncoder.nextPart();
    emit(_AnimatedQRDataState(_currentQR));
    timer = Timer.periodic(const Duration(milliseconds: 250), (_) {
      _currentQR = urEncoder.nextPart();
      emit(_AnimatedQRDataState(_currentQR));
    });
  }

  @override
  Future<void> close() async {
    timer.cancel();
    super.close();
  }

  String get currentQR => _currentQR;
}

class AnimatedQRCodeStyle {
  final double size;

  AnimatedQRCodeStyle({
    this.size = 300,
  });

  const AnimatedQRCodeStyle.factory() : size = 300;
}

class AnimatedQRCode extends StatelessWidget {
  final UREncoder urEncoder;
  final AnimatedQRCodeStyle style;

  const AnimatedQRCode(
      {Key? key,
      required this.urEncoder,
      this.style = const AnimatedQRCodeStyle.factory()})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => _Cubit(urEncoder, style),
      child: const _AnimatedQRCode(),
    );
  }
}

class _AnimatedQRCode extends StatefulWidget {
  const _AnimatedQRCode({Key? key}) : super(key: key);

  @override
  _AnimatedQRCodeState createState() => _AnimatedQRCodeState();
}

class _AnimatedQRCodeState extends State<_AnimatedQRCode> {
  _AnimatedQRCodeState();

  late _Cubit _cubit;

  @override
  void initState() {
    _cubit = BlocProvider.of(context);
    _cubit.initial();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<_Cubit, _State>(builder: (context, state) {
      if (state is _AnimatedQRDataState) {
        return QrImageView(
          data: state.data,
          size: _cubit.style.size,
          backgroundColor: const Color(0xFFFFFFFF),
        );
      }
      return QrImageView(
        data: _cubit.currentQR,
        size: _cubit.style.size,
        backgroundColor: const Color(0xFFFFFFFF),
      );
    });
  }

  @override
  void dispose() {
    _cubit.close();
    super.dispose();
  }
}


Scanning QR Codes and Parsing UR Results

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get/get.dart';
import 'package:gs_ur_dart/gs_ur_dart.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

extension FxSize on num {
  /// Auto adjust width with system text size setting. It is for vertical space size to auto changed with setting.
  double get aw => Get.mediaQuery.textScaler.scale(w);
}

class _State {
  final String? code;
  final String process;
  final double progress;

  _State({this.code, this.process = "0", this.progress = 0});
}

class _InitialState extends _State {}

typedef SuccessCallback = void Function(UR);
typedef FailureCallback = void Function(String);
typedef OnProcessed = void Function(String, String);

class _Cubit extends Cubit<_State> {
  // late final RegistryType target;
  final SuccessCallback onSuccess;
  final FailureCallback onFailed;
  // final OnProcessed? onProcessed;
  final QrScannerOverlayShape? overlay;
  URDecoder urDecoder = URDecoder();
  bool succeed = false;
  String? data;
  String process = "0";
  double progress = 0;

  _Cubit(
    // this.target,
    this.onSuccess,
    this.onFailed, {
    this.overlay,
    // this.onProcessed,
  }) : super(_InitialState());

  void receiveQRCode(String? code) {
    try {
      if (code != null) {
        data = code;
        // print("code: >>> $code");
        emit(_State(code: code));
        urDecoder.receivePart(code);
        progress = urDecoder.getProgress();
        process = (urDecoder.getProgress() * 100).toStringAsFixed(2);
        // if(onProcessed != null) onProcessed!(code, process);
        emit(_State(process: process, progress: progress));
        if (urDecoder.isComplete()) {
          final result = urDecoder.resultUR();
          if (!succeed) {
            onSuccess(result);
            succeed = true;
          }
        }
      }
    } catch (e) {
      onFailed("Error when receiving UR $e");
      reset();
      throw e;
    }
  }

  void reset() {
    urDecoder = URDecoder();
    succeed = false;
  }
}

class AnimatedQRScanner extends StatelessWidget {
  // final RegistryType target;
  final SuccessCallback onSuccess;
  final FailureCallback onFailed;
  // final OnProcessed? onProcessed;
  final QrScannerOverlayShape? overlay;

  const AnimatedQRScanner(
      {Key? key,
      // required this.target,
      required this.onSuccess,
      required this.onFailed,
      // this.onProcessed,
      this.overlay})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) =>
          _Cubit(onSuccess, onFailed, overlay: overlay),
      child: _AnimatedQRScanner(),
    );
  }
}

class _AnimatedQRScanner extends StatefulWidget {
  @override
  _AnimatedQRScannerState createState() => _AnimatedQRScannerState();
}

class _AnimatedQRScannerState extends State<_AnimatedQRScanner> {
  final GlobalKey<State<StatefulWidget>> keyQr = GlobalKey(debugLabel: 'QR');
  QRViewController? controller;
  late final _Cubit _cubit;

  @override
  void initState() {
    _cubit = BlocProvider.of(context);
    super.initState();
  }

  @override
  Future<void> reassemble() async {
    if (Platform.isAndroid) {
      await controller!.pauseCamera();
    }
    controller!.resumeCamera();
    super.reassemble();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          QRView(
            key: keyQr,
            onQRViewCreated: onQRViewCreated,
            overlay: QrScannerOverlayShape(
                borderLength: 236.w,
                borderColor: Colors.white,
                borderWidth: 12.w,
                borderRadius: 40.w,
                cutOutSize: 472.w,
                cutOutBottomOffset: 80.h),
          ),
          Positioned(
              top: 108.h,
              left: 32.w,
              child: GestureDetector(
                  onTap: () => Get.back(),
                  child: const Icon(Icons.arrow_back, color: Colors.white))),
          Positioned(
              bottom: 500.h,
              left: 130.w,
              right: 130.w,
              child: BlocBuilder<_Cubit, _State>(
                  bloc: _cubit,
                  builder: (context, state) {
                    return Row(children: [
                      Expanded(
                          child: LinearProgressIndicator(
                              minHeight: 12.w,
                              value: state.progress,
                              color: const Color(0xFFF4B400),
                              borderRadius:
                                  BorderRadius.all(Radius.circular(6.w)),
                              backgroundColor: Colors.grey)),
                      SizedBox(width: 24.w),
                      Text("${state.process}%",
                          textAlign: TextAlign.center,
                          style:
                              TextStyle(color: Colors.white, fontSize: 28.sp))
                    ]);
                  })),
          Positioned(
              bottom: 0,
              left: 0,
              right: 0,
              child: Column(children: [
                Image.asset('assets/images/img_scan_progress.png',
                    width: 160.w, height: 160.w),
                SizedBox(height: 16.w),
                Text('Scan the QR codes on your Goldshell wallet',
                    style: TextStyle(color: Colors.white, fontSize: 28.sp)),
                SizedBox(height: 200.w)
              ])),
        ],
      ),
    );
  }

  Future<void> onQRViewCreated(QRViewController controller) async {
    setState(() => this.controller = controller);
    reassemble();
    try {
      controller.scannedDataStream.listen((event) {
        _cubit.receiveQRCode(event.code);
      });
    } catch (e) {
      _cubit.onFailed("Error when receiving UR: $e");
      _cubit.reset();
    }
  }

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}


Libraries

gs_ur_dart