Skip to content

Commit

Permalink
feature: Intial test build on Solana cNFT minting option
Browse files Browse the repository at this point in the history
  • Loading branch information
newbreedofgeek committed Sep 12, 2024
1 parent e373cbc commit 025e79f
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 2 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@itheum/sdk-mx-data-nft",
"version": "3.6.1",
"description": "SDK for Itheum's Data NFT Technology on MultiversX Blockchain",
"version": "3.7.0-alpha.1",
"description": "SDK for Itheum's Data NFT Technology on MultiversX and Solana",
"main": "out/index.js",
"types": "out/index.d.js",
"files": [
Expand Down
225 changes: 225 additions & 0 deletions src/cnft-sol-minter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import {
dataNFTDataStreamAdvertise,
storeToIpfsOnlyImg,
createIpfsMetadataSolCNft,
storeToIpfsFullSolCNftMetadata
} from './common/mint-utils';
import { checkTraitsUrl, checkUrlIsUp } from './common/utils';
import { ErrArgumentNotSet } from './errors';
import { MinterSol } from './minter-sol';
import { StringValidator, validateResults } from './common/validator';

export class CNftSolMinter extends MinterSol {
/**
* Creates a new instance of the `SftMinter` class, which can be used to interact with the Data NFT-FT minter smart contract
* @param env 'devnet' | 'mainnet' | 'testnet'
*/
constructor(env: string) {
super(env);
}

/**
* Creates a `mint` transaction
*
* NOTE: The `dataStreamUrl` is being encrypted and the `media` and `metadata` urls are build and uploaded to IPFS
*
* NOTE: The `options.nftStorageToken` is required when not using custom image and traits, when using custom image and traits the traits should be compliant with the `traits` structure
*
* For more information, see the [README documentation](https://github.com/Itheum/sdk-mx-data-nft#create-a-mint-transaction).
*
* @param creatorAddress the address of the creator who we mint a CNft for
* @param tokenName the name of the DataNFT-FT. Between 3 and 20 alphanumeric characters, no spaces.
* @param dataMarshalUrl the url of the data marshal. A live HTTPS URL that returns a 200 OK HTTP code.
* @param dataStreamUrl the url of the data stream to be encrypted. A live HTTPS URL that returns a 200 OK HTTP code.
* @param dataPreviewUrl the url of the data preview. A live HTTPS URL that returns a 200 OK HTTP code.
* @param datasetTitle the title of the dataset. Between 10 and 60 alphanumeric characters.
* @param datasetDescription the description of the dataset. Between 10 and 400 alphanumeric characters.
* @param options [optional] below parameters are optional or required based on use case
* - imageUrl: the URL of the image for the Data NFT
* - traitsUrl: the URL of the traits for the Data NFT
* - nftStorageToken: the nft storage token to be used to upload the image and metadata to IPFS
* - extraAssets: [optional] extra URIs to attached to the NFT. Can be media files, documents, etc. These URIs are public
* - imgGenBg: [optional] the custom series bg to influence the image generation service
* - imgGenSet: [optional] the custom series layer set to influence the image generation service
*
*/
async mint(
creatorAddress: string,
tokenName: string,
dataMarshalUrl: string,
dataStreamUrl: string,
dataPreviewUrl: string,
datasetTitle: string,
datasetDescription: string,
options?: {
imageUrl?: string;
traitsUrl?: string;
nftStorageToken?: string;
extraAssets?: string[];
imgGenBg?: string;
imgGenSet?: string;
}
): Promise<{ imageUrl: string; metadataUrl: string; mintMeta: object }> {
const {
imageUrl,
traitsUrl,
nftStorageToken,
extraAssets,
imgGenBg,
imgGenSet
} = options ?? {};

const tokenNameValidator = new StringValidator()
.notEmpty()
.alphanumeric()
.minLength(3)
.maxLength(20)
.validate(tokenName);

const datasetTitleValidator = new StringValidator()
.notEmpty()
.minLength(10)
.maxLength(60)
.validate(datasetTitle.trim());

const datasetDescriptionValidator = new StringValidator()
.notEmpty()
.minLength(10)
.maxLength(400)
.validate(datasetDescription);

validateResults([
tokenNameValidator,
datasetTitleValidator,
datasetDescriptionValidator
]);

// deep validate all mandatory URLs
try {
await checkUrlIsUp(dataPreviewUrl, [200]);
await checkUrlIsUp(dataMarshalUrl + '/health-check', [200]);
} catch (error) {
throw error;
}

let imageOnIpfsUrl: string;
let metadataOnIpfsUrl: string;

const { dataNftHash, dataNftStreamUrlEncrypted } =
await dataNFTDataStreamAdvertise(
dataStreamUrl,
dataMarshalUrl,
creatorAddress // the caller is the Creator
);

if (!imageUrl) {
if (!nftStorageToken) {
throw new ErrArgumentNotSet(
'nftStorageToken',
'NFT Storage token is required when not using custom image and traits'
);
}

// create the img generative service API based on user options
let imgGenServiceApi = `${this.imageServiceUrl}/v1/generateNFTArt?hash=${dataNftHash}`;

if (imgGenBg && imgGenBg.trim() !== '') {
imgGenServiceApi += `&bg=${imgGenBg.trim()}`;
}

if (imgGenSet && imgGenSet.trim() !== '') {
imgGenServiceApi += `&set=${imgGenSet.trim()}`;
}

let resImgCall: any = '';
let dataImgCall: any = '';
let _imageFile: Blob = new Blob();

resImgCall = await fetch(imgGenServiceApi);
dataImgCall = await resImgCall.blob();
_imageFile = dataImgCall;

const traitsFromImgHeader = resImgCall.headers.get('x-nft-traits') || '';

const { imageOnIpfsUrl: imgOnIpfsUrl } = await storeToIpfsOnlyImg(
nftStorageToken,
_imageFile
);

const cNftMetadataContent = createIpfsMetadataSolCNft(
tokenName,
datasetTitle,
datasetDescription,
imgOnIpfsUrl,
creatorAddress,
dataNftStreamUrlEncrypted,
dataPreviewUrl,
dataMarshalUrl,
traitsFromImgHeader,
extraAssets ?? []
);

const { metadataIpfsUrl } = await storeToIpfsFullSolCNftMetadata(
nftStorageToken,
cNftMetadataContent
);

imageOnIpfsUrl = imgOnIpfsUrl;
metadataOnIpfsUrl = metadataIpfsUrl;
} else {
if (!traitsUrl) {
throw new ErrArgumentNotSet(
'traitsUrl',
'Traits URL is required when using custom image'
);
}

await checkTraitsUrl(traitsUrl);

imageOnIpfsUrl = imageUrl;
metadataOnIpfsUrl = traitsUrl;
}

// we not make a call to our private cNFt minter API
let mintMeta: any = {};

try {
const postHeaders = new Headers();
postHeaders.append('Content-Type', 'application/json');

const raw = JSON.stringify({
metadataOnIpfsUrl,
tokenName,
mintForSolAddr: creatorAddress,
solSignature: 'solSignature',
signatureNonce: 'signatureNonce'
});

const requestOptions = {
method: 'POST',
headers: postHeaders,
body: raw
};

let resMintCall: any = '';
let dataMintCall: any = '';

resMintCall = await fetch(this.solCNftMinterServiceUrl, requestOptions);
dataMintCall = await resMintCall.text();
mintMeta = dataMintCall;

// .then((response) => response.text())
// .then((result) => console.log(result))
// .catch((error) => console.error(error));
} catch (e: any) {
mintMeta = { error: true, errMsg: e.toString() };
throw e;
}

return {
imageUrl: imageOnIpfsUrl,
metadataUrl: metadataOnIpfsUrl,
mintMeta
};
}
}
104 changes: 104 additions & 0 deletions src/common/mint-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,37 @@ export async function storeToIpfs(
}
}

