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

feat: add basic nova search #1102

Merged
merged 10 commits into from
Feb 14, 2024
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 @@ -202,6 +202,7 @@ export const routes: IRoute[] = [
func: "get",
},
// Nova
{ 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 {
begonaalvarezd marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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
Loading