Skip to content

Commit

Permalink
Show ClaimQR-code as scale-encoded hex-string (#190)
Browse files Browse the repository at this point in the history
* scale-enccode claim qr codes

* [JS] add unit-tests that show that basic encode/decode works

* [JS] fix decoding of scanned claim

* [scanClaimQrCode] fix handling of decoded `ClaimOfAttendance`

* [scanClaimQrCode] show a snackbar if the qr-code could not be decoded.

* remove/fix obsolete comments

* fix: revert gesell-dev address to 10.0.2.2
  • Loading branch information
clangenb authored Dec 3, 2021
1 parent 7ef74d0 commit e49202c
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 34 deletions.
6 changes: 4 additions & 2 deletions lib/js_service_encointer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import account from './service/account.js';
import encointer from './service/encointer.js';
import settings from './service/settings.js';
import chain from './service/chain.js';
import codec from './service/scale-codec.js';

// send message to JSChannel: PolkaWallet
function send (path, data) {
Expand All @@ -20,9 +21,10 @@ function send (path, data) {

window.send = send;

window.settings = settings;
window.account = account;
window.encointer = encointer;
window.chain = chain;
window.encointer = encointer;
window.codec = codec;
window.settings = settings;

console.log('Initialized Window');
45 changes: 45 additions & 0 deletions lib/js_service_encointer/src/service/scale-codec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { u8aToU8a } from '@polkadot/util';

/**
* Decode given data into type. The type must be registered in the api's type registry.
*
* Data can be either a hex-string or a Uint8Array. If the json-representation is passed, this function is a no-op.
*
* @param {string} type
* @param {unknown} data
*/
export async function decode (type, data) {
return api.createType(type, u8aToU8a(data));
}

/**
* Encode the given object of type to a Uint8Array. The type must be registered in the api's type registry.
*
* Most likely, the object will be a hex-string or json-representation of the object. If a Uint8Array is passed, this
* function is a no-op.
*
* @param {string} type
* @param {unknown} object
*/
export async function encode (type, object) {
return api.createType(type, object).toU8a();
}

/**
* Encode the given object of type to a hex-string. The type must be registered in the api's type registry.
*
* Most likely, the object will be a Uint8Array or json-representation of the object. If a hex-string is passed, this
* function is a no-op.
*
* @param {string} type
* @param {unknown} object
*/
export async function encodeToHex (type, object) {
return api.createType(type, object).toHex();
}

export default {
decode,
encode,
encodeToHex
};
90 changes: 90 additions & 0 deletions lib/js_service_encointer/test/service/scale-codec.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* @jest-environment jsdom
*/

import '../../src';
import {
_signClaimOfAttendance
} from '../../src/service/encointer';
import { localDockerNetwork } from '../testUtils/networks';
import { beforeAll, describe, it, jest } from '@jest/globals';
import { testSetup } from '../testUtils/testSetup';
import { Keyring } from '@polkadot/api';
import { decode, encode } from '../../src/service/scale-codec';

describe('scale-codec', () => {
const network = localDockerNetwork();
let keyring;

const testClaim = {
claimant_public: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
ceremony_index: 63,
community_identifier: '0x22c51e6a656b19dd1e34c6126a75b8af02b38eedbeec51865063f142c83d40d3',
meetup_index: 1,
location: {
lat: '0x00000000000000237bf1ac299e7e0000',
lon: '0x00000000000000128b26000000000000'
},
timestamp: 1638441840000,
number_of_participants_confirmed: 3,
signature: null
};

beforeAll(async () => {
jest.setTimeout(90000);

await testSetup(network);
keyring = new Keyring({ type: 'sr25519' });
});

describe('encode', () => {
it('works correctly', async () => {
const claimant = keyring.addFromUri('//Alice', { name: 'Alice default' });

const claimSigned = await _signClaimOfAttendance(claimant, testClaim);

expect(
await encode('ClaimOfAttendance', claimSigned)
).toStrictEqual(claimSigned.toU8a());
});
});

describe('decode', () => {
it('works correctly', async () => {
const claimant = keyring.addFromUri('//Alice', { name: 'Alice default' });

const claimSigned = await _signClaimOfAttendance(claimant, testClaim);

expect(
await decode('ClaimOfAttendance', claimSigned.toU8a())
).toStrictEqual(claimSigned);
});

it('can parse data from wallet when scanning the qr code', async () => {
// data which is encoded and displayed on cellphone 1.
const walletClaim = {
claimant_public: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
ceremony_index: 2,
community_identifier: '0x2cbd65a5f087b3d60aec997e6369ef694f125582f5f7cffd7bbddc56a71858fc',
meetup_index: 1,
location: {
lat: '0x00000000000000237bf1ac299e7e0000',
lon: '0x00000000000000128b26000000000000'
},
timestamp: 1638441840000,
number_of_participants_confirmed: 3,
signature: {
sr25519: '0xf03d063bfdf951438bf48ba74623c8c3689625c7ddcfc73e8600ed459b8d483e2d45d0b7e4dff69a05467251a45a48ac7e66f04803b1f8db5407a4f4fcf4968b'
}
};

// Bytes scanned by cellphone when scanning above claim
const walletData =
[212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, 2, 0, 0, 0, 44, 189, 101, 165, 240, 135, 179, 214, 10, 236, 153, 126, 99, 105, 239, 105, 79, 18, 85, 130, 245, 247, 207, 253, 123, 189, 220, 86, 167, 24, 88, 252, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 158, 41, 172, 241, 123, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38, 139, 18, 0, 0, 0, 0, 0, 0, 0, 128, 109, 190, 122, 125, 1, 0, 0, 3, 0, 0, 0, 1, 1, 240, 61, 6, 59, 253, 249, 81, 67, 139, 244, 139, 167, 70, 35, 200, 195, 104, 150, 37, 199, 221, 207, 199, 62, 134, 0, 237, 69, 155, 141, 72, 62, 45, 69, 208, 183, 228, 223, 246, 154, 5, 70, 114, 81, 164, 90, 72, 172, 126, 102, 240, 72, 3, 177, 248, 219, 84, 7, 164, 244, 252, 244, 150, 139];

expect(
await decode('ClaimOfAttendance', walletData)
).toStrictEqual(api.createType('ClaimOfAttendance', walletClaim));
});
});
});
4 changes: 3 additions & 1 deletion lib/mocks/api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:encointer_wallet/mocks/api/apiAssets.dart';
import 'package:encointer_wallet/store/app.dart';
import 'package:encointer_wallet/service/substrateApi/api.dart';
import 'package:encointer_wallet/service/substrateApi/apiAccount.dart';
import 'package:encointer_wallet/service/substrateApi/codecApi.dart';
import 'package:encointer_wallet/mocks/api/apiEncointer.dart';
import 'package:encointer_wallet/mocks/api/chain.dart';
import 'package:encointer_wallet/mocks/api/apiIpfs.dart';
Expand All @@ -18,9 +19,10 @@ class MockApi extends Api {
jsStorage = GetStorage();

account = ApiAccount(this);
encointer = MockApiEncointer(this);
assets = MockApiAssets(this);
chain = MockChainApi(this);
codec = CodecApi(this);
encointer = MockApiEncointer(this);
ipfs = MockIpfs();

if (withUi) {
Expand Down
26 changes: 13 additions & 13 deletions lib/page-encointer/meetup/claimQrCode.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'package:encointer_wallet/store/encointer/types/claimOfAttendance.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:typed_data';

import 'package:encointer_wallet/common/components/addressIcon.dart';
import 'package:encointer_wallet/store/app.dart';
import 'package:encointer_wallet/utils/i18n/index.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';

import 'scanClaimQrCode.dart';
Expand All @@ -19,7 +21,9 @@ class ClaimQrCode extends StatelessWidget {
final AppStore store;

final String title;
final Future<ClaimOfAttendance> claim;

/// future of a scale-encoded `ClaimOfAttendance`
final Future<Uint8List> claim;
final int confirmedParticipantsCount;

@override
Expand Down Expand Up @@ -66,23 +70,19 @@ class ClaimQrCode extends StatelessWidget {
),
Container(width: 8, height: 8),
Container(
width: 380,
height: 380,
width: 395,
height: 395,
decoration: BoxDecoration(
border: Border.all(width: 4, color: themeColor),
borderRadius: BorderRadius.all(const Radius.circular(8)),
),
child: FutureBuilder<ClaimOfAttendance>(
child: FutureBuilder<Uint8List>(
future: claim,
builder: (_, AsyncSnapshot<ClaimOfAttendance> snapshot) {
builder: (_, AsyncSnapshot<Uint8List> snapshot) {
if (snapshot.hasData) {
return QrImage(
data: snapshot.data.toString(),
data: base64.encode(snapshot.data),
errorCorrectionLevel: QrErrorCorrectLevel.L,
//embeddedImage:
// AssetImage('assets/images/public/app.png'),
//embeddedImageStyle:
// QrEmbeddedImageStyle(size: Size(40, 40)),
);
} else {
return CupertinoActivityIndicator();
Expand Down
46 changes: 33 additions & 13 deletions lib/page-encointer/meetup/scanClaimQrCode.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import 'dart:convert';

import 'package:encointer_wallet/service/substrateApi/api.dart';
import 'package:encointer_wallet/service/substrateApi/codecApi.dart';
import 'package:encointer_wallet/store/app.dart';
import 'package:encointer_wallet/store/encointer/types/claimOfAttendance.dart';
import 'package:encointer_wallet/utils/i18n/index.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_qr_scan/qrcode_reader_view.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_mobx/flutter_mobx.dart';

// TODO: scan image failed
class ScanClaimQrCode extends StatelessWidget {
ScanClaimQrCode(this.store, this.confirmedParticipantsCount);

Expand All @@ -28,25 +29,44 @@ class ScanClaimQrCode extends StatelessWidget {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
backgroundColor: Colors.white,
content: Text(msg, style: TextStyle(color: Colors.black54)),
duration: Duration(milliseconds: 1000),
duration: Duration(milliseconds: 1500),
));
}

void validateAndStoreClaim(BuildContext context, ClaimOfAttendance claim, Map dic) {
if (!store.encointer.meetupRegistry.contains(claim.claimantPublic)) {
// this is important because the runtime checks if there are too many claims trying to be registered.
_showSnackBar(context, dic['meetup.claimant.invalid']);
} else {
String msg = store.encointer.containsClaim(claim) ? dic['claims.scanned.already'] : dic['claims.scanned.new'];
store.encointer.addParticipantClaim(claim);
_showSnackBar(context, msg);
}
}

@override
Widget build(BuildContext context) {
final Map dic = I18n.of(context).encointer;

Future _onScan(String data, String _rawData) async {
if (data != null) {
var claim = ClaimOfAttendance.fromJson(json.decode(data));
Future _onScan(String base64Data, String _rawData) async {
if (base64Data != null) {
var data = base64.decode(base64Data);

// Todo: Not good to use the global webApi here, but I wanted to prevent big changes into the code for now.
// Fix this when #132 is tackled.
var claim = await webApi.codec
.decodeBytes(ClaimOfAttendanceJSRegistryName, data)
.then((c) => ClaimOfAttendance.fromJson(c))
.timeout(
const Duration(seconds: 3),
onTimeout: () {
_showSnackBar(context, dic['claims.scanned.decode.failed']);
return null;
},
);

if (!store.encointer.meetupRegistry.contains(claim.claimantPublic)) {
// this is important because the runtime checks if there are too many claims trying to be registered.
_showSnackBar(context, dic['meetup.claimant.invalid']);
} else {
String msg = store.encointer.containsClaim(claim) ? dic['claims.scanned.already'] : dic['claims.scanned.new'];
store.encointer.addParticipantClaim(claim);
_showSnackBar(context, msg);
if (claim != null) {
validateAndStoreClaim(context, claim, dic);
}

// If we don't wait, scans of the same qr code are spammed.
Expand Down
7 changes: 5 additions & 2 deletions lib/page-encointer/meetup/startMeetup.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'package:encointer_wallet/service/substrateApi/api.dart';
import 'package:encointer_wallet/service/substrateApi/codecApi.dart';
import 'package:encointer_wallet/store/app.dart';
import 'package:encointer_wallet/utils/i18n/index.dart';
import 'package:flutter/material.dart';

import 'confirmAttendeesDialog.dart';
import 'claimQrCode.dart';
import 'confirmAttendeesDialog.dart';

Future<void> startMeetup(BuildContext context, AppStore store) async {
var amount = await Navigator.of(context).push(MaterialPageRoute(builder: (context) => ConfirmAttendeesDialog()));
Expand All @@ -13,7 +14,9 @@ Future<void> startMeetup(BuildContext context, AppStore store) async {
builder: (BuildContext context) => ClaimQrCode(
store,
title: I18n.of(context).encointer['claim.qr'],
claim: webApi.encointer.signClaimOfAttendance(amount, store.account.cachedPin),
claim: webApi.encointer
.signClaimOfAttendance(amount, store.account.cachedPin)
.then((claim) => webApi.codec.encodeToBytes(ClaimOfAttendanceJSRegistryName, claim)),
confirmedParticipantsCount: amount,
),
),
Expand Down
8 changes: 5 additions & 3 deletions lib/service/substrateApi/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:encointer_wallet/service/ipfsApi/httpApi.dart';
import 'package:encointer_wallet/service/subscan.dart';
import 'package:encointer_wallet/service/substrateApi/apiAccount.dart';
import 'package:encointer_wallet/service/substrateApi/apiAssets.dart';
import 'package:encointer_wallet/service/substrateApi/codecApi.dart';
import 'package:encointer_wallet/service/substrateApi/encointer/apiEncointer.dart';
import 'package:encointer_wallet/service/substrateApi/chainApi.dart';
import 'package:encointer_wallet/service/substrateApi/types/genExternalLinksParams.dart';
Expand All @@ -26,10 +27,10 @@ class Api {
var jsStorage;

ApiAccount account;
ApiEncointer encointer;
ApiAssets assets;
ChainApi chain;

CodecApi codec;
ApiEncointer encointer;
Ipfs ipfs;

SubScanApi subScanApi = SubScanApi();
Expand All @@ -47,9 +48,10 @@ class Api {
jsStorage = GetStorage();

account = ApiAccount(this);
encointer = ApiEncointer(this);
assets = ApiAssets(this);
chain = ChainApi(this);
codec = CodecApi(this);
encointer = ApiEncointer(this);
ipfs = Ipfs(gateway: store.settings.ipfsGateway);

print("first launch of webview");
Expand Down
Loading

0 comments on commit e49202c

Please sign in to comment.