Skip to content
Draft
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
31 changes: 31 additions & 0 deletions public/contracts/SuperheroIds.aes
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
contract SuperheroID =
record state =
{ data : map(address, map(string, string)) }

entrypoint init() =
{ data = {} }

stateful entrypoint set_data(key : string, value : string) =
let user_data = Map.lookup_default(Call.caller, state.data, {})
let new_user_data = user_data{ [key] = value }
put(state{ data[Call.caller] = new_user_data })

entrypoint get_data(key : string) : option(string) =
switch(Map.lookup(Call.caller, state.data))
None => None
Some(user_data) => Map.lookup(key, user_data)

entrypoint has_data() : bool =
Map.member(Call.caller, state.data)

entrypoint has_key(key : string) : bool =
switch(Map.lookup(Call.caller, state.data))
None => false
Some(user_data) => Map.member(key, user_data)

stateful entrypoint delete_key(key : string) =
switch(Map.lookup(Call.caller, state.data))
None => ()
Some(user_data) =>
let new_user_data = Map.delete(key, user_data)
put(state{ data[Call.caller] = new_user_data })
15 changes: 9 additions & 6 deletions src/composables/addressBook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export const useAddressBook = createCustomScopedComposable(() => {
Object.assign(savedEntry, entry);
}

function addAddressBookEntriesFromJson(json: string) {
function addAddressBookEntriesFromJson(json: string, showModal = true) {
try {
const newEntries: IAddressBookEntry[] = JSON.parse(json);
const totalEntries = Object.keys(newEntries).length;
Expand All @@ -162,11 +162,13 @@ export const useAddressBook = createCustomScopedComposable(() => {
}
});

openModal(MODAL_ADDRESS_BOOK_IMPORT, {
totalEntries,
successfulEntriesCount,
existingEntriesCount,
});
if (showModal) {
openModal(MODAL_ADDRESS_BOOK_IMPORT, {
totalEntries,
successfulEntriesCount,
existingEntriesCount,
});
}
} catch (error) {
handleUnknownError(error);
}
Expand Down Expand Up @@ -218,5 +220,6 @@ export const useAddressBook = createCustomScopedComposable(() => {
toggleBookmarkAddressBookEntry,
exportAddressBook,
importAddressBook,
addAddressBookEntriesFromJson,
};
});
2 changes: 2 additions & 0 deletions src/composables/aeSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
METHODS,
RPC_STATUS,
Encoded,
CompilerHttp,
} from '@aeternity/aepp-sdk';

import type {
Expand Down Expand Up @@ -144,6 +145,7 @@ export function useAeSdk() {
}],
id: APP_NAME,
type: IS_EXTENSION || IS_OFFSCREEN_TAB ? WALLET_TYPE.extension : WALLET_TYPE.window,
onCompiler: new CompilerHttp('https://v8.compiler.aepps.com'),
Copy link
Copy Markdown
Member

@davidyuk davidyuk Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, the http compiler is not for production use, it's better to precompile the contract on build using CompilerCli
I would use https://github.com/aeternity/contract-builder, but it is not maintained currently

onConnection(aeppId, params, origin) {
aeppInfo[aeppId] = { ...params, origin };
},
Expand Down
1 change: 1 addition & 0 deletions src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ export * from './addressBook';
export * from './addressBookEntryForm';
export * from './accountSelector';
export * from './ledger';
export * from './superheroId';
151 changes: 151 additions & 0 deletions src/composables/superheroId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {
Ref,
ref,
watch,
computed,
} from 'vue';
import { SuperheroIDService } from '@/protocols/aeternity/libs/SuperheroIDService';
import type { Encoded } from '@aeternity/aepp-sdk';
import { useAccounts } from '@/composables/accounts';
import { useAeSdk } from '@/composables/aeSdk';
import { useModals } from '@/composables/modals';
import { MODAL_CONFIRM_TRANSACTION_SIGN, PROTOCOLS } from '@/constants';
import { unpackTx } from '@aeternity/aepp-sdk';
import { useNetworks } from '@/composables/networks';
import { useAddressBook } from '@/composables/addressBook';
import { useCurrencies } from '@/composables/currencies';
import { CurrencyCode, ITx } from '@/types';

const superheroSvcRef: Ref<SuperheroIDService | null> = ref(null);
const hasSuperheroIdRef = ref(false);

