Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Support external Namespaces in AlgoSigner #410

Merged
merged 4 commits into from
Jun 16, 2022
Merged
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
74 changes: 63 additions & 11 deletions packages/common/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Namespace } from './types';
import { Namespace, Ledger, Alias } from './types';

const MAX_ALIASES_PER_NAMESPACE = 6;

// prettier-ignore
interface ConfigTemplate {
name: string; // Formatted name, used for titles
ledgers: Array<string> | null; // Ledgers supported, null if there's no restriction
api: string; // Templated URL to call for uncached aliases
findAliasedAddresses: Function; // How to process the API response to get the aliased address
// @TODO: add caching/expiry
ledgers: any; // Object holding supported Ledgers as keys, templated API URL as value
findAliasedAddresses: Function; // How to process the API response to get the aliased addresses array
apiTimeout: number; // Amount in ms to wait for the API to respond
}

const noop = (): void => {
Expand All @@ -16,27 +18,77 @@ export class AliasConfig {
static [Namespace.AlgoSigner_Contacts]: ConfigTemplate = {
name: 'AlgoSigner Contact',
ledgers: null,
api: '',
findAliasedAddresses: noop,
apiTimeout: 0,
};

static [Namespace.AlgoSigner_Accounts]: ConfigTemplate = {
name: 'AlgoSigner Account',
ledgers: null,
api: '',
findAliasedAddresses: noop,
apiTimeout: 0,
};

static [Namespace.NFD]: ConfigTemplate = {
name: 'NFDomains',
ledgers: {
[Ledger.TestNet]:
'https://api.testnet.nf.domains/nfd?prefix=${term}&requireAddresses=true' +
`&limit=${MAX_ALIASES_PER_NAMESPACE}`,
[Ledger.MainNet]:
'https://api.nf.domains/nfd?prefix=${term}&requireAddresses=true' +
`&limit=${MAX_ALIASES_PER_NAMESPACE}`,
},
findAliasedAddresses: (response): Array<Alias> =>
response.map((o) => ({
name: o['name'],
address: o['caAlgo'][0],
namespace: Namespace.NFD,
})),
apiTimeout: 2000,
};

static [Namespace.ANS]: ConfigTemplate = {
name: 'Algorand Namespace Service',
ledgers: {
[Ledger.TestNet]:
'https://testnet.api.algonameservice.com/names?pattern=${term}' +
`&limit=${MAX_ALIASES_PER_NAMESPACE}`,
[Ledger.MainNet]:
'https://api.algonameservice.com/names?pattern=${term}' +
`&limit=${MAX_ALIASES_PER_NAMESPACE}`,
},
findAliasedAddresses: (response): Array<Alias> =>
response
.sort((a1, a2) => a1.name.localeCompare(a2.name))
.map((o) => ({
name: o['name'],
address: o['address'],
namespace: Namespace.ANS,
})),
apiTimeout: 2000,
};

public static getMatchingNamespaces(ledger: string): Array<any> {
const matchingNamespaces: Array<string> = [];
public static getMatchingNamespaces(ledger: string): Array<Namespace> {
const matchingNamespaces: Array<Namespace> = [];
for (const n in Namespace) {
if (
AliasConfig[n] &&
(AliasConfig[n].ledgers === null || AliasConfig[n].ledgers.includes(ledger))
(AliasConfig[n].ledgers === null || Object.keys(AliasConfig[n].ledgers).includes(ledger))
) {
matchingNamespaces.push(n);
matchingNamespaces.push(n as Namespace);
}
}
return matchingNamespaces;
}

public static getExternalNamespaces(): Array<string> {
const externalNamespaces: Array<string> = [];
for (const n in Namespace) {
if (AliasConfig[n] && AliasConfig[n].apiTimeout > 0) {
externalNamespaces.push(n);
}
}
return externalNamespaces;
}
}
2 changes: 2 additions & 0 deletions packages/common/src/messaging/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export enum JsonRpcMethod {
SaveContact = 'save-contact',
DeleteContact = 'delete-contact',
GetAliasedAddresses = 'get-aliased-addresses',
GetNamespaceConfigs = 'get-namespace-configs',
ToggleNamespaceConfig = 'toggle-namespace-config',

// Ledger Device Methods
LedgerSaveAccount = 'ledger-save-account',
Expand Down
4 changes: 4 additions & 0 deletions packages/common/src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ and Indexer in JSON format:
export const REFERENCE_ACCOUNT_TOOLTIP: string = `Reference accounts allow account tracking in AlgoSigner, but
they do not contain signing keys. They can only sign transactions if
they are rekeyed to another normal account also on AlgoSigner.`;

export const ALIAS_COLLISION_TOOLTIP: string = `Some of the aliases shown share the same name,
make sure to verify which Namespace you're using
by hovering on the alias before making a selection.`;
15 changes: 15 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,22 @@ export type WalletTransaction = {
readonly authAddr?: string;
};

export type Alias = {
readonly name: string;
readonly address: string;
readonly namespace: Namespace;
collides: boolean;
};

export enum Namespace {
AlgoSigner_Contacts = 'AlgoSigner_Contacts',
AlgoSigner_Accounts = 'AlgoSigner_Accounts',
NFD = 'NFD',
ANS = 'ANS',
}

export type NamespaceConfig = {
name: string;
namespace: Namespace;
toggle: boolean;
};
145 changes: 121 additions & 24 deletions packages/extension/src/background/messaging/internalMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import algosdk from 'algosdk';
import { JsonRpcMethod } from '@algosigner/common/messaging/types';
import { logging } from '@algosigner/common/logging';
import { ExtensionStorage } from '@algosigner/storage/src/extensionStorage';
import { Ledger, Namespace } from '@algosigner/common/types';
import { Alias, Ledger, Namespace, NamespaceConfig } from '@algosigner/common/types';
import { AliasConfig } from '@algosigner/common/config';
import { Task } from './task';
import { API, Cache } from './types';
import { Settings, } from '../config';
import { Settings } from '../config';
import encryptionWrap from '../encryptionWrap';
import Session from '../utils/session';
import AssetsDetailsHelper from '../utils/assetsDetailsHelper';
Expand Down Expand Up @@ -223,14 +223,33 @@ export class InternalMethods {
}
}
}

// Setup session
session.wallet = wallet;
session.ledger = Ledger.MainNet;
session.availableLedgers = availableLedgers;

// Load internal aliases
// Load internal aliases && namespace configurations
this.reloadAliases();
extensionStorage.getStorage('namespaces', (response: any) => {
const namespaceConfigs: Array<NamespaceConfig> = [];

const externalNamespaces: Array<string> = AliasConfig.getExternalNamespaces();
for (const n of externalNamespaces) {
const foundConfig = response.find((config) => config.namespace === n);
if (!foundConfig) {
namespaceConfigs.push({
name: AliasConfig[n].name,
namespace: n as Namespace,
toggle: true,
});
} else {
namespaceConfigs.push(foundConfig);
}
}

extensionStorage.setStorage('namespaces', namespaceConfigs, null);
});

sendResponse(session.session);
});
Expand Down Expand Up @@ -1235,36 +1254,114 @@ export class InternalMethods {
const { ledger, searchTerm } = request.body.params;

// Check if the term matches any of our namespaces
const matchingNamespaces = AliasConfig.getMatchingNamespaces(ledger);
const matchingNamespaces: Array<Namespace> = AliasConfig.getMatchingNamespaces(ledger);
const extensionStorage = new ExtensionStorage();

extensionStorage.getStorage('aliases', (response: any) => {
extensionStorage.getStorage('aliases', async (aliases: any) => {
// aliases: { ledger: { namespace: [...aliases] } }
const aliases = response;

// Search the storage for the aliases stored for the matching namespaces
const returnedAliasedAddresses = {};
for (const namespace of matchingNamespaces) {
const aliasesMatchingInNamespace = [];
if (aliases[ledger][namespace]) {
for (const alias of aliases[ledger][namespace]) {
if (alias.name.toLowerCase().includes(searchTerm.toLowerCase())) {
aliasesMatchingInNamespace.push({
name: alias.name,
address: alias.address,
namespace: namespace,
});
await extensionStorage.getStorage(
'namespaces',
async (storedConfigs: Array<NamespaceConfig>) => {
const availableExternalNamespaces: Array<string> = [];
storedConfigs
.filter((config) => config.toggle)
.map((config) => availableExternalNamespaces.push(config.namespace));

// Search the storage for the aliases stored for the matching namespaces
const returnedAliasedAddresses: Record<string, Array<Alias>> = {};
const apiFetches = [];
for (const namespace of matchingNamespaces) {
const aliasesMatchingInNamespace: Array<Alias> = [];
if (aliases[ledger][namespace]) {
for (const alias of aliases[ledger][namespace]) {
if (alias.name.toLowerCase().includes(searchTerm.toLowerCase())) {
aliasesMatchingInNamespace.push({
name: alias.name,
address: alias.address,
namespace: namespace,
collides: false,
});
}
}
}

if (
searchTerm &&
availableExternalNamespaces.includes(namespace) &&
AliasConfig[namespace].ledgers &&
AliasConfig[namespace].ledgers[ledger]?.length > 0
) {
// If we find enabled external namespaces, we prepare an API fetch
const apiURL = AliasConfig[namespace].ledgers[ledger].replace('${term}', searchTerm);
const apiTimeout = AliasConfig[namespace].apiTimeout;

// We set a max timeout for each call
const controller = new AbortController();
const timerId = setTimeout(() => controller.abort(), apiTimeout);
const handleResponse = async (response) => {
if (response.ok) {
await response.json().then((json) => {
const aliasesFromAPI = AliasConfig[namespace].findAliasedAddresses(json);
if (aliasesFromAPI && aliasesFromAPI.length) {
const aliasesInMemory: Array<Alias> = aliasesMatchingInNamespace;

// We add any new aliases to end of the namespace list
aliasesInMemory.push(...aliasesFromAPI);

// We add the updated aliases from the API to the response
returnedAliasedAddresses[namespace] = aliasesInMemory;
}
});
}
clearTimeout(timerId);
};
// We save the fetch request to later execute all in parallel
const apiCall = fetch(apiURL, { signal: controller.signal }).then(handleResponse);
apiFetches.push(apiCall);
} else {
returnedAliasedAddresses[namespace] = aliasesMatchingInNamespace;
}
}

await Promise.all(apiFetches);
sendResponse(returnedAliasedAddresses);
}
);
});

// Fallback to an api call goes here
return true;
}

returnedAliasedAddresses[namespace] = aliasesMatchingInNamespace;
}
sendResponse(returnedAliasedAddresses);
public static [JsonRpcMethod.GetNamespaceConfigs](request: any, sendResponse: Function) {
const extensionStorage = new ExtensionStorage();
extensionStorage.getStorage('namespaces', (response: any) => {
const namespaceConfigs: Array<NamespaceConfig> = response;
sendResponse(namespaceConfigs);
});
return true;
}

public static [JsonRpcMethod.ToggleNamespaceConfig](request: any, sendResponse: Function) {
const { namespace } = request.body.params;

const extensionStorage = new ExtensionStorage();
extensionStorage.getStorage('namespaces', (response: any) => {
const namespaceConfigs: Array<NamespaceConfig> = response;

const previousIndex = namespaceConfigs.findIndex((config) => config.namespace === namespace);
namespaceConfigs[previousIndex] = {
...namespaceConfigs[previousIndex],
toggle: !namespaceConfigs[previousIndex].toggle,
};

extensionStorage.setStorage('namespaces', namespaceConfigs, (isSuccessful: any) => {
if (isSuccessful) {
sendResponse(namespaceConfigs);
} else {
sendResponse({ error: 'Lock failed' });
}
});
});
return true;
}
}
Loading