Skip to content

Commit

Permalink
Merge pull request #2594 from build-5/master
Browse files Browse the repository at this point in the history
Merge latest
  • Loading branch information
adamunchained authored Sep 16, 2023
2 parents dd56160 + 6f65aa1 commit 45bf316
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 1 deletion.
4 changes: 3 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"joi": "^17.10.1",
"lodash": "^4.17.21",
"rxjs": "^7.8.1",
"ws": "^8.13.0"
"ws": "^8.13.0",
"@iota/iota.js-next": "npm:@iota/[email protected]",
"@iota/util.js-next": "npm:@iota/[email protected]"
},
"devDependencies": {
"@types/express": "^4.17.17",
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { getPriceChange } from './getPriceChange';
import { getTokenPrice } from './getTokenPrice';
import { getTopMilestones } from './getTopMilestones';
import { getUpdatedAfter } from './getUpdatedAfter';
import { getNftIds } from './metadataNft/getNftIds';
import { getNftMutableMetadata } from './metadataNft/getNftMutableMetadata';
import { getNftMutableMetadataHistory } from './metadataNft/getNftMutableMetadataHistory';
import { sendLiveUpdates } from './sendLiveUpdates';

const port = process.env.PORT || 3000;
Expand Down Expand Up @@ -83,6 +86,12 @@ const getObservable = (url: string): Promise<Observable<unknown>> => {
return getAddresses(url);
case ApiRoutes.GET_TOP_MILESTONES:
return getTopMilestones(url);
case ApiRoutes.GET_NFT_MUTABLE_METADATA:
return getNftMutableMetadata(url);
case ApiRoutes.GET_NFT_IDS:
return getNftIds(url);
case ApiRoutes.GET_NFT_MUTABLE_METADATA_HISTORY:
return getNftMutableMetadataHistory(url);
default:
throw { code: 400, message: 'Invalid route' };
}
Expand Down
46 changes: 46 additions & 0 deletions packages/api/src/metadataNft/getNftIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { GetNftIds, Network, WenError } from '@build-5/interfaces';
import {
Bech32Helper,
INftOutput,
IndexerPluginClient,
NFT_ADDRESS_TYPE,
TransactionHelper,
} from '@iota/iota.js-next';
import { Converter, HexHelper } from '@iota/util.js-next';
import Joi from 'joi';
import { of } from 'rxjs';
import { CommonJoi, getQueryParams } from '../common';
import { EMPTY_NFT_ID, getShimmerClient } from './wallet';

const getNftIdsSchema = Joi.object({
network: Joi.string().valid(Network.SMR, Network.RMS),
collectionId: CommonJoi.uid(),
});

export const getNftIds = async (url: string) => {
const body = getQueryParams<GetNftIds>(url, getNftIdsSchema);
try {
const client = await getShimmerClient(body.network);
const indexer = new IndexerPluginClient(client);

const collectionOutputId = (await indexer.nft(body.collectionId)).items[0];
const { nftId: collectionId } = (await client.output(collectionOutputId)).output as INftOutput;
const issuerAddress = Bech32Helper.toBech32(
NFT_ADDRESS_TYPE,
Converter.hexToBytes(HexHelper.stripPrefix(collectionId)),
body.network || Network.SMR,
);

const nftOutputIds = (await indexer.nfts({ issuerBech32: issuerAddress })).items;
const promises = nftOutputIds.map(async (outputId) => {
const output = (await client.output(outputId)).output as INftOutput;
if (output.nftId === EMPTY_NFT_ID) {
return TransactionHelper.resolveIdFromOutputId(outputId);
}
return output.nftId;
});
return of(await Promise.all(promises));
} catch (error) {
throw { code: 400, message: WenError.invalid_collection_id.key };
}
};
24 changes: 24 additions & 0 deletions packages/api/src/metadataNft/getNftMutableMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { GetNftMutableData, Network, WenError } from '@build-5/interfaces';
import { INftOutput, IndexerPluginClient } from '@iota/iota.js-next';
import Joi from 'joi';
import { of } from 'rxjs';
import { CommonJoi, getQueryParams } from '../common';
import { getMutableMetadata, getShimmerClient } from './wallet';

const getNftMutableDataSchema = Joi.object({
network: Joi.string().valid(Network.SMR, Network.RMS),
nftId: CommonJoi.uid(),
});

export const getNftMutableMetadata = async (url: string) => {
const body = getQueryParams<GetNftMutableData>(url, getNftMutableDataSchema);
try {
const client = await getShimmerClient(body.network);
const indexer = new IndexerPluginClient(client);
const outputId = (await indexer.nft(body.nftId)).items[0];
const output = (await client.output(outputId)).output as INftOutput;
return of(getMutableMetadata(output));
} catch {
throw { code: 400, message: WenError.invalid_nft_id.key };
}
};
75 changes: 75 additions & 0 deletions packages/api/src/metadataNft/getNftMutableMetadataHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { GetNftMutableMetadatHistory, Network, WenError } from '@build-5/interfaces';
import {
INftOutput,
IOutputResponse,
ITransactionPayload,
IndexerPluginClient,
NFT_OUTPUT_TYPE,
SingleNodeClient,
TransactionHelper,
} from '@iota/iota.js-next';
import Joi from 'joi';
import { isEqual, last } from 'lodash';
import { of } from 'rxjs';
import { CommonJoi, getQueryParams } from '../common';
import { EMPTY_NFT_ID, getMutableMetadata, getShimmerClient } from './wallet';

const getNftMutableMetadataHistorySchema = Joi.object({
network: Joi.string().valid(Network.SMR, Network.RMS),
nftId: CommonJoi.uid(),
});

export const getNftMutableMetadataHistory = async (url: string) => {
const body = getQueryParams<GetNftMutableMetadatHistory>(url, getNftMutableMetadataHistorySchema);
const history: any[] = [];
try {
const client = await getShimmerClient(body.network);
const indexer = new IndexerPluginClient(client);
const outputId = (await indexer.nft(body.nftId)).items[0];
let outputResponse: IOutputResponse | undefined = await client.output(outputId);
do {
const metadata = getMutableMetadata(outputResponse.output as INftOutput);
if (!isEqual(metadata, last(history))) {
history.push(metadata);
}
outputResponse = await getPrevNftOutput(client, outputResponse);
} while (outputResponse !== undefined);

history.reverse();
const response = history.reduce((acc, act, i) => ({ ...acc, [i]: act }), {});
return of(response);
} catch {
throw { code: 400, message: WenError.invalid_nft_id.key };
}
};

const getPrevNftOutput = async (client: SingleNodeClient, output: IOutputResponse) => {
if ((output.output as INftOutput).nftId === EMPTY_NFT_ID) {
return undefined;
}
const block = await client.block(output.metadata.blockId);
const inputs = (block.payload as ITransactionPayload).essence.inputs;
const prevOutputIds = inputs.map(({ transactionId, transactionOutputIndex }) =>
TransactionHelper.outputIdFromTransactionData(transactionId, transactionOutputIndex),
);
for (const prevOutputId of prevOutputIds) {
const prevOutputResponse = await client.output(prevOutputId);
const prevOutput = prevOutputResponse.output;
if (prevOutput.type !== NFT_OUTPUT_TYPE) {
continue;
}
const prevNftId = getNftId(prevOutputId, prevOutput);
if (prevNftId === (output.output as INftOutput).nftId) {
return prevOutputResponse;
}
}

return undefined;
};

const getNftId = (outputId: string, output: INftOutput) => {
if (output.nftId === EMPTY_NFT_ID) {
return TransactionHelper.resolveIdFromOutputId(outputId);
}
return output.nftId;
};
51 changes: 51 additions & 0 deletions packages/api/src/metadataNft/wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Network } from '@build-5/interfaces';
import {
IMetadataFeature,
INftOutput,
METADATA_FEATURE_TYPE,
SingleNodeClient,
} from '@iota/iota.js-next';
import { Converter } from '@iota/util.js-next';