export function useSuperheroId(): {
hasSuperheroId: Ref<boolean>;
syncAddressBook: (json: string) => Promise<void>;
syncSettings: (json: string) => Promise<void>;
loadAddressBook: () => Promise<void>;
loadSettings: () => Promise<void>;
deployContract: () => Promise<string>;
} { // eslint-disable-line
const { aeAccounts } = useAccounts();
const { getAeSdk } = useAeSdk();
const { openModal } = useModals();
const { addAddressBookEntriesFromJson } = useAddressBook();

const firstAeAddress = computed(() => (
aeAccounts.value?.[0]?.address as Encoded.AccountAddress | undefined));

function getService(): SuperheroIDService {
if (!(superheroSvcRef.value instanceof SuperheroIDService)) {
superheroSvcRef.value = new SuperheroIDService();
}
return superheroSvcRef.value;
}
// Settings (currency) integration (manual sync only)
const { currentCurrencyCode, setCurrentCurrency } = useCurrencies();

async function loadSettings() {
const addr = firstAeAddress.value;
if (!addr) return;
const svc = getService();
const val = await svc.getData('settings');
if (!val) return;
try {
const parsed = JSON.parse(val) as { currency?: CurrencyCode };
if (parsed?.currency && parsed.currency !== currentCurrencyCode.value) {
setCurrentCurrency(parsed.currency as CurrencyCode);
}
} catch { /* NOOP */ }
}

async function refreshHasSuperheroId(): Promise<void> {
const addr = firstAeAddress.value;
if (!addr) {
hasSuperheroIdRef.value = false;
return;
}
const svc = getService();
try {
hasSuperheroIdRef.value = await svc.hasAnyData();
} catch {
hasSuperheroIdRef.value = false;
}
}

async function syncData(domain: string, json: string): Promise<void> {
const addr = firstAeAddress.value;
if (!addr) throw new Error('No æternity account');
const svc = getService();
const txBase64 = await svc.buildSetDataTx(domain, json) as Encoded.Transaction;
const tx = unpackTx(txBase64) as unknown as ITx;
await openModal(MODAL_CONFIRM_TRANSACTION_SIGN, {
txBase64,
tx,
protocol: PROTOCOLS.aeternity,
app: { host: window.location.origin, href: window.location.origin },
});
const aeSdk = await getAeSdk();
const signed = await aeSdk
.signTransaction(txBase64, { fromAccount: addr } as any) as Encoded.Transaction;
const { txHash } = await aeSdk.api.postTransaction({ tx: signed as Encoded.Transaction });
await aeSdk.poll(txHash);
hasSuperheroIdRef.value = true;
}

async function syncAddressBook(json: string): Promise<void> {
return syncData('address_book', json);
}

async function syncSettings(json: string): Promise<void> {
return syncData('settings', json);
}

async function loadAddressBook() {
const addr = firstAeAddress.value;
if (!addr) throw new Error('No æternity account');
const svc = getService();
const val = await svc.getData('address_book');
hasSuperheroIdRef.value = !!val;
if (val) {
addAddressBookEntriesFromJson(val, false);
}
}

async function deployContract(): Promise<string> {
const svc = getService();
const res = await fetch('/contracts/SuperheroIds.aes');
const source = await res.text();
return svc.deployFromSource(source);
}

// Re-init on network change if an AE account exists
const { onNetworkChange } = useNetworks();
onNetworkChange(async () => {
await refreshHasSuperheroId();
await loadSettings();
});

// Auto-initialize service when first AE account is available; clear on removal
watch(
() => firstAeAddress.value,
async (addr) => {
if (addr) {
await refreshHasSuperheroId();
await loadSettings();
} else {
superheroSvcRef.value = null;
hasSuperheroIdRef.value = false;
}
},
{ immediate: true },
);

return {
hasSuperheroId: hasSuperheroIdRef,
syncAddressBook,
syncSettings,
loadAddressBook,
loadSettings,
deployContract,
};
}
2 changes: 2 additions & 0 deletions src/popup/components/DashboardBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
<slot name="buttons" />
</div>

<!-- action buttons are provided by parent via #buttons slot -->

<slot name="widgets" />

