Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SW-737, SW-765] AirGap Integration #1829

Closed
wants to merge 6 commits into from
Closed
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
931 changes: 918 additions & 13 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@
"@aeternity/ga-multisig-contract": "github:aeternity/ga-multisig-contract#b09c381c7845a92ea5471d1721b091cca943bfee",
"@aeternity/hd-wallet": "^0.2.0",
"@aeternity/ledger-app-api": "0.2.1",
"@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",
"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
12 changes: 10 additions & 2 deletions src/composables/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { computed } from '@vue/composition-api';
import type { IAccount, IDefaultComposableOptions, IFormSelectOption } from '../types';
import { FAUCET_URL, buildSimplexLink, getAccountNameToDisplay } from '../popup/utils';
import {
FAUCET_URL,
ACCOUNT_AIR_GAP_WALLET,
buildSimplexLink,
getAccountNameToDisplay,
} from '../popup/utils';

export function useAccounts({ store }: IDefaultComposableOptions) {
// TODO in the future the state of the accounts should be stored in this composable
Expand All @@ -12,6 +17,8 @@ export function useAccounts({ store }: IDefaultComposableOptions) {
() => activeAccount.value && Object.keys(activeAccount.value).length > 0,
);

const isAirGap = computed((): boolean => activeAccount.value.type === ACCOUNT_AIR_GAP_WALLET);

/**
* Accounts data formatted as the form select options
*/
Expand All @@ -37,7 +44,7 @@ export function useAccounts({ store }: IDefaultComposableOptions) {

function setActiveAccountByIdx(idx: number = 0) {
// TODO replace with updating local state after removing the Vuex
store.commit('accounts/setActiveIdx', +(accounts.value[idx].idx || 0));
store.commit('accounts/setActiveIdx', idx);
}

function setActiveAccountByAddress(address?: string) {
Expand Down Expand Up @@ -67,5 +74,6 @@ export function useAccounts({ store }: IDefaultComposableOptions) {
getAccountByAddress,
setActiveAccountByAddress,
setActiveAccountByIdx,
isAirGap,
};
}
141 changes: 141 additions & 0 deletions src/composables/airGap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { decode } from '@aeternity/aepp-sdk/es/tx/builder/helpers';
import { UR, UREncoder } from '@ngraveio/bc-ur';
import bs58check from 'bs58check';
import { IACMessageType, SerializerV3 } from '@airgap/serializer';
import type {
AccountShareResponse,
IACMessageDefinitionObjectV3,
TransactionSignResponse,
TransactionSignRequest,
} from '@airgap/serializer';
import { AeternityAddress } from '@airgap/aeternity';
import { MainProtocolSymbols } from '@airgap/coinlib-core';
import type { IAccount } from '../types';
import { ACCOUNT_AIR_GAP_WALLET, MOBILE_SCHEMA, handleUnknownError } from '../popup/utils';

export function useAirGap() {
/**
* Encodes an array of IACMessageDefinitionObjectV3 objects into a UR string.
* @param data - The array of IACMessageDefinitionObjectV3 objects to encode.
* @returns The UR string or null if an error occurs.
*/
async function encodeIACMessageDefinitionObjects(data: IACMessageDefinitionObjectV3[]) {
try {
const serializer = SerializerV3.getInstance();
const serializedData = await serializer.serialize(data);

const buffer = await bs58check.decode(serializedData);
const ur = UR.fromBuffer(buffer);

// Set the chunk sizes for single-chunk and multi-chunk encoding.
const SETTINGS_SERIALIZER_SINGLE_CHUNK_SIZE = 500;
const SETTINGS_SERIALIZER_MULTI_CHUNK_SIZE = 250;

// Create a UR encoder for single-chunk encoding.
const singleEncoder = new UREncoder(
ur,
SETTINGS_SERIALIZER_SINGLE_CHUNK_SIZE,
);
// If the UR requires multi-chunk encoding,
// create a new UR encoder with the appropriate chunk size.
if (singleEncoder.fragmentsLength !== 1) {
const multiEncoder = new UREncoder(
ur,
SETTINGS_SERIALIZER_MULTI_CHUNK_SIZE,
);
const fragments = [];

// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _index in [...Array(multiEncoder.fragmentsLength)]
) {
// eslint-disable-next-line no-await-in-loop
fragments.push(await multiEncoder.nextPart());
}
return fragments;
}

// Encode the UR and return the UR string in upper case.
return [(await singleEncoder.nextPart()).toUpperCase()];
} catch (error) {
handleUnknownError(error);
return [];
}
}

/**
* Extracts shared addresses groups from encoded UR Data.
* @param serializedData
* @returns IAccount[]
*/
async function extractAccountShareResponseData(
data: IACMessageDefinitionObjectV3[] = [],
): Promise<IAccount[]> {
return data
.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,
airGapPublicKey: (item.payload as AccountShareResponse).publicKey,
} as IAccount;
});
}

