diff --git a/package.json b/package.json index 33b220b..0977766 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "apillon-web3-tools", - "version": "2.0.0", + "version": "3.0.0", "description": "Monorepo for Apillon tools", "author": "Apillon", "license": "MIT", diff --git a/packages/cli/README.md b/packages/cli/README.md index 7f02e4b..05dfeb7 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -302,8 +302,7 @@ apillon storage list-objects --bucket-uuid "123e4567-e89b-12d3-a456-426655440000 "updateTime": "2023-11-23T08:55:46.000Z", "uuid": "14a7a891-877c-41ac-900c-7382347e1e77", "name": "index.html", - "CID": "QmWX5CcNvnaVmgGBn4o82XW9uW1uLvsHQDdNrANrQeSdXm", - "CIDv1": "bafybeidzrd7p5ddj67j2mud32cbnze2c7b2pvbhn...", + "CID": "bafybeidzrd7p5ddj67j2mud32cbnze2c7b2pvbhn...", "status": "AVAILABLE_ON_IPFS_AND_REPLICATED", "directoryUuid": null, "type": "FILE", @@ -350,8 +349,7 @@ apillon storage list-files --bucket-uuid "123e4567-e89b-12d3-a456-426655440000" "createTime": "2023-11-15T09:58:04.000Z", "updateTime": "2023-11-15T09:58:10.000Z", "name": "style.css", - "CID": "QmWX5CcNvnaVmgGBn4o82XW9uW1uLvsHQDdNrANrQeSdXm", - "CIDv1": "bafybeidzrd7p5ddj67j2mud32cbnze2c7b2pvbag...", + "CID": "bafybeidzrd7p5ddj67j2mud32cbnze2c7b2pvbag...", "status": "AVAILABLE_ON_IPFS_AND_REPLICATED", "directoryUuid": null, "type": "FILE", @@ -438,7 +436,7 @@ Lists all IPNS records for a specific bucket. **Example** ```sh -apillon ipns list --bucket-uuid "123e4567-e89b-12d3-a456-426655440000" +apillon storage ipns list --bucket-uuid "123e4567-e89b-12d3-a456-426655440000" ``` **Example response** @@ -484,41 +482,44 @@ Creates a new IPNS record for a specific bucket. **Example** ```sh -apillon ipns create --bucket-uuid "123e4567-e89b-12d3-a456-426655440000" --name "my-ipns-record" --cid "QmWX5CcNvnaVmgGBn4o82XW9uW1uLvsHQDdNrANrQeSdXm" +apillon storage ipns create --bucket-uuid "123e4567-e89b-12d3-a456-426655440000" --name "my-ipns-record" --cid "QmWX5CcNvnaVmgGBn4o82XW9uW1uLvsHQDdNrANrQeSdXm" ``` #### `storage ipns get` Retrieves information about a specific IPNS record. **Options** +- `-b, --bucket-uuid `: UUID of the bucket. - `-i, --ipns-uuid `: UUID of the IPNS record. **Example** ```sh -apillon ipns get --ipns-uuid "123e4567-e89b-12d3-a456-426655440000" +apillon storage ipns get --ipns-uuid "123e4567-e89b-12d3-a456-426655440000" ``` #### `storage ipns publish` Publishes an IPNS record to IPFS and links it to a CID. **Options** +- `-b, --bucket-uuid `: UUID of the bucket. - `-i, --ipns-uuid `: UUID of the IPNS record. - `-c, --cid `: CID to which this IPNS name will point. **Example** ```sh -apillon ipns publish --ipns-uuid "123e4567-e89b-12d3-a456-426655440000" --cid "QmWX5CcNvnaVmgGBn4o82XW9uW1uLvsHQDdNrANrQeSdXm" +apillon storage ipns publish --ipns-uuid "123e4567-e89b-12d3-a456-426655440000" --cid "QmWX5CcNvnaVmgGBn4o82XW9uW1uLvsHQDdNrANrQeSdXm" ``` #### `storage ipns delete` Deletes an IPNS record from a specific bucket. **Options** +- `-b, --bucket-uuid `: UUID of the bucket. - `-i, --ipns-uuid `: UUID of the IPNS record. **Example** ```sh -apillon ipns delete --ipns-uuid "123e4567-e89b-12d3-a456-426655440000" +apillon storage ipns delete --ipns-uuid "123e4567-e89b-12d3-a456-426655440000" ``` ## NFT Commands diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 1d68c05..47e1901 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -9,8 +9,8 @@ as well as compresses multi step flows into single operations. ## Requirements -- npm 8.4.0 or higher -- node.js 16.17.0 or higher +- npm 10.0.0 or higher +- node.js 20.0.0 or higher - Apillon API key and secret ## Getting started @@ -46,6 +46,12 @@ View each individual module examples in the sections below. This wiki only contains the basic installation and examples of SDK usage. For additional information on using the SDK, see the [Detailed SDK documentation](https://sdk-docs.apillon.io/). +### Examples + +Examples for using Apillon can be found in a demo repo [here](https://github.com/Apillon/apillon-sdk-demo). Instructions on running the examples are in the [README file](https://github.com/Apillon/apillon-sdk-demo/blob/master/README.md). + +> You can run examples directly in your browser via [CodeSandbox](https://codesandbox.io/p/github/Apillon/apillon-sdk-demo/master). + ## Hosting Hosting module encapsulates functionalities for Hosting service available on Apillon dashboard. @@ -76,7 +82,7 @@ import * as fs from 'fs'; const hosting = new Hosting({ key: 'yourApiKey', secret: 'yourApiSecret', - logLevel: LogLevel.NONE, + logLevel: LogLevel.VERBOSE, }); // list all websites @@ -134,7 +140,7 @@ import * as fs from 'fs'; const storage = new Storage({ key: 'yourApiKey', secret: 'yourApiSecret', - logLevel: LogLevel.NONE, + logLevel: LogLevel.VERBOSE, }); // list buckets @@ -190,7 +196,7 @@ import { Storage, LogLevel } from '@apillon/sdk'; const storage = new Storage({ key: 'yourApiKey', secret: 'yourApiSecret', - logLevel: LogLevel.NONE, + logLevel: LogLevel.VERBOSE, }); // create and instance of a bucket directly through uuid @@ -235,11 +241,11 @@ import { const nft = new Nft({ key: 'yourApiKey', secret: 'yourApiSecret', - logLevel: LogLevel.NONE, + logLevel: LogLevel.VERBOSE, }); // create a new collection -const collection1 = await nft.create({ +let collection = await nft.create({ collectionType: CollectionType.GENERIC, chain: EvmChain.MOONBEAM, name: 'SpaceExplorers', @@ -258,20 +264,31 @@ const collection1 = await nft.create({ dropPrice: 0.05, dropReserve: 100, }); +// or create a substrate collection +const substrateCollection = await nft.createSubstrate({ + collectionType: CollectionType.GENERIC, + chain: SubstrateChain.ASTAR, + name: 'SpaceExplorers', + symbol: 'SE', + ... +}); // check if collection is deployed - available on chain -if (collection1.collectionStatus == CollectionStatus.DEPLOYED) { - console.log('Collection deployed: ', collection1.transactionHash); +if (collection.collectionStatus == CollectionStatus.DEPLOYED) { + console.log('Collection deployed: ', collection.transactionHash); } // search through collections await nft.listCollections({ search: 'My NFT' }); // create and instance of collection directly through uuid -const collection = await nft.collection('uuid').get(); +collection = await nft.collection('uuid').get(); // mint a new nft in the collection -await collection.mint('0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 1); +await collection.mint({ + receivingAddress: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', + quantity: 1, +}); // nest mint a new nft if collection type is NESTABLE await collection.nestMint(collection.uuid, 1, 1); @@ -292,7 +309,6 @@ await collection.transferOwnership( ); ``` - ## Identity Identity module encapsulates functionalities for validating EVM and Polkadot wallet signatures, as well as fetching Polkadot Identity data for any wallet. @@ -302,14 +318,13 @@ For detailed hosting SDK method, class and property documentation visit [SDK ide ### Usage example ```ts -import { Identity } from './modules/identity/identity'; -import { LogLevel } from './types/apillon'; +import { Identity, LogLevel } from '@apillon/sdk'; // Note: for signature-related methods API config is not required const identity = new Identity({ key: 'yourApiKey', secret: 'yourApiSecret', - logLevel: LogLevel.NONE, + logLevel: LogLevel.VERBOSE, }); // obtain on-chain identity data for a Polkadot wallet @@ -364,4 +379,90 @@ async function validatePolkadotWalletSignature() { } ``` +## Computing + +The Computing module provides functionalities for managing computing contracts, including creating contracts, listing contracts, and interacting with specific contracts for operations like encryption and ownership transfer. + +### Usage example + +```ts +import { Computing } from '@apillon/sdk'; + +const computing = new Computing({ + key: 'yourApiKey', + secret: 'yourApiSecret', +}); + +// List all computing contracts +const contracts = await computing.listContracts(); + +// Create a new computing contract +const newContract = await computing.createContract({ + name: 'New Contract', + description: 'Description of the new contract', + bucket_uuid, + contractData: { + nftContractAddress: '0xabc...', + nftChainRpcUrl: ChainRpcUrl.ASTAR, + }, +}); + +// Interact with a specific computing contract +const contract = computing.contract(newContract.uuid); + +// Get details of the contract +const contractDetails = await contract.get(); + +// List transactions of the contract +const transactions = await contract.listTransactions(); + +// Encrypt a file and upload it to the associated bucket +const encryptionResult = await contract.encryptFile({ + fileName: 'example.txt', + content: Buffer.from('Hello, world!'), + nftId: 1, // NFT ID used for decryption authentication +}); + +// Transfer ownership of the contract +const newOwnerAddress = '0xNewOwnerAddress'; +const successResult = await contract.transferOwnership(newOwnerAddress); +console.log( + `Ownership transfer was ${successResult ? 'successful' : 'unsuccessful'}.`, +); +``` + +## Social + +The Social module provides functionalities for managing social hubs and channels within the Apillon platform. This includes creating, listing, and interacting with hubs and channels. In the background it utilizes Grill.chat, a mobile-friendly, anonymous chat application powered by Subsocial. + +### Usage example + +```ts +import { Social } from '@apillon/sdk'; + +const social = new Social({ key: 'yourApiKey', secret: 'yourApiSecret' }); +// Create a new hub +const hub = await social.createHub({ + name: 'Apillon Hub', + about: 'Hub for Apillon channels', + tags: 'apillon,web3,build', +}); + +// Get a specific hub by UUID +const hubDetails = await social.hub(hub.uuid).get(); +// List all Hubs +const hubs = await social.listHubs(); + +// Create a new channel within a hub +const channel = await social.createChannel({ + title: 'Web3 Channel', + body: "Let's discuss Web3", + tags: 'web3,crypto', + hubUuid: hub.uuid, +}); +// Get a specific channel by UUID +const channelDetails = await social.channel(channel.uuid).get(); +// List all channels within a Hub +const channels = await social.listChannels({ hubUuid: hub.uuid }); +``` \ No newline at end of file diff --git a/packages/sdk/package.json b/packages/sdk/package.json index e3f2bc2..eb7a6b3 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,7 +1,7 @@ { "name": "@apillon/sdk", "description": "▶◀ Apillon SDK for NodeJS ▶◀", - "version": "2.0.1", + "version": "3.0.0", "author": "Apillon", "license": "MIT", "main": "./dist/index.js", diff --git a/packages/sdk/src/docs-index.ts b/packages/sdk/src/docs-index.ts index 86d6f63..641a34e 100644 --- a/packages/sdk/src/docs-index.ts +++ b/packages/sdk/src/docs-index.ts @@ -3,6 +3,7 @@ export * from './types/nfts'; export * from './types/hosting'; export * from './types/storage'; export * from './types/identity'; +export * from './types/computing'; export * from './lib/apillon'; export * from './modules/storage/storage'; export * from './modules/storage/storage-bucket'; @@ -15,3 +16,8 @@ export * from './modules/hosting/hosting-website'; export * from './modules/nft/nft'; export * from './modules/nft/nft-collection'; export * from './modules/identity/identity'; +export * from './modules/computing/computing'; +export * from './modules/computing/computing-contract'; +export * from './modules/social/social'; +export * from './modules/social/social-hub'; +export * from './modules/social/social-channel'; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index fe1b1e0..0356f48 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -9,3 +9,5 @@ export * from './modules/storage/storage'; export * from './modules/hosting/hosting'; export * from './modules/nft/nft'; export * from './modules/identity/identity'; +export * from './modules/computing/computing'; +export * from './modules/social/social'; diff --git a/packages/sdk/src/lib/apillon-api.ts b/packages/sdk/src/lib/apillon-api.ts index 56001b2..5d4fb41 100644 --- a/packages/sdk/src/lib/apillon-api.ts +++ b/packages/sdk/src/lib/apillon-api.ts @@ -61,6 +61,8 @@ export class ApillonApi { } }, ); + + return config; } public static async get(url: string, config?: any): Promise { diff --git a/packages/sdk/src/lib/apillon.ts b/packages/sdk/src/lib/apillon.ts index cc78181..60fcd93 100644 --- a/packages/sdk/src/lib/apillon.ts +++ b/packages/sdk/src/lib/apillon.ts @@ -31,8 +31,10 @@ export interface ApillonConfig { } export class ApillonModule { + protected config: ApillonConfig; + public constructor(config?: ApillonConfig) { - ApillonApi.initialize(config); + this.config = ApillonApi.initialize(config); ApillonLogger.initialize( config?.debug ? LogLevel.VERBOSE : config?.logLevel || LogLevel.ERROR, ); @@ -71,8 +73,7 @@ export class ApillonModel { protected populate(data: object) { if (data != null) { Object.keys(data || {}).forEach((key) => { - const prop = this[key]; - if (prop === null) { + if (this[key] === null) { this[key] = data[key]; } }); diff --git a/packages/sdk/src/modules/computing/computing-contract.ts b/packages/sdk/src/modules/computing/computing-contract.ts new file mode 100644 index 0000000..4fe5167 --- /dev/null +++ b/packages/sdk/src/modules/computing/computing-contract.ts @@ -0,0 +1,201 @@ +import { ApillonApi } from '../../lib/apillon-api'; +import { ApillonConfig, ApillonModel } from '../../lib/apillon'; +import { + ComputingContractData, + ComputingContractStatus, + ComputingContractType, + ComputingTransactionType, + IComputingTransaction, + IEncryptData, + ITransactionListFilters, +} from '../../types/computing'; +import { IApillonList } from '../../types/apillon'; +import { constructUrlWithQueryParams } from '../../lib/common'; +import { ApillonLogger } from '../../lib/apillon-logger'; +import { Storage } from '../storage/storage'; +import { FileUploadResult } from '../../docs-index'; + +export class ComputingContract extends ApillonModel { + /** + * Name of the contract. + */ + public name: string = null; + + /** + * Contract description. + */ + public description: string = null; + + /** + * The bucket where files encrypted by this contract are stored + */ + public bucketUuid: string = null; + + /** + * The computing contract's type + */ + public contractType: ComputingContractType = null; + + /** + * The computing contract's status + */ + public contractStatus: ComputingContractStatus = null; + + /** + * The computing contract's on-chain address + */ + public contractAddress: string = null; + + /** + * The computing contract's on-chain deployer address + */ + public deployerAddress: string = null; + + /** + * The computing contract's deployment transaction hash + */ + public transactionHash: string = null; + + /** + * The computing contract's additional data + */ + public data: ComputingContractData = null; + + /** + * Apillon config used to initialize a storage module + * for saving encrypted files + */ + private config: ApillonConfig; + + /** + * Constructor which should only be called via Computing class. + * @param uuid Unique identifier of the contract. + * @param data Data to populate computing contract with. + */ + constructor( + uuid: string, + data?: Partial, + config?: ApillonConfig, + ) { + super(uuid); + this.API_PREFIX = `/computing/contracts/${uuid}`; + this.populate(data); + this.config = config; + } + + /** + * Gets a computing contract's details. + * @returns ComputingContract instance + */ + async get(): Promise { + const data = await ApillonApi.get(this.API_PREFIX); + return this.populate(data); + } + + /** + * Gets list of transactions for this computing contract. + * @param {ITransactionListFilters} params Query filters. + * @returns {IComputingTransaction[]} List of transactions. + */ + public async listTransactions( + params?: ITransactionListFilters, + ): Promise> { + const url = constructUrlWithQueryParams( + `${this.API_PREFIX}/transactions`, + params, + ); + return await ApillonApi.get>(url); + } + + /** + * Transfers ownership of the computing contract. + * @param {string} accountAddress The address of the new owner. + * @returns Success status + */ + async transferOwnership(accountAddress: string): Promise { + const { success } = await ApillonApi.post<{ success: boolean }>( + `${this.API_PREFIX}/transfer-ownership`, + { accountAddress }, + ); + if (success) { + ApillonLogger.log( + `Ownership transferred successfully to ${accountAddress}`, + ); + } + return success; + } + + /** + * - Calls the encrypt method on the computing contract + * - Uploads the encrypted file to the bucket + * - Assigns the encrypted file's CID to the NFT used for decryption authentication + * @param {IEncryptData} data The data to use for encryption. + * @returns The uploaded encrypted file metadata + */ + async encryptFile(data: IEncryptData): Promise { + ApillonLogger.log(`Encrypting file...`); + const { encryptedContent } = await ApillonApi.post( + `${this.API_PREFIX}/encrypt`, + { + ...data, + content: data.content.toString('base64'), + }, + ); + if (!encryptedContent) { + throw new Error('Failed to encrypt file'); + } + if (!this.bucketUuid) { + await this.get(); + } + + ApillonLogger.log(`Uploading encrypted file to bucket...`); + const files = await new Storage(this.config) + .bucket(this.bucketUuid) + .uploadFiles( + [ + { + fileName: data.fileName, + content: Buffer.from(encryptedContent, 'utf-8'), + contentType: 'multipart/form-data', + }, + ], + { awaitCid: true }, + ); + ApillonLogger.log(`Assigning file CID to NFT ID...`); + await this.assignCidToNft({ cid: files[0].CID, nftId: data.nftId }); + + return files; + } + + /** + * Assigns a CID to an NFT on the contract. + * @param data The payload for assigning a CID to an NFT + * @returns Success status + */ + private async assignCidToNft(data: { + cid: string; + nftId: number; + }): Promise { + const { success } = await ApillonApi.post<{ success: boolean }>( + `${this.API_PREFIX}/assign-cid-to-nft`, + data, + ); + if (success) { + ApillonLogger.log( + `Encrypted file CID assigned successfully to NFT with ID=${data.nftId}`, + ); + } + return success; + } + + protected override serializeFilter(key: string, value: any) { + const serialized = super.serializeFilter(key, value); + const enums = { + contractType: ComputingContractType[serialized], + contractStatus: ComputingContractStatus[serialized], + transactionType: ComputingTransactionType[serialized], + transactionStatus: ComputingContractStatus[serialized], + }; + return Object.keys(enums).includes(key) ? enums[key] : serialized; + } +} diff --git a/packages/sdk/src/modules/computing/computing.ts b/packages/sdk/src/modules/computing/computing.ts new file mode 100644 index 0000000..391bfde --- /dev/null +++ b/packages/sdk/src/modules/computing/computing.ts @@ -0,0 +1,64 @@ +import { ApillonModule } from '../../lib/apillon'; +import { ApillonApi } from '../../lib/apillon-api'; +import { constructUrlWithQueryParams } from '../../lib/common'; +import { IApillonList } from '../../types/apillon'; +import { + IContractListFilters, + ICreateComputingContract, +} from '../../types/computing'; +import { ComputingContract } from './computing-contract'; + +export class Computing extends ApillonModule { + /** + * API url for computing. + */ + private API_PREFIX = '/computing/contracts'; + + /** + * Lists all computing contracts. + * @param {IContractListFilters} params Filter for listing collections. + * @returns Array of ComputingContract objects. + */ + public async listContracts( + params?: IContractListFilters, + ): Promise> { + const url = constructUrlWithQueryParams(this.API_PREFIX, params); + + const data = await ApillonApi.get< + IApillonList + >(url); + + return { + ...data, + items: data.items.map( + (contract) => + new ComputingContract(contract.contractUuid, contract, this.config), + ), + }; + } + + /** + * Creates a new computing contract based on the provided data. + * @param {ICreateComputingContract} data Data for creating the contract. + * @returns {ComputingContract} Newly created computing contract. + */ + public async createContract( + data: ICreateComputingContract, + ): Promise { + const contract = await ApillonApi.post< + ComputingContract & { contractUuid: string } + >(this.API_PREFIX, { + ...data, + contractType: 1, // Hardcoded until new type is added + }); + return new ComputingContract(contract.contractUuid, contract, this.config); + } + + /** + * @param uuid Unique contract identifier. + * @returns An instance of ComputingContract. + */ + public contract(uuid: string): ComputingContract { + return new ComputingContract(uuid, null, this.config); + } +} diff --git a/packages/sdk/src/modules/hosting/hosting-website.ts b/packages/sdk/src/modules/hosting/hosting-website.ts index 34b8839..d41933d 100644 --- a/packages/sdk/src/modules/hosting/hosting-website.ts +++ b/packages/sdk/src/modules/hosting/hosting-website.ts @@ -92,7 +92,7 @@ export class HostingWebsite extends ApillonModel { folderPath: string, params?: IFileUploadRequest, ): Promise { - await uploadFiles(folderPath, this.API_PREFIX, params); + await uploadFiles({ apiPrefix: this.API_PREFIX, folderPath, params }); } /** @@ -104,7 +104,7 @@ export class HostingWebsite extends ApillonModel { files: FileMetadata[], params?: IFileUploadRequest, ): Promise { - await uploadFiles(null, this.API_PREFIX, params, files); + await uploadFiles({ apiPrefix: this.API_PREFIX, params, files }); } /** diff --git a/packages/sdk/src/modules/identity/identity.ts b/packages/sdk/src/modules/identity/identity.ts index 75563d9..deaad33 100644 --- a/packages/sdk/src/modules/identity/identity.ts +++ b/packages/sdk/src/modules/identity/identity.ts @@ -101,10 +101,7 @@ export class Identity extends ApillonModule { */ public validatePolkadotWalletSignature( data: IValidatePolkadotWalletSignature, - ): { - isValid: boolean; - address: string; - } { + ): VerifySignedMessageResult { const { message, signature, walletAddress, timestamp } = data; const signingMessage = diff --git a/packages/sdk/src/modules/nft/nft-collection.ts b/packages/sdk/src/modules/nft/nft-collection.ts index d6633f6..5c3d716 100644 --- a/packages/sdk/src/modules/nft/nft-collection.ts +++ b/packages/sdk/src/modules/nft/nft-collection.ts @@ -1,6 +1,7 @@ import { IMintNftData, INftActionResponse, + SubstrateChain, TransactionStatus, } from './../../types/nfts'; import { ApillonApi } from '../../lib/apillon-api'; @@ -8,7 +9,6 @@ import { ApillonLogger } from '../../lib/apillon-logger'; import { constructUrlWithQueryParams } from '../../lib/common'; import { IApillonList } from '../../types/apillon'; import { - ICollection, ITransactionFilters, ITransaction, CollectionType, @@ -125,7 +125,7 @@ export class NftCollection extends ApillonModel { /** * Chain on which the smart contract was deployed. */ - public chain: EvmChain = null; + public chain: EvmChain | SubstrateChain = null; /** * Constructor which should only be called via Nft class. @@ -143,7 +143,7 @@ export class NftCollection extends ApillonModel { * @returns Collection instance. */ public async get(): Promise { - const data = await ApillonApi.get(this.API_PREFIX); + const data = await ApillonApi.get(this.API_PREFIX); return this.populate(data); } @@ -200,7 +200,7 @@ export class NftCollection extends ApillonModel { * Burns a nft. * @warn Can only burn NFTs if the collection is revokable. * @param tokenId Token ID of the NFT we want to burn. - * @returns Status. + * @returns Success status and transaction hash. */ public async burn(tokenId: string): Promise { if (this.isRevokable != null && !this.isRevokable) { @@ -223,7 +223,7 @@ export class NftCollection extends ApillonModel { * @returns Collection data. */ public async transferOwnership(address: string): Promise { - const data = await ApillonApi.post( + const data = await ApillonApi.post( `${this.API_PREFIX}/transfer`, { address }, ); @@ -237,7 +237,7 @@ export class NftCollection extends ApillonModel { /** * Gets list of transactions that occurred on this collection through Apillon. * @param params Filters. - * @returns List of transactions. + * @returns {ITransaction[]} List of transactions. */ public async listTransactions( params?: ITransactionFilters, @@ -247,14 +247,7 @@ export class NftCollection extends ApillonModel { params, ); - const data = await ApillonApi.get>(url); - - return { - ...data, - items: data.items.map((t) => - JSON.parse(JSON.stringify(t, this.serializeFilter)), - ), - }; + return await ApillonApi.get>(url); } protected override serializeFilter(key: string, value: any) { @@ -264,8 +257,8 @@ export class NftCollection extends ApillonModel { collectionStatus: CollectionStatus[serialized], transactionType: TransactionType[serialized], transactionStatus: TransactionStatus[serialized], - chain: EvmChain[serialized], - chainId: EvmChain[serialized], + chain: EvmChain[serialized] || SubstrateChain[serialized], + chainId: EvmChain[serialized] || SubstrateChain[serialized], }; return Object.keys(enums).includes(key) ? enums[key] : serialized; } diff --git a/packages/sdk/src/modules/nft/nft.ts b/packages/sdk/src/modules/nft/nft.ts index ef98425..3cd3448 100644 --- a/packages/sdk/src/modules/nft/nft.ts +++ b/packages/sdk/src/modules/nft/nft.ts @@ -4,8 +4,9 @@ import { constructUrlWithQueryParams } from '../../lib/common'; import { IApillonList } from '../../types/apillon'; import { ICollectionFilters, - ICollection, ICreateCollection, + ICreateSubstrateCollection, + ICreateCollectionBase, } from '../../types/nfts'; import { NftCollection } from './nft-collection'; @@ -33,7 +34,9 @@ export class Nft extends ApillonModule { ): Promise> { const url = constructUrlWithQueryParams(this.API_PREFIX, params); - const data = await ApillonApi.get>(url); + const data = await ApillonApi.get< + IApillonList + >(url); return { ...data, @@ -44,17 +47,31 @@ export class Nft extends ApillonModule { } /** - * Deploys a new NftCollection smart contract. + * Deploys a new EVM NftCollection smart contract. * @param data NFT collection data. * @returns A NftCollection instance. */ public async create(data: ICreateCollection) { + return await this.createNft(data, true); + } + + /** + * Deploys a new Substrate NftCollection smart contract. + * @param data NFT collection data. + * @returns A NftCollection instance. + */ + public async createSubstrate(data: ICreateSubstrateCollection) { + return await this.createNft(data, false); + } + + private async createNft(data: ICreateCollectionBase, isEvm: boolean) { // If not drop, set drop properties to default 0 if (!data.drop) { data.dropStart = data.dropPrice = data.dropReserve = 0; } - const response = await ApillonApi.post(this.API_PREFIX, data); - + const response = await ApillonApi.post< + NftCollection & { collectionUuid: string } + >(`${this.API_PREFIX}/${isEvm ? 'evm' : 'substrate'}`, data); return new NftCollection(response.collectionUuid, response); } } diff --git a/packages/sdk/src/modules/social/social-channel.ts b/packages/sdk/src/modules/social/social-channel.ts new file mode 100644 index 0000000..75d3496 --- /dev/null +++ b/packages/sdk/src/modules/social/social-channel.ts @@ -0,0 +1,45 @@ +import { ApillonApi } from '../../lib/apillon-api'; +import { ApillonModel } from '../../lib/apillon'; +import { HubStatus } from '../../types/social'; + +export class SocialChannel extends ApillonModel { + /** + * Channel ID on Subsocial chain. This ID is used in widget. + */ + public channelId: number = null; + + /** + * Channel status (1: draft - deploying to chain, 5: active, 100: error). + */ + public status: HubStatus = null; + + /** + * Name of the channel. + */ + public title: string = null; + + /** + * Short description or content of the channel. + */ + public body: string = null; + + /** + * Comma separated tags associated with the channel. + */ + public tags: string = null; + + constructor(uuid: string, data?: Partial) { + super(uuid); + this.API_PREFIX = `/social/channels/${uuid}`; + this.populate(data); + } + + /** + * Fetches and populates the channel details from the API. + * @returns An instance of Channel class with filled properties. + */ + public async get(): Promise { + const data = await ApillonApi.get(this.API_PREFIX); + return this.populate(data); + } +} diff --git a/packages/sdk/src/modules/social/social-hub.ts b/packages/sdk/src/modules/social/social-hub.ts new file mode 100644 index 0000000..f40d3b2 --- /dev/null +++ b/packages/sdk/src/modules/social/social-hub.ts @@ -0,0 +1,51 @@ +import { ApillonApi } from '../../lib/apillon-api'; +import { ApillonModel } from '../../lib/apillon'; +import { HubStatus } from '../../types/social'; + +export class SocialHub extends ApillonModel { + /** + * Hub ID on Subsocial chain. + * @example https://grillapp.net/12927 + */ + public hubId: number = null; + + /** + * Hub status (1: draft - deploying to chain, 5: active, 100: error). + */ + public status: HubStatus = null; + + /** + * Name of the hub. + */ + public name: string = null; + + /** + * Short description about the hub. + */ + public about: string = null; + + /** + * Comma separated tags associated with the hub. + */ + public tags: string = null; + + /** + * Number of channels in the hub. + */ + public numOfChannels: number = null; + + constructor(uuid: string, data?: Partial) { + super(uuid); + this.API_PREFIX = `/social/hubs/${uuid}`; + this.populate(data); + } + + /** + * Fetches and populates the hub details from the API. + * @returns An instance of Hub class with filled properties. + */ + public async get(): Promise { + const data = await ApillonApi.get(this.API_PREFIX); + return this.populate(data); + } +} diff --git a/packages/sdk/src/modules/social/social.ts b/packages/sdk/src/modules/social/social.ts new file mode 100644 index 0000000..8a19c47 --- /dev/null +++ b/packages/sdk/src/modules/social/social.ts @@ -0,0 +1,98 @@ +import { ApillonModule } from '../../lib/apillon'; +import { ApillonApi } from '../../lib/apillon-api'; +import { constructUrlWithQueryParams } from '../../lib/common'; +import { IApillonList, IApillonPagination } from '../../types/apillon'; +import { + ICreateHub, + ICreateChannel, + IChannelFilters, +} from '../../types/social'; // Assume these types are defined similarly to the NFT types +import { SocialChannel } from './social-channel'; +import { SocialHub } from './social-hub'; + +export class Social extends ApillonModule { + private HUBS_API_PREFIX = '/social/hubs'; + private CHANNELS_API_PREFIX = '/social/channels'; + + /** + * Lists all hubs with optional filters. + * @param {IApillonPagination} params Optional filters for listing hubs. + * @returns A list of Hub instances. + */ + public async listHubs( + params?: IApillonPagination, + ): Promise> { + const data = await ApillonApi.get< + IApillonList + >(constructUrlWithQueryParams(this.HUBS_API_PREFIX, params)); + + return { + ...data, + items: data.items.map((hub) => new SocialHub(hub.hubUuid, hub)), + }; + } + + /** + * Lists all channels with optional filters. + * @param {IChannelFilters} params Optional filters for listing channels. + * @returns A list of Channel instances. + */ + public async listChannels( + params?: IChannelFilters, + ): Promise> { + const url = constructUrlWithQueryParams(this.CHANNELS_API_PREFIX, params); + const data = await ApillonApi.get< + IApillonList + >(url); + + return { + ...data, + items: data.items.map( + (channel) => new SocialChannel(channel.channelUuid, channel), + ), + }; + } + + /** + * Creates a new hub. + * @param {ICreateHub} hubData Data for creating the hub. + * @returns The created Hub instance. + */ + public async createHub(hubData: ICreateHub): Promise { + const hub = await ApillonApi.post( + this.HUBS_API_PREFIX, + hubData, + ); + return new SocialHub(hub.hubUuid, hub); + } + + /** + * Creates a new channel. + * @param {ICreateChannel} channelData Data for creating the channel. + * @returns The created Channel instance. + */ + public async createChannel( + channelData: ICreateChannel, + ): Promise { + const channel = await ApillonApi.post< + SocialChannel & { channelUuid: string } + >(this.CHANNELS_API_PREFIX, channelData); + return new SocialChannel(channel.channelUuid, channel); + } + + /** + * @param uuid Unique hub identifier. + * @returns An instance of Hub. + */ + public hub(uuid: string): SocialHub { + return new SocialHub(uuid); + } + + /** + * @param uuid Unique channel identifier. + * @returns An instance of SocialChannel. + */ + public channel(uuid: string): SocialChannel { + return new SocialChannel(uuid); + } +} diff --git a/packages/sdk/src/modules/storage/directory.ts b/packages/sdk/src/modules/storage/directory.ts index ea30755..cc4f9b3 100644 --- a/packages/sdk/src/modules/storage/directory.ts +++ b/packages/sdk/src/modules/storage/directory.ts @@ -21,7 +21,7 @@ export class Directory extends ApillonModel { public name: string = null; /** - * Directory unique ipfs identifier. + * Directory unique IPFS content identifier. */ public CID: string = null; diff --git a/packages/sdk/src/modules/storage/file.ts b/packages/sdk/src/modules/storage/file.ts index 117e19d..1212676 100644 --- a/packages/sdk/src/modules/storage/file.ts +++ b/packages/sdk/src/modules/storage/file.ts @@ -21,15 +21,10 @@ export class File extends ApillonModel { public name: string = null; /** - * File unique ipfs identifier. + * File unique IPFS content identifier. */ public CID: string = null; - /** - * File content identifier V1. - */ - public CIDv1: string = null; - /** * File upload status. */ diff --git a/packages/sdk/src/modules/storage/storage-bucket.ts b/packages/sdk/src/modules/storage/storage-bucket.ts index b0e1223..aaeb176 100644 --- a/packages/sdk/src/modules/storage/storage-bucket.ts +++ b/packages/sdk/src/modules/storage/storage-bucket.ts @@ -1,5 +1,6 @@ import { Directory } from './directory'; import { + BucketType, FileMetadata, FileUploadResult, IBucketFilesRequest, @@ -34,6 +35,11 @@ export class StorageBucket extends ApillonModel { */ public size: number = null; + /** + * Type of bucket (storage, hosting or NFT metadata) + */ + public bucketType: number = null; + /** * Bucket content which are files and directories. */ @@ -55,10 +61,8 @@ export class StorageBucket extends ApillonModel { * @returns Bucket instance */ async get(): Promise { - const data = await ApillonApi.get( - this.API_PREFIX, - ); - return new StorageBucket(data.bucketUuid, data); + const data = await ApillonApi.get(this.API_PREFIX); + return this.populate(data); } /** @@ -123,11 +127,11 @@ export class StorageBucket extends ApillonModel { folderPath: string, params?: IFileUploadRequest, ): Promise { - const { files: uploadedFiles, sessionUuid } = await uploadFiles( + const { files: uploadedFiles, sessionUuid } = await uploadFiles({ + apiPrefix: this.API_PREFIX, folderPath, - this.API_PREFIX, params, - ); + }); if (!params?.awaitCid) { return this.getUploadedFiles(sessionUuid, uploadedFiles.length); @@ -145,12 +149,11 @@ export class StorageBucket extends ApillonModel { files: FileMetadata[], params?: IFileUploadRequest, ): Promise { - const { files: uploadedFiles, sessionUuid } = await uploadFiles( - null, - this.API_PREFIX, + const { files: uploadedFiles, sessionUuid } = await uploadFiles({ + apiPrefix: this.API_PREFIX, params, files, - ); + }); if (!params?.awaitCid) { return this.getUploadedFiles(sessionUuid, uploadedFiles.length); @@ -190,7 +193,7 @@ export class StorageBucket extends ApillonModel { resolvedFiles = await this.getUploadedFiles(sessionUuid, limit); await new Promise((resolve) => setTimeout(resolve, 1000)); - if (++retryTimes >= 15) { + if (++retryTimes >= 30) { ApillonLogger.log('Unable to resolve file CIDs', LogLevel.ERROR); return resolvedFiles; } @@ -249,4 +252,12 @@ export class StorageBucket extends ApillonModel { return new Ipns(this.uuid, data.ipnsUuid, data); } //#endregion + + protected override serializeFilter(key: string, value: any) { + const serialized = super.serializeFilter(key, value); + const enums = { + bucketType: BucketType[value], + }; + return Object.keys(enums).includes(key) ? enums[key] : serialized; + } } diff --git a/packages/sdk/src/modules/storage/storage.ts b/packages/sdk/src/modules/storage/storage.ts index 5d7b7c3..73d0bd2 100644 --- a/packages/sdk/src/modules/storage/storage.ts +++ b/packages/sdk/src/modules/storage/storage.ts @@ -12,7 +12,7 @@ export class Storage extends ApillonModule { /** * Lists all buckets. - * @param {ICollectionFilters} params Filter for listing collections. + * @param {IApillonPagination} params Filter for listing collections. * @returns Array of StorageBucket objects. */ public async listBuckets( diff --git a/packages/sdk/src/tests/computing.test.ts b/packages/sdk/src/tests/computing.test.ts new file mode 100644 index 0000000..640b2be --- /dev/null +++ b/packages/sdk/src/tests/computing.test.ts @@ -0,0 +1,143 @@ +import { ChainRpcUrl } from '../docs-index'; +import { Computing } from '../modules/computing/computing'; +import { ComputingContract } from '../modules/computing/computing-contract'; +import { + ComputingContractStatus, + ComputingTransactionType, +} from '../types/computing'; +import { + getBucketUUID, + getComputingContractUUID, + getConfig, + getPhalaAddress, +} from './helpers/helper'; +import { resolve } from 'path'; +import * as fs from 'fs'; + +describe('Computing tests', () => { + let computing: Computing; + let contractUuid: string; + let receivingAddress: string; + let bucket_uuid: string; + + const name = 'Schrodinger SDK Test'; + const description = 'Schrodinger SDK Test computing contract'; + const nftContractAddress = '0xe6C61ef02729a190Bd940A3077f8464c27C2E593'; + + beforeAll(() => { + computing = new Computing(getConfig()); + contractUuid = getComputingContractUUID(); + receivingAddress = getPhalaAddress(); + bucket_uuid = getBucketUUID(); + }); + + test('Create new contract', async () => { + const contract = await computing.createContract({ + name, + description, + bucket_uuid, + contractData: { + nftContractAddress, + nftChainRpcUrl: ChainRpcUrl.MOONBASE, + }, + }); + expect(contract).toBeInstanceOf(ComputingContract); + expect(contract.name).toEqual(name); + expect(contract.description).toEqual(description); + expect(contract.uuid).toBeTruthy(); + expect(contract.bucketUuid).toEqual(bucket_uuid); + expect(contract.data.nftContractAddress).toEqual(nftContractAddress); + expect(contract.data.nftChainRpcUrl).toEqual(ChainRpcUrl.MOONBASE); + + contractUuid = contract.uuid; + }); + + test('Creating new contract with missing contract data should fail', async () => { + const createContract = () => + computing.createContract({ + name, + description, + bucket_uuid, + contractData: { + nftContractAddress: undefined, + nftChainRpcUrl: undefined, + }, + }); + await expect(createContract).rejects.toThrow(); + }); + + test('List contracts', async () => { + const { items } = await computing.listContracts(); + + expect(items.length).toBeGreaterThanOrEqual(0); + items.forEach((contract) => { + expect(contract instanceof ComputingContract).toBeTruthy(); + expect(contract.name).toBeTruthy(); + }); + expect( + items.find((contract) => contract.uuid === contractUuid), + ).toBeTruthy(); + }); + + test('Get specific contract', async () => { + const contract = await computing.contract(contractUuid).get(); + + expect(contract).toBeInstanceOf(ComputingContract); + expect(contract.name).toEqual(name); + expect(contract.description).toEqual(description); + expect(contract.uuid).toEqual(contractUuid); + expect(contract.data.nftContractAddress).toEqual(nftContractAddress); + expect(contract.data.nftChainRpcUrl).toEqual(ChainRpcUrl.MOONBASE); + }); + + test('List all transactions for computing contract', async () => { + const { items } = await computing + .contract(contractUuid) + .listTransactions({ limit: 10 }); + expect(items.length).toBeGreaterThanOrEqual(0); + items.forEach((transaction) => { + expect(transaction).toBeDefined(); + expect(transaction.transactionHash).toBeDefined(); + expect( + Object.keys(ComputingContractStatus).includes( + transaction.transactionStatus.toString(), + ), + ); + expect( + Object.keys(ComputingTransactionType).includes( + transaction.transactionType.toString(), + ), + ); + }); + }); + + test('List all transactions with specific type', async () => { + const { items } = await computing.contract(contractUuid).listTransactions({ + transactionType: ComputingTransactionType.DEPLOY_CONTRACT, + }); + expect(items.length).toEqual(1); + expect(items[0].transactionType).toEqual( + ComputingTransactionType.DEPLOY_CONTRACT, + ); + }); + + test.skip('Encrypt data using computing contract', async () => { + const html = fs.readFileSync( + resolve(__dirname, './helpers/website/style.css'), + ); + const files = await computing + .contract(contractUuid) + .encryptFile({ content: html, fileName: 'style.css', nftId: 5 }); + + expect(files).toHaveLength(1); + expect(files[0].fileName).toBe('style.css'); + expect(files[0].CID).toBeDefined(); + }); + + test.skip('Transfer ownership of computing contract', async () => { + const success = await computing + .contract(contractUuid) + .transferOwnership(receivingAddress); + expect(success).toBeTruthy(); + }); +}); diff --git a/packages/sdk/src/tests/helpers/helper.ts b/packages/sdk/src/tests/helpers/helper.ts index 087a7a2..f8c2822 100644 --- a/packages/sdk/src/tests/helpers/helper.ts +++ b/packages/sdk/src/tests/helpers/helper.ts @@ -30,3 +30,19 @@ export function getWebsiteUUID() { export function getMintAddress() { return process.env['MINT_ADDRESS']; } + +export function getPhalaAddress() { + return process.env['PHALA_ADDRESS']; +} + +export function getComputingContractUUID() { + return process.env['COMPUTING_CONTRACT_UUID']; +} + +export function getDirectoryUUID() { + return process.env['DIRECTORY_UUID']; +} + +export function getFileUUID() { + return process.env['FILE_UUID']; +} diff --git a/packages/sdk/src/tests/hosting.test.ts b/packages/sdk/src/tests/hosting.test.ts index f03a53e..1508b6d 100644 --- a/packages/sdk/src/tests/hosting.test.ts +++ b/packages/sdk/src/tests/hosting.test.ts @@ -33,7 +33,7 @@ describe('Hosting tests', () => { const website = hosting.website(websiteUuid); const uploadDir = resolve(__dirname, './helpers/website/'); - await website.uploadFromFolder(uploadDir); + await website.uploadFromFolder(uploadDir, { ignoreFiles: false }); const deployment = await website.deploy(DeployToEnvironment.TO_STAGING); expect(deployment.environment).toEqual(DeployToEnvironment.TO_STAGING); deploymentUuid = deployment.uuid; @@ -42,32 +42,27 @@ describe('Hosting tests', () => { expect(website.lastDeploymentStatus).toEqual(DeploymentStatus.INITIATED); }); - test.skip('upload files from buffer', async () => { + test('upload files from buffer', async () => { const html = fs.readFileSync( resolve(__dirname, './helpers/website/index.html'), ); const css = fs.readFileSync( resolve(__dirname, './helpers/website/style.css'), ); - try { - console.time('File upload complete'); - await hosting.website(websiteUuid).uploadFiles([ - { - fileName: 'index.html', - contentType: 'text/html', - content: html, - }, - { - fileName: 'style.css', - contentType: 'text/css', - content: css, - }, - ]); - console.timeEnd('File upload complete'); - // console.log(content); - } catch (e) { - console.log(e); - } + console.time('File upload complete'); + await hosting.website(websiteUuid).uploadFiles([ + { + fileName: 'index.html', + contentType: 'text/html', + content: html, + }, + { + fileName: 'style.css', + contentType: 'text/css', + content: css, + }, + ]); + console.timeEnd('File upload complete'); }); test('list all deployments', async () => { diff --git a/packages/sdk/src/tests/nft.test.ts b/packages/sdk/src/tests/nft.test.ts index c6936ad..5b04d60 100644 --- a/packages/sdk/src/tests/nft.test.ts +++ b/packages/sdk/src/tests/nft.test.ts @@ -1,21 +1,17 @@ import { Nft } from '../modules/nft/nft'; import { NftCollection } from '../modules/nft/nft-collection'; -import { CollectionType, EvmChain } from '../types/nfts'; +import { CollectionType, EvmChain, SubstrateChain } from '../types/nfts'; import { getCollectionUUID, getConfig, getMintAddress } from './helpers/helper'; const nftData = { - chain: EvmChain.MOONBASE, collectionType: CollectionType.GENERIC, name: 'SDK Test', description: 'Created from SDK tests', symbol: 'SDKT', royaltiesFees: 0, - royaltiesAddress: '0x0000000000000000000000000000000000000000', baseUri: 'https://test.com/metadata/', baseExtension: '.json', maxSupply: 5, - isRevokable: false, - isSoulbound: false, drop: false, }; @@ -37,17 +33,40 @@ describe('Nft tests', () => { }); test('creates a new collection', async () => { - const collection = await nft.create(nftData); + const collection = await nft.create({ + ...nftData, + chain: EvmChain.MOONBASE, + isRevokable: true, + isSoulbound: true, + }); expect(collection.uuid).toBeDefined(); expect(collection.contractAddress).toBeDefined(); expect(collection.symbol).toEqual('SDKT'); expect(collection.name).toEqual('SDK Test'); expect(collection.description).toEqual('Created from SDK tests'); expect(collection.isAutoIncrement).toEqual(true); + expect(collection.isRevokable).toEqual(true); + expect(collection.isSoulbound).toEqual(true); collectionUuid = collection.uuid; }); + test('creates a new substrate collection', async () => { + const collection = await nft.createSubstrate({ + ...nftData, + chain: SubstrateChain.ASTAR, + royaltiesAddress: 'b3k5JvUnYjdZrCCNkf15PFpqChMunu11aeRoLropayUmhR4', + }); + expect(collection.uuid).toBeDefined(); + expect(collection.contractAddress).toBeDefined(); + expect(collection.symbol).toEqual('SDKT'); + expect(collection.name).toEqual('SDK Test'); + expect(collection.description).toEqual('Created from SDK tests'); + expect(collection.isAutoIncrement).toEqual(true); + expect(collection.isRevokable).toEqual(false); + expect(collection.isSoulbound).toEqual(false); + }); + test('mints a new nft', async () => { const collection = nft.collection(collectionUuid); const res = await collection.mint({ @@ -98,7 +117,10 @@ describe('Nft tests', () => { const collection = await nft.create({ ...nftData, name: 'SDK Test isAutoIncrement=false', + chain: EvmChain.MOONBASE, isAutoIncrement: false, + isRevokable: false, + isSoulbound: false, }); expect(collection.uuid).toBeDefined(); expect(collection.contractAddress).toBeDefined(); diff --git a/packages/sdk/src/tests/social.test.ts b/packages/sdk/src/tests/social.test.ts new file mode 100644 index 0000000..ffba1d4 --- /dev/null +++ b/packages/sdk/src/tests/social.test.ts @@ -0,0 +1,74 @@ +import { Social } from '../modules/social/social'; +import { getConfig } from './helpers/helper'; + +describe('Social tests', () => { + let social: Social; + let hubUuid: string; + let channelUuid: string; + + const hubData = { + name: 'Test Hub', + about: 'This is a test hub', + tags: 'sdk,test,hub', + }; + const channelData = { + title: 'Test Channel', + body: 'This is a test channel', + tags: 'sdk,test,channel', + }; + + beforeAll(async () => { + social = new Social(getConfig()); + }); + + test('Create a new Hub', async () => { + const hub = await social.createHub(hubData); + expect(hub.uuid).toBeDefined(); + expect(hub.name).toEqual(hubData.name); + expect(hub.about).toEqual(hubData.about); + expect(hub.tags).toEqual(hubData.tags); + hubUuid = hub.uuid; + }); + + test('Get Hub', async () => { + const hub = await social.hub(hubUuid).get(); + expect(hub.uuid).toEqual(hubUuid); + expect(hub.name).toEqual(hubData.name); + expect(hub.about).toEqual(hubData.about); + expect(hub.tags).toEqual(hubData.tags); + }); + + test('List Hubs', async () => { + const response = await social.listHubs(); + expect(response.items.length).toBeGreaterThanOrEqual(1); + expect(response.items.every((h) => !!h.uuid)); + expect(response.items.some((h) => h.uuid === hubUuid)); + }); + + test('Create a new Channel', async () => { + const channel = await social.createChannel({ + ...channelData, + // hubUuid, + }); + expect(channel.uuid).toBeDefined(); + expect(channel.title).toEqual(channelData.title); + expect(channel.body).toEqual(channelData.body); + expect(channel.tags).toEqual(channelData.tags); + channelUuid = channel.uuid; + }); + + test('Get Channel', async () => { + const channel = await social.channel(channelUuid).get(); + expect(channel.uuid).toEqual(channelUuid); + expect(channel.title).toEqual(channelData.title); + expect(channel.body).toEqual(channelData.body); + expect(channel.tags).toEqual(channelData.tags); + }); + + test('List Channels', async () => { + const response = await social.listChannels(); + expect(response.items.length).toBeGreaterThanOrEqual(1); + expect(response.items.every((c) => !!c.uuid)); + expect(response.items.some((c) => c.uuid === channelUuid)); + }); +}); diff --git a/packages/sdk/src/tests/storage.test.ts b/packages/sdk/src/tests/storage.test.ts index 1e5c011..86dd9fc 100644 --- a/packages/sdk/src/tests/storage.test.ts +++ b/packages/sdk/src/tests/storage.test.ts @@ -1,19 +1,26 @@ import { resolve } from 'path'; import { Storage } from '../modules/storage/storage'; import { StorageContentType } from '../types/storage'; -import { getBucketUUID, getConfig } from './helpers/helper'; +import { + getBucketUUID, + getConfig, + getDirectoryUUID, + getFileUUID, +} from './helpers/helper'; import * as fs from 'fs'; describe('Storage tests', () => { let storage: Storage; let bucketUuid: string; // For get and delete tests - const directoryUuid = '6c9c6ab1-801d-4915-a63e-120eed21fee0'; - const fileUuid = 'cf6a0d3d-2abd-4a0d-85c1-10b8f04cd4fc'; + let directoryUuid: string; + let fileUuid: string; beforeAll(async () => { storage = new Storage(getConfig()); bucketUuid = getBucketUUID(); + directoryUuid = getDirectoryUUID(); + fileUuid = getFileUUID(); }); test('List buckets', async () => { @@ -77,21 +84,13 @@ describe('Storage tests', () => { }); test('upload files from folder', async () => { - try { - const uploadDir = resolve(__dirname, './helpers/website/'); - - console.time('File upload complete'); - const files = await storage - .bucket(bucketUuid) - .uploadFromFolder(uploadDir); - console.timeEnd('File upload complete'); + const uploadDir = resolve(__dirname, './helpers/website/'); - expect(files.every((f) => !!f.fileUuid)).toBeTruthy(); + console.time('File upload complete'); + const files = await storage.bucket(bucketUuid).uploadFromFolder(uploadDir); + console.timeEnd('File upload complete'); - // console.log(content); - } catch (e) { - console.log(e); - } + expect(files.every((f) => !!f.fileUuid)).toBeTruthy(); }); test('upload files from folder with awaitCid', async () => { @@ -120,21 +119,15 @@ describe('Storage tests', () => { }); test('upload files from buffer', async () => { - const html = fs.readFileSync( - resolve(__dirname, './helpers/website/index.html'), - ); + // const html = fs.readFileSync( + // resolve(__dirname, './helpers/website/index.html'), + // ); const css = fs.readFileSync( resolve(__dirname, './helpers/website/style.css'), ); console.time('File upload complete'); await storage.bucket(bucketUuid).uploadFiles( [ - { - fileName: 'index.html', - contentType: 'text/html', - path: null, - content: html, - }, { fileName: 'style.css', contentType: 'text/css', diff --git a/packages/sdk/src/types/apillon.ts b/packages/sdk/src/types/apillon.ts index d9c3c8d..d944234 100644 --- a/packages/sdk/src/types/apillon.ts +++ b/packages/sdk/src/types/apillon.ts @@ -9,6 +9,7 @@ export interface IApillonPagination { limit?: number; orderBy?: string; desc?: boolean; + status?: number; } export enum LogLevel { @@ -16,3 +17,9 @@ export enum LogLevel { ERROR = 2, VERBOSE = 3, } + +export enum ChainRpcUrl { + ASTAR = 'https://evm.astar.network', + MOONBASE = 'https://rpc.api.moonbase.moonbeam.network', + MOONBEAM = 'https://rpc.api.moonbeam.network', +} diff --git a/packages/sdk/src/types/computing.ts b/packages/sdk/src/types/computing.ts new file mode 100644 index 0000000..39d773d --- /dev/null +++ b/packages/sdk/src/types/computing.ts @@ -0,0 +1,105 @@ +import { ChainRpcUrl, IApillonPagination } from './apillon'; + +export enum ComputingContractType { + SCHRODINGER = 1, +} + +export enum ComputingContractStatus { + CREATED = 0, + DEPLOY_INITIATED = 1, + DEPLOYING = 2, //INSTANTIATING + DEPLOYED = 3, //INSTANTIATED + TRANSFERRING = 4, + TRANSFERRED = 5, + FAILED = 6, +} + +export enum ComputingTransactionType { + DEPLOY_CONTRACT = 1, + TRANSFER_CONTRACT_OWNERSHIP = 2, + DEPOSIT_TO_CONTRACT_CLUSTER = 3, + ASSIGN_CID_TO_NFT = 4, +} + +export enum ComputingTransactionStatus { + PENDING = 1, + CONFIRMED = 2, + FAILED = 3, + ERROR = 4, + WORKER_SUCCESS = 5, + WORKER_FAILED = 6, +} + +export interface SchrodingerContractData { + /** + * Contract address of NFT which will be used to authenticate decryption + */ + nftContractAddress: string; + /** + * RPC URL of the chain the NFT collection exists on + */ + nftChainRpcUrl: ChainRpcUrl | string; + /** + * If true, only the owner is able to use the contract for data encryption/decryption + * @default true + */ + restrictToOwner?: boolean; +} + +export interface ComputingContractData extends SchrodingerContractData { + /** + * The IPFS gateway where the encrypted files are stored on + */ + ipfsGatewayUrl: string; + /** + * Identifier of the Phala computing cluster the contract runs on + */ + clusterId: string; +} + +export interface IContractListFilters extends IApillonPagination { + contractStatus?: ComputingContractStatus; +} + +export interface ICreateComputingContract { + name: string; + description?: string; + /** + * Bucket where the encrypted files will be stored via IPFS + * @optional If this parameter is not passed, a new bucket will be created for the contract + */ + bucket_uuid?: string; + contractData: SchrodingerContractData; +} + +export interface IComputingTransaction { + walletAddress: string; + transactionType: ComputingTransactionType; + transactionStatus: ComputingContractStatus; + transactionStatusMessage: string; + transactionHash: string; + updateTime: string; + createTime: string; +} + +export interface ITransactionListFilters extends IApillonPagination { + transactionStatus?: ComputingTransactionStatus; + transactionType?: ComputingTransactionType; +} + +export interface IEncryptData { + /** + * fileName for the encrypted file that will be stored in the bucket + */ + fileName: string; + /** + * Contents of the file to encrypt. If the file is an image, the format needs to be base64. + */ + content: Buffer; + /** + * Token ID of the NFT which will be used to decrypt the file's content + * + * The NFT should be a part of the contract's `data.nftContractAddress` field. + */ + nftId: number; +} diff --git a/packages/sdk/src/types/nfts.ts b/packages/sdk/src/types/nfts.ts index 3920a88..c01b248 100644 --- a/packages/sdk/src/types/nfts.ts +++ b/packages/sdk/src/types/nfts.ts @@ -6,6 +6,10 @@ export enum EvmChain { ASTAR = 592, } +export enum SubstrateChain { + ASTAR = 8, +} + export enum CollectionStatus { CREATED = 0, DEPLOY_INITIATED = 1, @@ -36,41 +40,31 @@ export enum TransactionType { NEST_MINT_NFT = 6, } -export interface ICreateCollection { +export interface ICreateCollectionBase { collectionType: CollectionType; - chain: EvmChain; name: string; symbol: string; description?: string; baseUri: string; baseExtension: string; maxSupply?: number; - isRevokable: boolean; - isSoulbound: boolean; - royaltiesAddress: string; + royaltiesAddress?: string; royaltiesFees: number; drop: boolean; dropStart?: number; dropPrice?: number; dropReserve?: number; +} + +export interface ICreateCollection extends ICreateCollectionBase { + isRevokable: boolean; + isSoulbound: boolean; isAutoIncrement?: boolean; + chain: EvmChain; } -//OUTPUTS -export interface ICollection extends ICreateCollection { - collectionUuid: string; - contractAddress: string; - deployerAddress: string; - transactionHash: string; - collectionStatus: number; - collectionType: number; - chain: number; - name: string; - symbol: string; - description: string; - bucketUuid: string; - updateTime: string; - createTime: string; +export interface ICreateSubstrateCollection extends ICreateCollectionBase { + chain: SubstrateChain; } export interface ITransaction { diff --git a/packages/sdk/src/types/social.ts b/packages/sdk/src/types/social.ts new file mode 100644 index 0000000..14a0041 --- /dev/null +++ b/packages/sdk/src/types/social.ts @@ -0,0 +1,43 @@ +import { IApillonPagination } from './apillon'; + +export enum HubStatus { + DRAFT = 1, + ACTIVE = 5, + ERORR = 100, +} + +export interface ICreateHub { + name: string; + /** + * Short description about the hub. + */ + about?: string; + /** + * Comma separated tags associated with the hub. + */ + tags?: string; +} + +export interface ICreateChannel { + title: string; + /** + * Short description or content of the channel. + */ + body: string; + /** + * Comma separated tags associated with the channel. + */ + tags?: string; + /** + * Hub in which the channel will be created + * @default Apillon default hub + */ + hubUuid?: string; +} + +export interface IChannelFilters extends IApillonPagination { + /** + * Parent hub unique identifier + */ + hubUuid?: string; +} diff --git a/packages/sdk/src/types/storage.ts b/packages/sdk/src/types/storage.ts index 960ddd8..08f4a21 100644 --- a/packages/sdk/src/types/storage.ts +++ b/packages/sdk/src/types/storage.ts @@ -12,6 +12,12 @@ export enum FileStatus { AVAILABLE_ON_IPFS_AND_REPLICATED = 4, } +export enum BucketType { + STORAGE = 1, + HOSTING = 2, + NFT_METADATA = 3, +} + export interface IStorageBucketContentRequest extends IApillonPagination { directoryUuid?: string; markedForDeletion?: boolean; diff --git a/packages/sdk/src/util/file-utils.ts b/packages/sdk/src/util/file-utils.ts index 3b6cd1e..78a3a2c 100644 --- a/packages/sdk/src/util/file-utils.ts +++ b/packages/sdk/src/util/file-utils.ts @@ -12,12 +12,14 @@ import { import { LogLevel } from '../types/apillon'; import { randomBytes } from 'crypto'; -export async function uploadFiles( - folderPath: string, - apiPrefix: string, - params?: IFileUploadRequest, - files?: FileMetadata[], -): Promise<{ sessionUuid: string; files: FileMetadata[] }> { +export async function uploadFiles(uploadParams: { + apiPrefix: string; + params?: IFileUploadRequest; + folderPath?: string; + files?: FileMetadata[]; +}): Promise<{ sessionUuid: string; files: FileMetadata[] }> { + const { folderPath, apiPrefix, params } = uploadParams; + let files = uploadParams.files; if (folderPath) { ApillonLogger.log(`Preparing to upload files from ${folderPath}...`); } else if (files?.length) { @@ -46,7 +48,15 @@ export async function uploadFiles( for (const fileGroup of chunkify(files, fileChunkSize)) { const { files } = await ApillonApi.post( `${apiPrefix}/upload`, - { files: fileGroup, sessionUuid }, + { + files: fileGroup.map((fg) => { + // Remove content property from the payload + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { content, ...rest } = fg; + return rest; + }), + sessionUuid, + }, ); await uploadFilesToS3(files, fileGroup);