Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev' into feat/add-address-balan…
Browse files Browse the repository at this point in the history
…ce-chronicle-nova
  • Loading branch information
begonaalvarezd committed Feb 14, 2024
2 parents 1ac0b34 + 788ea46 commit 67bfbea
Show file tree
Hide file tree
Showing 21 changed files with 1,344 additions and 114 deletions.
11 changes: 11 additions & 0 deletions api/src/models/api/nova/ISearchRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ISearchRequest {
/**
* The network to search on.
*/
network: string;

/**
* The query to look for.
*/
query: string;
}
42 changes: 42 additions & 0 deletions api/src/models/api/nova/ISearchResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Block, OutputResponse } from "@iota/sdk-nova";
import { IAddressDetails } from "./IAddressDetails";
import { IResponse } from "../IResponse";

export interface ISearchResponse extends IResponse {
/**
* Block if it was found.
*/
block?: Block;

/**
* Address details.
*/
addressDetails?: IAddressDetails;

/**
* Output if it was found (block will also be populated).
*/
output?: OutputResponse;

/**
* Account id if it was found.
*/
accountId?: string;

/**
* Anchor id if it was found.
*/
anchorId?: string;

/**
* Foundry id if it was found.
*/
foundryId?: string;

/**
* Nft id if it was found.
*/
nftId?: string;
}
1 change: 1 addition & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export const routes: IRoute[] = [
folder: "nova/address/balance/chronicle",
func: "get",
},
{ path: "/nova/search/:network/:query", method: "get", folder: "nova", func: "search" },
{ path: "/nova/output/:network/:outputId", method: "get", folder: "nova/output", func: "get" },
{ path: "/nova/output/rewards/:network/:outputId", method: "get", folder: "nova/output/rewards", func: "get" },
{ path: "/nova/account/:network/:accountId", method: "get", folder: "nova/account", func: "get" },
Expand Down
30 changes: 30 additions & 0 deletions api/src/routes/nova/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ServiceFactory } from "../../factories/serviceFactory";
import { ISearchRequest } from "../../models/api/nova/ISearchRequest";
import { ISearchResponse } from "../../models/api/nova/ISearchResponse";
import { IConfiguration } from "../../models/configuration/IConfiguration";
import { NOVA } from "../../models/db/protocolVersion";
import { NetworkService } from "../../services/networkService";
import { NovaApiService } from "../../services/nova/novaApiService";
import { ValidationHelper } from "../../utils/validationHelper";

/**
* Find the object from the network.
* @param _ The configuration.
* @param request The request.
* @returns The response.
*/
export async function search(_: IConfiguration, request: ISearchRequest): Promise<ISearchResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.string(request.query, "query");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const novaApiService = ServiceFactory.get<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.search(request.query);
}
19 changes: 19 additions & 0 deletions api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,28 @@ import { IBlockResponse } from "../../models/api/nova/IBlockResponse";
import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse";
import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse";
import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse";
import { ISearchResponse } from "../../models/api/nova/ISearchResponse";
import { INetwork } from "../../models/db/INetwork";
import { HexHelper } from "../../utils/hexHelper";
import { SearchExecutor } from "../../utils/nova/searchExecutor";
import { SearchQueryBuilder } from "../../utils/nova/searchQueryBuilder";

/**
* Class to interact with the nova API.
*/
export class NovaApiService {
/**
* The network in context.
*/
private readonly network: INetwork;

/**
* The client to use for requests.
*/
private readonly client: Client;

constructor(network: INetwork) {
this.network = network;
this.client = ServiceFactory.get<Client>(`client-${network.network}`);
}

Expand Down Expand Up @@ -154,4 +163,14 @@ export class NovaApiService {

return manaRewardsResponse ? { outputId, manaRewards: manaRewardsResponse } : { outputId, message: "Rewards data not found" };
}

/**
* Find item on the stardust network.
* @param query The query to use for finding items.
* @returns The item found.
*/
public async search(query: string): Promise<ISearchResponse> {
const searchQuery = new SearchQueryBuilder(query, this.network.bechHrp).build();
return new SearchExecutor(this.network, searchQuery).run();
}
}
146 changes: 146 additions & 0 deletions api/src/utils/nova/addressHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import {
Address,
AddressType,
AccountAddress,
Ed25519Address,
NftAddress,
AnchorAddress,
Utils,
ImplicitAccountCreationAddress,
RestrictedAddress,
} from "@iota/sdk-nova";
import { plainToInstance } from "class-transformer";
import { IAddressDetails } from "../../models/api/nova/IAddressDetails";
import { HexHelper } from "../hexHelper";