<DashboardCard
Expand Down
4 changes: 4 additions & 0 deletions src/popup/components/DashboardCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
:href="href"
:to="to"
:variant="variant"
:disabled="disabled"
inline
@click="$emit('click')"
/>
</Card>
</template>
Expand All @@ -30,10 +32,12 @@
BtnMain,
Card,
},
emits: ['click'],
props: {

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35

Check warning on line 36 in src/popup/components/DashboardCard.vue

View workflow job for this annotation

GitHub Actions / main

The "props" property should be above the "emits" property on line 35
title: { type: String, required: true },
description: { type: String, required: true },
btnText: { type: String, required: true },
disabled: { type: Boolean, default: false },
background: { type: String, default: null },
variant: { type: String as PropType<BtnVariant>, default: 'secondary' },
href: { type: String, default: null },
Expand Down
19 changes: 19 additions & 0 deletions src/popup/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@
"latestTransactionCard": {
"title": "Latest transactions"
},
"superheroId": {
"title": "Superhero ID",
"createDescription": "Create your Superhero ID to store your settings to the blockchain",
"connectDescription": "Connect to restore your settings from the blockchain",
"createBtn": "Create",
"connectBtn": "Connect",
"createdMsg": "Created Superhero ID.",
"createFailed": "Create failed",
"connectFailed": "Connection to Superhero ID contract failed",
"restoreMsg": "Your address book and preferences have been restored",
"deployFailed": "Deploy failed",
"deployedMsg": "Deployed: {ct}",
"syncSettingsBtn": "Sync Settings",
"settingsSynced": "Settings synced",
"settingsSyncFailed": "Settings sync failed"
},
"pendingMultisigCard": {
"title": "Pending multisig transaction"
}
Expand Down Expand Up @@ -736,6 +752,9 @@
"addressBook": "Address Book",
"own": "Own",
"recent": "Recent"
},
"superheroId": {
"synced": "Address Book Sync with SH ID completed"
}
},
"secureLogin": {
Expand Down
43 changes: 40 additions & 3 deletions src/popup/pages/AddressBook.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
:icon="ExportIcon"
@click="exportAddressBook()"
/>
<BtnBox
v-if="hasSuperheroId"
data-cy="sync-address-book"
:text="isSyncing ? 'Syncing…' : 'Sync'"
:disabled="isSyncing"
:icon="ExportIcon"
@click="onSyncAddressBook"
/>
</div>
</Transition>

Expand All @@ -35,16 +43,22 @@
<script lang="ts">
import { IonPage } from '@ionic/vue';
import { defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n';

import { ROUTE_ADDRESS_BOOK_ADD } from '@/popup/router/routeNames';
import { useAddressBook } from '@/composables';

import {
useAddressBook,
useModals,
useAccounts,
useSuperheroId,
} from '@/composables';
import AddressBookList from '@/popup/components/AddressBook/AddressBookList.vue';
import BtnBox from '@/popup/components/buttons/BtnBox.vue';

import AddIcon from '@/icons/plus-circle.svg?vue-component';
import ImportIcon from '@/icons/import-address-book.svg?vue-component';
import ExportIcon from '@/icons/export-address-book.svg?vue-component';
import { handleUnknownError } from '@/utils';

export default defineComponent({
components: {
Expand All @@ -54,13 +68,36 @@ export default defineComponent({
},
setup() {
const hideButtons = ref(false);
const { t } = useI18n();

const { exportAddressBook, importAddressBook, addressBook } = useAddressBook();
const { openDefaultModal } = useModals();
const { aeAccounts } = useAccounts();
const { syncAddressBook, hasSuperheroId } = useSuperheroId();

const isSyncing = ref(false);

const { exportAddressBook, importAddressBook } = useAddressBook();
async function onSyncAddressBook() {
try {
isSyncing.value = true;
const addr = aeAccounts.value?.[0]?.address as `ak_${string}`;
if (!addr) throw new Error('No æternity account');
await syncAddressBook(JSON.stringify(addressBook.value));
openDefaultModal({ title: t('dashboard.superheroId.title'), msg: t('pages.addressBook.superheroId.synced') });
} catch (e) {
handleUnknownError(e);
} finally {
isSyncing.value = false;
}
}

return {
hideButtons,
exportAddressBook,
importAddressBook,
onSyncAddressBook,
hasSuperheroId,
isSyncing,
AddIcon,
ImportIcon,
ExportIcon,
Expand Down
Loading
Loading