export async function storeToIpfsFullSolCNftMetadata(
storageToken: string,
metadataStructureSolCNft: object
): Promise<{ metadataIpfsUrl: string }> {
try {
const metadataIpfsHash = await storeTraitsToIpfs(
metadataStructureSolCNft,
storageToken
);
return {
metadataIpfsUrl: `https://ipfs.io/ipfs/${metadataIpfsHash}`
};
} catch (error) {
throw error;
}
}

export async function storeToIpfsOnlyImg(
storageToken: string,
image: Blob
): Promise<{ imageOnIpfsUrl: string }> {
try {
const imageHash = await storeImageToIpfs(image, storageToken);
return {
imageOnIpfsUrl: `https://ipfs.io/ipfs/${imageHash}`
};
} catch (error) {
throw error;
}
}

async function storeImageToIpfs(image: Blob, storageToken: string) {
const form = new FormData();
form.append('file', image);
Expand Down Expand Up @@ -127,6 +158,49 @@ export function createIpfsMetadata(
return metadata;
}

export function createIpfsMetadataSolCNft(
tokenName: string,
datasetTitle: string,
datasetDescription: string,
imageOnIpfsUrl: string,
creatorAddress: string,
dataNFTStreamUrl: string,
dataNFTStreamPreviewUrl: string,
dataNFTDataMarshalUrl: string,
traits: string,
extraAssets: string[]
) {
const metadata: Record<string, any> = {
name: tokenName,
description: `${datasetTitle} : ${datasetDescription}`,
image: imageOnIpfsUrl,
itheum_creator: creatorAddress,
itheum_data_stream_url: dataNFTStreamUrl,
itheum_data_preview_url: dataNFTStreamPreviewUrl,
itheum_data_marshal_url: dataNFTDataMarshalUrl,
attributes: [] as object[]
};

if (extraAssets && extraAssets.length > 0) {
metadata.extra_assets = extraAssets;
}

const attributes = traits
.split(',')
.filter((element) => element.trim() !== '');

const metadataAttributes = [];

for (const attribute of attributes) {
const [key, value] = attribute.split(':');
const trait = { trait_type: key.trim(), value: value.trim() };
metadataAttributes.push(trait);
}

metadata.attributes = metadataAttributes;
return metadata;
}

export async function createFileFromUrl(
url: string,
datasetTitle: string,
Expand Down Expand Up @@ -154,3 +228,33 @@ export async function createFileFromUrl(
const _traitsFile = traits;
return { image: _imageFile, traits: _traitsFile };
}

export async function createFileFromUrlSolCNft(
url: string,
datasetTitle: string,
datasetDescription: string,
dataNFTStreamPreviewUrl: string,
address: string,
extraAssets: string[]
) {
let res: any = '';
let data: any = '';
let _imageFile: Blob = new Blob();

if (url) {
res = await fetch(url);
data = await res.blob();
_imageFile = data;
}

const traits = createIpfsMetadata(
res.headers.get('x-nft-traits') || '',
datasetTitle,
datasetDescription,
dataNFTStreamPreviewUrl,
address,
extraAssets
);
const _traitsFile = traits;
return { image: _imageFile, traits: _traitsFile };
}
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export const imageService: { [key in EnvironmentsEnum]: string } = {
testnet: ''
};

export const solCNftMinterService: { [key in EnvironmentsEnum]: string } = {
devnet: 'https://api.itheumcloud-stg.com/datadexapi/solNftUtils/mintNft',
mainnet: 'https://api.itheumcloud.com/datadexapi/solNftUtils/mintNft',
testnet: ''
};

export const marshalUrls = {
devnet: 'https://api.itheumcloud-stg.com/datamarshalapi/router/v1',
mainnet: 'https://api.itheumcloud.com/datamarshalapi/router/v1',
Expand Down
19 changes: 19 additions & 0 deletions src/contract-sol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { EnvironmentsEnum, networkConfiguration } from './config';
import { ErrNetworkConfig } from './errors';

export abstract class ContractSol {
readonly chainID: string;
readonly env: string;

protected constructor(env: string) {
if (!(env in EnvironmentsEnum)) {
throw new ErrNetworkConfig(
`Invalid environment: ${env}, Expected: 'devnet' | 'mainnet' | 'testnet'`
);
}

this.env = env;
const networkConfig = networkConfiguration[env as EnvironmentsEnum];
this.chainID = networkConfig.chainID;
}
}
Loading

0 comments on commit 025e79f

Please sign in to comment.