export class AddressHelper {
/**
* Build the address details.
* @param hrp The human readable part of the address.
* @param address The address to source the data from.
* @param typeHint The type of the address.
* @returns The parts of the address.
*/
public static buildAddress(hrp: string, address: string | Address, typeHint?: number): IAddressDetails {
return typeof address === "string" ? this.buildAddressFromString(hrp, address, typeHint) : this.buildAddressFromTypes(address, hrp);
}

private static buildAddressFromString(hrp: string, addressString: string, typeHint?: number): IAddressDetails {
let bech32: string;
let hex: string;
let type: AddressType;
if (Utils.isAddressValid(addressString)) {
try {
const address: Address = Utils.parseBech32Address(addressString);

if (address) {
bech32 = addressString;
type = address.type;
hex = Utils.bech32ToHex(addressString);
}
} catch (e) {
console.error(e);
}
}

if (!bech32) {
// We assume this is hex
hex = addressString;
if (typeHint) {
bech32 = this.computeBech32FromHexAndType(hex, type, hrp);
}
}

return {
bech32,
hex: hex ? HexHelper.addPrefix(hex) : hex,
type,
label: AddressHelper.typeLabel(type),
restricted: false,
};
}

private static buildAddressFromTypes(
address: Address,
hrp: string,
restricted: boolean = false,
capabilities?: number[],
): IAddressDetails {
let hex: string = "";
let bech32: string = "";

if (address.type === AddressType.Ed25519) {
hex = (address as Ed25519Address).pubKeyHash;
} else if (address.type === AddressType.Account) {
hex = (address as AccountAddress).accountId;
} else if (address.type === AddressType.Nft) {
hex = (address as NftAddress).nftId;
} else if (address.type === AddressType.Anchor) {
hex = (address as AnchorAddress).anchorId;
} else if (address.type === AddressType.ImplicitAccountCreation) {
const implicitAccountCreationAddress = plainToInstance(ImplicitAccountCreationAddress, address);
const innerAddress = implicitAccountCreationAddress.address();
hex = innerAddress.pubKeyHash;
} else if (address.type === AddressType.Restricted) {
const restrictedAddress = plainToInstance(RestrictedAddress, address);
const innerAddress = restrictedAddress.address;

return this.buildAddressFromTypes(
innerAddress,
hrp,
true,
Array.from(restrictedAddress.getAllowedCapabilities() as ArrayLike<number>),
);
}

bech32 = this.computeBech32FromHexAndType(hex, address.type, hrp);

return {
bech32,
hex,
type: address.type,
label: AddressHelper.typeLabel(address.type),
restricted,
capabilities,
};
}

private static computeBech32FromHexAndType(hex: string, addressType: AddressType, hrp: string) {
let bech32 = "";

if (addressType === AddressType.Ed25519) {
bech32 = Utils.hexToBech32(hex, hrp);
} else if (addressType === AddressType.Account) {
bech32 = Utils.accountIdToBech32(hex, hrp);
} else if (addressType === AddressType.Nft) {
bech32 = Utils.nftIdToBech32(hex, hrp);
} else if (addressType === AddressType.Anchor) {
// Update to Utils.anchorIdToBech32 when it gets implemented
bech32 = Utils.accountIdToBech32(hex, hrp);
} else if (addressType === AddressType.ImplicitAccountCreation) {
bech32 = Utils.hexToBech32(hex, hrp);
}

return bech32;
}

/**
* Convert the address type number to a label.
* @param addressType The address type to get the label for.
* @returns The label.
*/
private static typeLabel(addressType?: AddressType): string | undefined {
if (addressType === AddressType.Ed25519) {
return "Ed25519";
} else if (addressType === AddressType.Account) {
return "Account";
} else if (addressType === AddressType.Nft) {
return "NFT";
} else if (addressType === AddressType.Anchor) {
return "Anchor";
}
}
}
Loading

0 comments on commit 67bfbea

Please sign in to comment.