Skip to content

Commit

Permalink
feat: the ability to connect qr based hardware wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
ifaouibadi committed Mar 22, 2023
1 parent 49da939 commit ce7c67e
Show file tree
Hide file tree
Showing 20 changed files with 1,443 additions and 21 deletions.
916 changes: 902 additions & 14 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,22 @@
"dependencies": {
"@aeternity/aepp-sdk": "^11.0.1",
"@aeternity/bip39": "^0.1.0",
"@aeternity/ga-multisig-contract": "github:aeternity/ga-multisig-contract#b09c381c7845a92ea5471d1721b091cca943bfee",
"@aeternity/hd-wallet": "^0.2.0",
"@aeternity/ledger-app-api": "0.2.1",
"@aeternity/ga-multisig-contract": "github:aeternity/ga-multisig-contract#b09c381c7845a92ea5471d1721b091cca943bfee",
"@airgap/aeternity": "^0.13.10",
"@airgap/coinlib-core": "^0.13.10",
"@airgap/serializer": "^0.13.10",
"@fontsource/ibm-plex-mono": "^4.5.7",
"@fontsource/ibm-plex-sans": "^4.5.7",
"@keystonehq/bc-ur-registry": "^0.5.4",
"@ledgerhq/hw-transport-webusb": "^6.27.1",
"@ngraveio/bc-ur": "^1.1.6",
"@vue/composition-api": "^1.0.3",
"@zxing/library": "^0.19.1",
"aeternity-fungible-token": "github:aeternity/aeternity-fungible-token#v2.0.0",
"bignumber.js": "^9.0.2",
"bs58check": "^2.1.2",
"camelcase-keys-deep": "^0.1.0",
"cordova-android": "^10.1.1",
"cordova-clipboard": "^1.3.0",
Expand Down
101 changes: 101 additions & 0 deletions src/composables/airGap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { decode } from '@aeternity/aepp-sdk/es/tx/builder/helpers';
import { UR, URDecoder, UREncoder } from '@ngraveio/bc-ur';
import bs58check from 'bs58check';
import { IACMessageType, SerializerV3 } from '@airgap/serializer';
import type {
AccountShareResponse,
IACMessageDefinitionObjectV3,
} from '@airgap/serializer';
import { AeternityAddress } from '@airgap/aeternity';
import type { IAccount, IDefaultComposableOptions } from '../types';
import { ACCOUNT_AIR_GAP_WALLET } from '../popup/utils';

// eslint-disable-next-line no-unused-vars
export function useAirGap({ store }: IDefaultComposableOptions) {
/**
* Decodes a serialized data string that conforms to the "Uniform Resource" format (UR).
* The function first decodes the UR string using the URDecoder class, then decodes the
* resulting CBOR data, and finally deserializes the decoded data using a SerializerV3 instance.
*
* @param serializedData A string containing the serialized UR data to decode.
*
* @returns The deserialized data,
* or null if the input data is not in the expected format or decoding fails.
*/
async function decodeURSerializedData(
serializedData: string,
): Promise<IACMessageDefinitionObjectV3[]> {
if (!serializedData.toUpperCase().startsWith('UR:')) {
return [];
}

const decoder = new URDecoder();
decoder.receivePart(serializedData);

if (!decoder.isComplete() || !decoder.isSuccess()) {
return [];
}

const decoded = decoder.resultUR();
const combinedData = decoded.decodeCBOR();
const resultUr = bs58check.encode(combinedData);

const serializer = SerializerV3.getInstance();
const parsedData: IACMessageDefinitionObjectV3[] = await serializer.deserialize(resultUr);

return parsedData;
}

/**
* Encodes an array of IAC message definition objects into a Uniform Resource (UR) format.
*/
async function encodeIACMessageDefinitionObjects(data: IACMessageDefinitionObjectV3[]) {
const serializer = SerializerV3.getInstance();
const serializedData = await serializer.serialize(data);

const buffer = bs58check.decode(serializedData);
const ur = UR.fromBuffer(buffer);
const encoder = new UREncoder(ur);

return encoder;
}

/**
* Extracts shared addresses groups from encoded UR Data.
* @param serializedData
* @returns IAccount[]
*/
async function extractAccountShareResponseData(
serializedData: string,
): Promise<IAccount[]> {
const decodedData = await decodeURSerializedData(serializedData);

if (!decodedData) {
return [];
}

return decodedData
.filter((item) => item.type === IACMessageType.AccountShareResponse)
.map((item) => {
const address = AeternityAddress.from(
(item.payload as AccountShareResponse).publicKey,
).asString();
const publicKey = Buffer.from(decode(address, 'ak'));

return {
address,
publicKey,
name: '',
showed: true,
type: ACCOUNT_AIR_GAP_WALLET,
isOffline: true,
} as IAccount;
});
}

return {
decodeURSerializedData,
encodeIACMessageDefinitionObjects,
extractAccountShareResponseData,
};
}
1 change: 1 addition & 0 deletions src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './transactionTokens';
export * from './transactionAndTokenFilter';
export * from './ui';
export * from './viewport';
export * from './airGap';
5 changes: 5 additions & 0 deletions src/composables/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
fetchJson,
postJson,
fetchRespondChallenge,
ACCOUNT_HD_WALLET,
} from '../popup/utils';
import { useSdk } from './sdk';
import { useAccounts } from './accounts';
Expand Down Expand Up @@ -75,6 +76,10 @@ export function useNotifications({
);

async function fetchAllNotifications(): Promise<INotification[]> {
if (account.value.type !== ACCOUNT_HD_WALLET) {
return [];
}

const fetchUrl = `${activeNetwork.value.backendUrl}/notification/user/${account.value.address}`;
const [responseChallenge, sdk] = await Promise.all([
fetchJson(fetchUrl),
Expand Down
4 changes: 4 additions & 0 deletions src/icons/air-gap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/popup/components/AccountCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:address="account.address"
:name="account.name"
:idx="account.idx"
:is-air-gap="isAirGapAccount"
/>
</template>

Expand All @@ -20,6 +21,10 @@
:current-account="account"
:selected="selected"
/>
<AirGapIcon
v-if="isAirGapAccount"
class="air-gap-icon"
/>
</template>
</AccountCardBase>
</template>
Expand All @@ -33,18 +38,22 @@ import {
import type { IAccount } from '../../types';
import { ROUTE_ACCOUNT_DETAILS } from '../router/routeNames';
import { useBalances } from '../../composables';
import { ACCOUNT_AIR_GAP_WALLET } from '../utils';
import AccountInfo from './AccountInfo.vue';
import BalanceInfo from './BalanceInfo.vue';
import AccountCardTotalTokens from './AccountCardTotalTokens.vue';
import AccountCardBase from './AccountCardBase.vue';
import AirGapIcon from '../../icons/air-gap.svg?vue-component';
export default defineComponent({
components: {
AccountCardBase,
AccountCardTotalTokens,
AccountInfo,
BalanceInfo,
AirGapIcon,
},
props: {
account: { type: Object as PropType<IAccount>, required: true },
Expand All @@ -54,11 +63,22 @@ export default defineComponent({
const { balance } = useBalances({ store: root.$store });
const numericBalance = computed<number>(() => balance.value.toNumber());
const isAirGapAccount = computed((): boolean => props.account.type === ACCOUNT_AIR_GAP_WALLET);
return {
numericBalance,
isAirGapAccount,
ROUTE_ACCOUNT_DETAILS,
};
},
});
</script>

<style lang="scss" scoped>
.account-card-base {
.air-gap-icon {
width: 24px;
height: 24px;
}
}
</style>
75 changes: 75 additions & 0 deletions src/popup/components/AccountImportRow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<template>
<div class="account-import-row">
<AccountInfo
:address="account.address"
:name="account.name"
:idx="account.idx"
:is-air-gap="isAirGapAccount"
/>

<TokenAmount :amount="+numericBalance" />
</div>
</template>

<script lang="ts">
import BigNumber from 'bignumber.js';
import {
computed,
defineComponent,
onMounted,
ref,
PropType,
} from '@vue/composition-api';
import AccountInfo from './AccountInfo.vue';
import TokenAmount from './TokenAmount.vue';
import type { IAccount } from '../../types';
import { ROUTE_ACCOUNT_DETAILS } from '../router/routeNames';
import { useSdk } from '../../composables';
import { ACCOUNT_AIR_GAP_WALLET, aettosToAe } from '../utils';
export default defineComponent({
components: {
AccountInfo,
TokenAmount,
},
props: {
account: { type: Object as PropType<IAccount>, required: true },
},
setup(props, { root }) {
const { getSdk } = useSdk({ store: root.$store });
const balance = ref(new BigNumber(0));
const numericBalance = computed<number>(() => balance.value.toNumber());
const isAirGapAccount = computed((): boolean => props.account.type === ACCOUNT_AIR_GAP_WALLET);
onMounted(async () => {
const sdk = await getSdk();
const fetchedBalance = await (sdk as any).balance(props.account.address);
balance.value = new BigNumber(aettosToAe(fetchedBalance));
});
return {
numericBalance,
isAirGapAccount,
ROUTE_ACCOUNT_DETAILS,
};
},
});
</script>

<style lang="scss" scoped>
@use '../../styles/mixins';
.account-import-row {
@include mixins.flex(space-between, center, row);
::v-deep .token-amount {
.amount,
.fiat {
display: block;
text-align: right;
}
}
}
</style>
3 changes: 2 additions & 1 deletion src/popup/components/AccountInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
data-cy="account-name-number"
class="account-name"
>
{{ $t('pages.account.heading') }} {{ idx + 1 }}
{{ isAirGap ? $t('airGap') : $t('pages.account.heading') }} {{ idx + 1 }}
</div>
<div
v-if="address && address.length"
Expand Down Expand Up @@ -67,6 +67,7 @@ export default defineComponent({
idx: { type: Number, default: 0 },
canCopyAddress: Boolean,
isMultisig: Boolean,
isAirGap: Boolean,
},
setup(props) {
const activeNetwork = useGetter('activeNetwork');
Expand Down
2 changes: 1 addition & 1 deletion src/popup/components/DashboardHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default defineComponent({
const addressList = computed(() => accounts.value.map((acc) => acc.address));
function selectAccount(index: number) {
root.$store.commit('accounts/setActiveIdx', +(accounts.value[index].idx || 0));
root.$store.commit('accounts/setActiveIdx', index);
}
return {
Expand Down
Loading

0 comments on commit ce7c67e

Please sign in to comment.