/**
* Extracts the signed transaction response data from a serialized string
* and returns the transaction object.
* @param serializedData - The serialized string to extract data from.
* @returns The transaction object or null if no transaction object is found.
*/
async function extractSignedTransactionResponseData(
data: IACMessageDefinitionObjectV3[] = [],
): Promise<string | null> {
const payload = data.find(
(item) => item.type === IACMessageType.TransactionSignResponse,
)?.payload as TransactionSignResponse;

return payload?.transaction || null;
}

async function generateTransactionURDataFragments(
publicKey: string,
transaction: string,
networkId: string,
): Promise<string[]> {
const id = Math.floor(Math.random() * 90000000 + 10000000);
const callbackURL = `${MOBILE_SCHEMA}?d=`;
const payload: TransactionSignRequest = {
callbackURL,
publicKey,
transaction: {
networkId,
transaction,
},
};
const messageDefinitionObject: IACMessageDefinitionObjectV3 = {
id,
payload,
protocol: MainProtocolSymbols.AE,
type: IACMessageType.TransactionSignRequest,
};

return encodeIACMessageDefinitionObjects([messageDefinitionObject]);
}

return {
encodeIACMessageDefinitionObjects,
extractAccountShareResponseData,
generateTransactionURDataFragments,
extractSignedTransactionResponseData,
};
}
1 change: 1 addition & 0 deletions src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export * from './transactionTokens';
export * from './transactionAndTokenFilter';
export * from './ui';
export * from './viewport';
export * from './airGap';
4 changes: 4 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,9 @@ export function useNotifications({
);

async function fetchAllNotifications(): Promise<INotification[]> {
if (activeAccount.value.type !== ACCOUNT_HD_WALLET) {
return [];
}
const fetchUrl = `${activeNetwork.value.backendUrl}/notification/user/${activeAccount.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.
2 changes: 1 addition & 1 deletion src/lib/accounts/superhero.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class AccountSuperhero extends AccountBase {
sign(data: string | Uint8Array, opt: any): Promise<Uint8Array> {
const { activeAccount } = useAccounts({ store: this.store });
return IS_EXTENSION_BACKGROUND
? sign(data, activeAccount.value.secretKey) as any
? sign(data, activeAccount.value.secretKey as any) as any
: this.store.dispatch('accounts/sign', data, opt);
}

Expand Down
20 changes: 20 additions & 0 deletions src/popup/components/AccountCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
:name="account.name"
:idx="account.idx"
avatar-borderless
:is-air-gap="isAirGapAccount"
/>
</template>

Expand All @@ -21,6 +22,10 @@
:current-account="account"
:selected="selected"
/>
<AirGapIcon
v-if="isAirGapAccount"
class="air-gap-icon"
/>
</template>
</AccountCardBase>
</template>
Expand All @@ -34,18 +39,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 @@ -55,11 +64,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>
53 changes: 53 additions & 0 deletions src/popup/components/AccountImportRow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<template>
<AccountInfoCard
:address="account.address"
:name="account.name"
:idx="account.idx"
:is-air-gap="isAirGapAccount"
/>
</template>

<script lang="ts">
import BigNumber from 'bignumber.js';
import {
computed,
defineComponent,
onMounted,
ref,
PropType,
} from '@vue/composition-api';

import type { IAccount } from '../../types';
import { ROUTE_ACCOUNT_DETAILS } from '../router/routeNames';
import { useSdk } from '../../composables';
import { ACCOUNT_AIR_GAP_WALLET, aettosToAe } from '../utils';
import AccountInfoCard from './AccountInfoCard.vue';

export default defineComponent({
components: {
AccountInfoCard,
},
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.balance(props.account.address);
balance.value = new BigNumber(aettosToAe(fetchedBalance));
});

return {
numericBalance,
isAirGapAccount,
ROUTE_ACCOUNT_DETAILS,
};
},
});
</script>
3 changes: 2 additions & 1 deletion src/popup/components/AccountInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
data-cy="account-name-number"
class="account-name"
>
{{ $t('pages.account.heading') }} {{ idx + 1 }}
{{ isAirGap ? $t('common.airGap') : $t('pages.account.heading') }} {{ idx + 1 }}
</div>
<div
v-if="address && address.length"
Expand Down Expand Up @@ -74,6 +74,7 @@ export default defineComponent({
idx: { type: Number, default: 0 },
canCopyAddress: Boolean,
isMultisig: Boolean,
isAirGap: Boolean,
avatarBorderless: Boolean,
isListName: Boolean,
},
Expand Down
Loading