const RMS_API_ENDPOINTS = ['https://rms1.svrs.io/'];

const SMR_API_ENDPOINTS = ['https://smr1.svrs.io/', 'https://smr3.svrs.io/'];

export const getShimmerClient = async (network = Network.SMR) => {
let url = '';
for (let i = 0; i < 5; ++i) {
url = getEndpointUrl(network);
try {
const client = new SingleNodeClient(url);
const healty = await client.health();
if (healty) {
return client;
}
} catch (error) {
console.warn(`Could not connect to client ${network}`, url, error);
}
await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 1000 + 500)));
}
console.error(`Could not connect to client ${network}`, url);
throw Error(`Could not connect to any client ${network}`);
};

const getEndpointUrl = (network: Network) => {
const urls = network === Network.SMR ? SMR_API_ENDPOINTS : RMS_API_ENDPOINTS;
return getRandomElement(urls);
};

const getRandomElement = <T>(array: T[]) => array[Math.floor(Math.random() * array.length)];

export const EMPTY_NFT_ID = '0x0000000000000000000000000000000000000000000000000000000000000000';

export const getMutableMetadata = (output: INftOutput) => {
const hexMetadata = <IMetadataFeature | undefined>(
output?.features?.find((f) => f.type === METADATA_FEATURE_TYPE)
);
if (!hexMetadata?.data) {
return {};
}
const mutableMetadata = JSON.parse(Converter.hexToUtf8(hexMetadata.data) || '{}');
return mutableMetadata;
};
4 changes: 4 additions & 0 deletions packages/interfaces/src/api/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export enum ApiRoutes {
GET_ADDRESSES = '/addresses',
GET_TOP_MILESTONES = '/getTopMilestones',
KEEP_ALIVE = '/keepAlive',

GET_NFT_MUTABLE_METADATA = '/getNftMutableMetadata',
GET_NFT_IDS = '/getNftIds',
GET_NFT_MUTABLE_METADATA_HISTORY = '/getNftMutableMetadataHistory',
}

export const PING_INTERVAL = 10000;
33 changes: 33 additions & 0 deletions packages/interfaces/src/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,36 @@ export interface GetPriceChangeResponse {
export interface GetTopMilestonesRequest {}

export type GetTopMilestonesResponse = { [key: string]: Milestone };

/**
* Get mutable metadata for an nft
*/
export interface GetNftMutableData {
/**
* Network, set it only if nft was not minted on mainnet
*/
readonly network?: Network;
readonly nftId: string;
}

/**
* Get all nft ids belonging to a collection
*/
export interface GetNftIds {
/**
* Network, set it only if nft was not minted on mainnet
*/
readonly network?: Network;
readonly collectionId: string;
}

/**
* Get historycal mutable metadata for an nft
*/
export interface GetNftMutableMetadatHistory {
/**
* Network, set it only if nft was not minted on mainnet
*/
readonly network?: Network;
readonly nftId: string;
}

0 comments on commit 45bf316

Please sign in to comment.