Skip to content

Commit 7d4a977

Browse files
subhod-iperonczyk
authored andcommitted
feat: create bitcoin account
1 parent a9f0325 commit 7d4a977

File tree

19 files changed

+472
-146
lines changed

19 files changed

+472
-146
lines changed

package-lock.json

+266-55
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@aeternity/bip39": "^0.1.0",
3232
"@aeternity/hd-wallet": "^0.2.0",
3333
"@aeternity/json-bigint": "^0.3.1",
34+
"@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3",
3435
"@fontsource/ibm-plex-mono": "^4.5.7",
3536
"@fontsource/ibm-plex-sans": "^4.5.7",
3637
"@intlify/eslint-plugin-vue-i18n": "^2.0.0",
@@ -39,6 +40,8 @@
3940
"@vee-validate/rules": "^4.8.6",
4041
"@zxing/library": "^0.19.1",
4142
"bignumber.js": "^9.0.2",
43+
"bip32": "^4.0.0",
44+
"bitcoinjs-lib": "^6.1.3",
4245
"camelcase-keys-deep": "^0.1.0",
4346
"cordova-android": "^10.1.1",
4447
"cordova-clipboard": "^1.3.0",

src/composables/accounts.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Encoded } from '@aeternity/aepp-sdk';
33
import type {
44
IAccount,
55
IAccountOverview,
6-
IAeternityAccountRaw,
6+
IAccountRaw,
77
IDefaultComposableOptions,
88
IFormSelectOption,
99
INetwork,
@@ -13,12 +13,16 @@ import { getAccountNameToDisplay } from '@/utils';
1313
import { AE_FAUCET_URL } from '@/protocols/aeternity/config';
1414
import { buildSimplexLink } from '@/protocols/aeternity/helpers';
1515
import { AeScan } from '@/protocols/aeternity/libs/AeScan';
16+
import { PROTOCOL_AETERNITY } from '@/constants';
1617

1718
export function useAccounts({ store }: IDefaultComposableOptions) {
1819
// TODO in the future the state of the accounts should be stored in this composable
1920
const activeIdx = computed((): number => store.state.accounts?.activeIdx || 0);
20-
const accountsRaw = computed((): IAeternityAccountRaw[] => store.state.accounts?.list || []);
21+
const accountsRaw = computed((): IAccountRaw[] => store.state.accounts?.list || []);
2122
const accounts = computed((): IAccount[] => store.getters.accounts || []);
23+
const aeAccounts = computed(
24+
() => accounts.value.filter(({ protocol }) => protocol === PROTOCOL_AETERNITY),
25+
);
2226
const accountsAddressList = computed(() => accounts.value.map((acc) => acc.address));
2327
const activeAccount = computed((): IAccount => accounts.value[activeIdx.value] || {});
2428
const isLoggedIn = computed(
@@ -76,6 +80,7 @@ export function useAccounts({ store }: IDefaultComposableOptions) {
7680

7781
return {
7882
accounts,
83+
aeAccounts,
7984
accountsAddressList,
8085
accountsSelectOptions,
8186
accountsRaw,

src/composables/balances.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function useBalances({ store }: IDefaultComposableOptions) {
7070
async ({
7171
address,
7272
protocol,
73-
}) => (ProtocolAdapterFactory.getAdapter(protocol)).getBalance(address).catch((error) => {
73+
}) => ProtocolAdapterFactory.getAdapter(protocol).getBalance(address).catch((error) => {
7474
if (!isNotFoundError(error)) {
7575
handleUnknownError(error);
7676
}

src/composables/latestTransactionList.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const { onNetworkChange } = createNetworkWatcher();
2525
* that wants to use this data.
2626
*/
2727
export function useLatestTransactionList({ store }: IDefaultComposableOptions) {
28-
const { accounts } = useAccounts({ store });
28+
const { aeAccounts } = useAccounts({ store });
2929
const { balancesTotal } = useBalances({ store });
3030
const { nodeNetworkId } = useAeSdk({ store });
3131

@@ -73,7 +73,7 @@ export function useLatestTransactionList({ store }: IDefaultComposableOptions) {
7373

7474
isTransactionListLoading.value = true;
7575

76-
await Promise.all(accounts.value.map(async ({ address }) => {
76+
await Promise.all(aeAccounts.value.map(async ({ address }) => {
7777
try {
7878
return (await fetchTransactions(
7979
DASHBOARD_TRANSACTION_LIMIT,

src/composables/multisigAccounts.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const initPollingWatcher = createPollingBasedOnMountedComponents(POLLING_INTERVA
6565

6666
export function useMultisigAccounts({ store, pollOnce = false }: MultisigAccountsOptions) {
6767
const { nodeNetworkId, getAeSdk } = useAeSdk({ store });
68-
const { accounts } = useAccounts({ store });
68+
const { aeAccounts } = useAccounts({ store });
6969

7070
const activeNetwork = computed<INetwork>(() => store.getters.activeNetwork);
7171
const allMultisigAccounts = computed<IMultisigAccount[]>(() => [
@@ -168,7 +168,7 @@ export function useMultisigAccounts({ store, pollOnce = false }: MultisigAccount
168168
*/
169169
let rawMultisigData: IMultisigAccountResponse[] = [];
170170
try {
171-
await Promise.all(accounts.value.map(async ({ address }) => rawMultisigData.push(
171+
await Promise.all(aeAccounts.value.map(async ({ address }) => rawMultisigData.push(
172172
...(await fetchJson(`${activeNetwork.value.multisigBackendUrl}/${address}`)),
173173
)));
174174
} catch {
@@ -183,7 +183,7 @@ export function useMultisigAccounts({ store, pollOnce = false }: MultisigAccount
183183
return (
184184
account.hasPendingTransaction
185185
&& account.signers.some((signer) => (
186-
accounts.value.map(({ address }) => address).includes(signer)
186+
aeAccounts.value.map(({ address }) => address).includes(signer)
187187
&& !account.confirmedBy.includes(signer)
188188
))
189189
);

src/composables/notifications.ts

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
NOTIFICATION_TYPE_WALLET,
1414
NOTIFICATION_ENTITY_TYPE_TIP,
1515
AGGREGATOR_URL,
16+
PROTOCOL_AETERNITY,
1617
} from '@/constants';
1718
import { useAccounts } from './accounts';
1819
import { createPollingBasedOnMountedComponents } from './composablesHelpers';
@@ -73,6 +74,9 @@ export function useNotifications({
7374
);
7475

7576
async function fetchAllNotifications(): Promise<INotification[]> {
77+
// TODO: Remove this condition once global filter is ready
78+
if (activeAccount.value.protocol !== PROTOCOL_AETERNITY) return [];
79+
7680
const fetchUrl = `${activeNetwork.value.backendUrl}/notification/user/${activeAccount.value.address}`;
7781
const responseChallenge = await fetchJson(fetchUrl);
7882
const respondChallenge = await fetchRespondChallenge(responseChallenge);

src/constants/common.ts

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const DISTINCT_PROTOCOL_VIEWS = [
4040
] as const;
4141

4242
export const SEED_LENGTH = 12;
43+
export const MAXIMUM_ACCOUNTS_TO_DISCOVER = 5;
4344

4445
export const DECIMAL_PLACES_HIGH_PRECISION = 9;
4546
export const DECIMAL_PLACES_LOW_PRECISION = 2;

src/popup/components/Modals/AccountCreate.vue

+23-5
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,19 @@
2424

2525
<BtnSubheader
2626
v-if="!isMultisig"
27-
:header="$t('pages.accounts.addAccount')"
27+
:header="$t('pages.accounts.addAeternityAccount')"
2828
:subheader="$t('modals.createAccount.btnSubtitle')"
2929
:icon="PlusCircleIcon"
3030
:disabled="!isOnline"
31-
@click="createPlainAccount()"
31+
@click="createPlainAccount(PROTOCOL_AETERNITY)"
32+
/>
33+
<BtnSubheader
34+
v-if="!isMultisig"
35+
:header="$t('pages.accounts.addBitcoinAccount')"
36+
:subheader="$t('modals.createAccount.btnSubtitle')"
37+
:icon="PlusCircleIcon"
38+
:disabled="!isOnline"
39+
@click="createPlainAccount(PROTOCOL_BITCOIN)"
3240
/>
3341
<BtnSubheader
3442
:header="$t('modals.createMultisigAccount.btnText')"
@@ -45,7 +53,12 @@
4553
<script lang="ts">
4654
import { defineComponent, PropType, ref } from 'vue';
4755
import { useStore } from 'vuex';
48-
import { MODAL_MULTISIG_VAULT_CREATE } from '@/constants';
56+
import type { Protocol } from '@/types';
57+
import {
58+
MODAL_MULTISIG_VAULT_CREATE,
59+
PROTOCOL_AETERNITY,
60+
PROTOCOL_BITCOIN,
61+
} from '@/constants';
4962
import { useConnection, useModals } from '@/composables';
5063
5164
import BtnSubheader from '../buttons/BtnSubheader.vue';
@@ -70,9 +83,12 @@ export default defineComponent({
7083
7184
const loading = ref(false);
7285
73-
async function createPlainAccount() {
86+
async function createPlainAccount(protocol: Protocol) {
7487
loading.value = true;
75-
await store.dispatch('accounts/hdWallet/create');
88+
await store.dispatch('accounts/hdWallet/create', {
89+
isRestored: false,
90+
protocol,
91+
});
7692
loading.value = false;
7793
props.resolve();
7894
}
@@ -83,6 +99,8 @@ export default defineComponent({
8399
}
84100
85101
return {
102+
PROTOCOL_AETERNITY,
103+
PROTOCOL_BITCOIN,
86104
PlusCircleIcon,
87105
isOnline,
88106
loading,

src/popup/locales/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,8 @@
491491
},
492492
"accounts": {
493493
"addAccount": "Add account",
494+
"addAeternityAccount": "Add Aeternity account",
495+
"addBitcoinAccount": "Add Bitcoin account",
494496
"searchAccountsPlaceholder": "Search for account",
495497
"addAccountDescription": "Add an account to your wallet to manage funds separately."
496498
},

src/protocols/BaseProtocolAdapter.ts

+25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
1+
import type { IHdWalletAccount } from '@/types';
2+
13
/**
24
* Represents common attributes and behavior of a protocol
35
*/
46
export abstract class BaseProtocolAdapter {
57
abstract getBalance(address: string): Promise<string>;
8+
9+
/**
10+
* Check whether the network has encountered this account.
11+
* @param address Account address
12+
*/
13+
abstract isAccountUsed(address: string): Promise<boolean>;
14+
15+
/**
16+
* Generate account from Mnemonic
17+
* @param seed 12 word seed array buffer
18+
* @param accountIndex Account Index in derivation path
19+
*/
20+
abstract getHdWalletAccountFromMnemonicSeed(
21+
seed: Uint8Array,
22+
accountIndex: number
23+
): IHdWalletAccount;
24+
25+
/**
26+
* Discover accounts that have been used in the past
27+
* @param seed 12 word seed array buffer
28+
* @returns Array of used accounts
29+
*/
30+
abstract discoverAccounts(seed: Uint8Array): Promise<number>;
631
}

src/protocols/aeternity/helpers/index.ts

-20
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
InvalidTxError,
66
Tag,
77
decode,
8-
derivePathFromKey,
9-
encode,
108
formatAmount,
11-
getKeyPair,
129
isAddressValid,
1310
unpackTx,
1411
} from '@aeternity/aepp-sdk';
@@ -20,11 +17,9 @@ import type {
2017
ICommonTransaction,
2118
IDexContracts,
2219
IGAAttachTx,
23-
IKeyPair,
2420
INameEntryFetched,
2521
ITransaction,
2622
ITx,
27-
IWallet,
2823
TxFunction,
2924
TxFunctionRaw,
3025
TxType,
@@ -137,21 +132,6 @@ export function getAeFee(value: number | string) {
137132
return +aettosToAe(new BigNumber(value || 0).toNumber());
138133
}
139134

140-
export function getHdWalletAccount(
141-
wallet: IWallet,
142-
accountIdx = 0,
143-
): IKeyPair & { address: Encoded.AccountAddress } {
144-
const keyTreeNode = derivePathFromKey(
145-
`${accountIdx}h/0h/0h`,
146-
{ ...wallet, secretKey: wallet.privateKey },
147-
);
148-
const keyPair = getKeyPair(keyTreeNode.secretKey);
149-
return {
150-
...keyPair,
151-
address: encode(keyPair.publicKey, Encoding.AccountAddress),
152-
};
153-
}
154-
155135
export function getTxOwnerAddress(innerTx?: ITx) {
156136
return innerTx?.accountId || innerTx?.callerId;
157137
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,51 @@
11
/* eslint-disable class-methods-use-this */
22

3-
import { Encoded } from '@aeternity/aepp-sdk';
3+
import {
4+
Encoded,
5+
getHdWalletAccountFromSeed,
6+
} from '@aeternity/aepp-sdk';
47
import { useStore } from 'vuex';
8+
import type { IHdWalletAccount } from '@/types';
59
import { useAeSdk } from '@/composables/aeSdk';
610
import { BaseProtocolAdapter } from '@/protocols/BaseProtocolAdapter';
11+
import { MAXIMUM_ACCOUNTS_TO_DISCOVER } from '@/constants';
712

813
export class AeternityAdapter extends BaseProtocolAdapter {
9-
async getBalance(address: Encoded.AccountAddress): Promise<string> {
14+
override async getBalance(address: Encoded.AccountAddress): Promise<string> {
1015
const store = useStore();
1116
const { getAeSdk } = useAeSdk({ store });
1217
const sdk = await getAeSdk();
1318
return sdk.getBalance(address);
1419
}
20+
21+
override async isAccountUsed(address: string): Promise<boolean> {
22+
const store = useStore();
23+
const { getAeSdk } = useAeSdk({ store });
24+
const aeSdk = await getAeSdk();
25+
return aeSdk.api.getAccountByPubkey(address).then(() => true, () => false);
26+
}
27+
28+
override getHdWalletAccountFromMnemonicSeed(
29+
seed: Uint8Array,
30+
accountIndex: number,
31+
): IHdWalletAccount {
32+
const account = getHdWalletAccountFromSeed(seed, accountIndex);
33+
return {
34+
...account,
35+
address: account.publicKey,
36+
};
37+
}
38+
39+
override async discoverAccounts(seed: Uint8Array): Promise<number> {
40+
let lastNotEmptyIdx = 0;
41+
// First Aeternity account is present in the state, hence index starts from 1
42+
for (let i = 1; i < MAXIMUM_ACCOUNTS_TO_DISCOVER; i += 1) {
43+
const account = this.getHdWalletAccountFromMnemonicSeed(seed, i);
44+
// eslint-disable-next-line no-await-in-loop
45+
if (await this.isAccountUsed(account.publicKey)) {
46+
lastNotEmptyIdx = i - 1;
47+
}
48+
}
49+
return lastNotEmptyIdx;
50+
}
1551
}
+50-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,58 @@
11
/* eslint-disable class-methods-use-this */
22

3+
import * as ecc from '@bitcoin-js/tiny-secp256k1-asmjs';
4+
import { BIP32Factory } from 'bip32';
5+
import { payments, networks } from 'bitcoinjs-lib';
6+
import type { IHdWalletAccount } from '@/types';
37
import { BaseProtocolAdapter } from '@/protocols/BaseProtocolAdapter';
8+
import { MAXIMUM_ACCOUNTS_TO_DISCOVER } from '@/constants';
49

510
export class BitcoinAdapter extends BaseProtocolAdapter {
11+
bip32 = BIP32Factory(ecc);
12+
613
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7-
async getBalance(address: string): Promise<string> {
8-
// TODO: Implement this with bitcoin adapter
9-
throw new Error('Method not implemented.');
14+
override async getBalance(address: string): Promise<string> {
15+
// TODO: Implement this once the mdw is ready
16+
return Promise.resolve('989983200000000000');
17+
}
18+
19+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
20+
override async isAccountUsed(address: string): Promise<boolean> {
21+
// TODO: Implement this
22+
return true;
23+
}
24+
25+
override getHdWalletAccountFromMnemonicSeed(
26+
seed: Uint8Array,
27+
accountIndex: number,
28+
): IHdWalletAccount {
29+
const node = this.bip32.fromSeed(Buffer.from(seed));
30+
const path = `m/84'/0'/${accountIndex}'/0/0`; // 44 for Legacy
31+
const child = node.derivePath(path);
32+
const { address } = payments.p2wpkh({ // p2pkh for Legacy
33+
pubkey: child.publicKey,
34+
// TODO: use bitcoin.networks.testnet once the network selection is ready
35+
network: networks.bitcoin,
36+
});
37+
const secretKey = child.toWIF();
38+
39+
return {
40+
secretKey,
41+
publicKey: child.publicKey!.toString('utf8'),
42+
address: address!,
43+
};
44+
}
45+
46+
override async discoverAccounts(seed: Uint8Array): Promise<number> {
47+
let lastNotEmptyIdx = 0;
48+
49+
for (let i = 0; i < MAXIMUM_ACCOUNTS_TO_DISCOVER; i += 1) {
50+
const account = this.getHdWalletAccountFromMnemonicSeed(seed, i);
51+
// eslint-disable-next-line no-await-in-loop
52+
if (await this.isAccountUsed(account.publicKey)) {
53+
lastNotEmptyIdx = i;
54+
}
55+
}
56+
return lastNotEmptyIdx;
1057
}
1158
}

0 commit comments

Comments
 (0)