From 39644fe71c05fd2685b68dde18a0fc595ab89c16 Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:53:37 +0100 Subject: [PATCH 1/7] feat: new api for NVM App integrators --- integration/nevermined/NVMAppAPI.test.ts | 303 ++++++++++++ .../{NVMAppFlows.ts => NVMAppFlows.test.ts} | 0 src/nevermined/NvmApp.ts | 434 ++++++++++++++++++ src/nevermined/resources/AppNetworks.ts | 104 +++++ 4 files changed, 841 insertions(+) create mode 100644 integration/nevermined/NVMAppAPI.test.ts rename integration/nevermined/{NVMAppFlows.ts => NVMAppFlows.test.ts} (100%) create mode 100644 src/nevermined/NvmApp.ts create mode 100644 src/nevermined/resources/AppNetworks.ts diff --git a/integration/nevermined/NVMAppAPI.test.ts b/integration/nevermined/NVMAppAPI.test.ts new file mode 100644 index 000000000..516747e3f --- /dev/null +++ b/integration/nevermined/NVMAppAPI.test.ts @@ -0,0 +1,303 @@ +import chai, { assert } from 'chai' +import chaiAsPromised from 'chai-as-promised' + +import { + Account, + AssetPrice, + MetaData, + Nevermined, + ResourceAuthentication, + SubscriptionCreditsNFTApi, + ZeroAddress, +} from '../../src' +import { config } from '../config' +import TestContractHandler from '../../test/keeper/TestContractHandler' +import { NVMAppEnvironments, NvmApp } from '../../src/nevermined/NvmApp' +import { Signer } from 'ethers' +import { generateSubscriptionMetadata, generateWebServiceMetadata, getMetadata } from '../utils' + +chai.use(chaiAsPromised) + +describe('NVM App API', () => { + describe('LOCAL: As NVM App integrator I want to initialize the api in different ways', () => { + let nvmApp: NvmApp + let signerAddress: string + let defaultSigner: Signer + let publisher: Account + let subscriptionNFTAddress: string + let subscriptionDid: string + let agentDid: string + let datasetDid: string + let agreementId + + // Agent/Service test configuration + const SERVICE_ENDPOINT = process.env.SERVICE_ENDPOINT || 'http://127.0.0.1:3000' + const OPEN_PATH = process.env.OPEN_PATH || '/openapi.json' + const OPEN_ENDPOINT = process.env.OPEN_ENDPOINT || `${SERVICE_ENDPOINT}${OPEN_PATH}` + const AUTHORIZATION_TYPE = (process.env.AUTHORIZATION_TYPE || + 'oauth') as ResourceAuthentication['type'] + const AUTHORIZATION_TOKEN = process.env.AUTHORIZATION_TOKEN || 'new_authorization_token' + const AUTHORIZATION_USER = process.env.AUTHORIZATION_USER || 'user' + const AUTHORIZATION_PASSWORD = process.env.AUTHORIZATION_PASSWORD || 'password' + + before(async () => { + TestContractHandler.setConfig(config) + + const nevermined = await Nevermined.getInstance(config) + ;[, publisher] = await nevermined.accounts.list() + + const contractABI = await TestContractHandler.getABI( + `NFT1155SubscriptionUpgradeable.geth-localnet`, + './artifacts/', + ) + const subscriptionNFT = await SubscriptionCreditsNFTApi.deployInstance( + config, + contractABI, + publisher, + [ + publisher.getId(), + nevermined.keeper.didRegistry.address, + 'App Subscription NFT', + 'CRED', + '', + nevermined.keeper.nvmConfig.address, + ], + ) + + subscriptionNFTAddress = subscriptionNFT.address + console.debug(`Deployed ERC-1155 Subscription NFT on address: ${subscriptionNFTAddress}`) + + await nevermined.contracts.loadNft1155Api(subscriptionNFT) + const neverminedNodeAddress = await nevermined.services.node.getProviderAddress() + + await subscriptionNFT.grantOperatorRole( + nevermined.keeper.conditions.transferNftCondition.address, + publisher, + ) + console.debug(`Granting operator role to Nevermined Node Address: ${neverminedNodeAddress}`) + await subscriptionNFT.grantOperatorRole(neverminedNodeAddress, publisher) + + assert.equal(nevermined.nfts1155.getContract.address, subscriptionNFTAddress) + }) + + it('I want to search content from the app', async () => { + nvmApp = await NvmApp.getInstance(NVMAppEnvironments.Local) + const results = await nvmApp.search.query({}) + console.log(JSON.stringify(results.totalResults)) + + assert.isDefined(results) + }) + + it('Get the default configuration used', async () => { + const appConfig = nvmApp.config + assert.isDefined(appConfig) + }) + + it('Overwrite the default config with some parameters', async () => { + assert.notEqual(nvmApp.config.artifactsFolder, './artifacts') + nvmApp = await NvmApp.getInstance(NVMAppEnvironments.Local, config) + assert.equal(nvmApp.config.artifactsFolder, './artifacts') + }) + + it('I want to connect my account', async () => { + assert.isFalse(nvmApp.isWeb3Connected()) + + defaultSigner = config.accounts[0] + signerAddress = await defaultSigner.getAddress() + console.log(`Account address: ${signerAddress}`) + await nvmApp.connect(signerAddress) + + assert.isTrue(nvmApp.isWeb3Connected()) + }) + + it('I want to create a time subscription', async () => { + const timeSubscriptionMetadata = generateSubscriptionMetadata( + 'NVM App Time only Subscription test', + ) + const subscriptionPrice = new AssetPrice(publisher.getId(), 10n).setTokenAddress(ZeroAddress) // Using native token + + const ddo = await nvmApp.createTimeSubscription( + timeSubscriptionMetadata, + subscriptionPrice, + 100, // 100 blocks duration + ) + + assert.isDefined(ddo) + const ddoFound = await nvmApp.search.byDID(ddo.id) + assert.equal(ddo.id, ddoFound.id) + }) + + it('I want to create a credits subscription', async () => { + const creditsSubscriptionMetadata = generateSubscriptionMetadata( + 'NVM App Credits Subscription test', + ) + const subscriptionPrice = new AssetPrice(publisher.getId(), 10n).setTokenAddress(ZeroAddress) // Using native token + + const ddo = await nvmApp.createCreditsSubscription( + creditsSubscriptionMetadata, + subscriptionPrice, + 50n, // blocks duration + ) + + assert.isDefined(ddo) + const ddoFound = await nvmApp.search.byDID(ddo.id) + assert.equal(ddo.id, ddoFound.id) + subscriptionDid = ddo.id + }) + + it('I want to register an Agent', async () => { + const agentMetadata = generateWebServiceMetadata( + 'Nevermined Web Service Metadata', + `${SERVICE_ENDPOINT}(.*)`, + [OPEN_ENDPOINT], + AUTHORIZATION_TYPE, + AUTHORIZATION_TOKEN, + AUTHORIZATION_USER, + AUTHORIZATION_PASSWORD, + ) as MetaData + + const ddo = await nvmApp.registerServiceAsset( + agentMetadata, + subscriptionDid, + // We are gonna configure the agent usage costs in a dynamic manner: + // The cost in credits for every succesful query to the agent will be between 1 and 5 credits being 2 credits the default cost + 2n, // default cost in credits for every succesful query to the agent + 1n, // min amount of credits to be consumed + 5n, // max amount of credits to be consumed + ) + + assert.isDefined(ddo) + const ddoFound = await nvmApp.search.byDID(ddo.id) + assert.equal(ddo.id, ddoFound.id) + agentDid = ddo.id + }) + + it('I want to register a Dataset', async () => { + const datasetMetadata = getMetadata() + + const ddo = await nvmApp.registerFileAsset( + datasetMetadata, + subscriptionDid, + 1n, // every file download costs 1 credit to the subscriber + ) + + assert.isDefined(ddo) + const ddoFound = await nvmApp.search.byDID(ddo.id) + assert.equal(ddo.id, ddoFound.id) + datasetDid = ddo.id + }) + + it('I want to order a subscription', async () => { + const orderResult = await nvmApp.orderSubscription(subscriptionDid) + assert.isDefined(orderResult) + assert.isTrue(orderResult.success) + assert.isTrue(orderResult.agreementId.length > 0) + agreementId = orderResult.agreementId + }) + + it('I want to get the token giving access to a remote agent', async () => { + const token = await nvmApp.getServiceAccessToken(agentDid) + assert.isDefined(token) + assert.isTrue(token.accessToken.length > 0) + assert.isTrue(token.neverminedProxyUri.length > 0) + }) + + it('I want to download a file asset', async () => { + const results = await nvmApp.downloadFiles( + datasetDid, + agreementId, + `/tmp/.nevermined/downloads/${datasetDid}/`, + ) + + assert.isDefined(results) + assert.isTrue(results.success) + }) + + it('I can disconnect and still search', async () => { + await nvmApp.disconnect() + const results = await nvmApp.search.query({}) + assert.isTrue(results.totalResults.value > 0) + }) + }) + + // describe.skip('TESTING: As NVM App integrator I want to initialize the api in different ways', () => { + + // let zerodevProvider: ZeroDevEthersProvider<'ECDSA'> + + // before(async () => { + // // TestContractHandler.setConfig(config) + + // // const projectId = process.env.PROJECT_ID! + // // const owner = ethers.Wallet.createRandom() + + // // zerodevProvider = await ZeroDevEthersProvider.init('ECDSA', { + // // projectId, + // // owner: convertEthersV6SignerToAccountSigner(owner), + // // }) + + // // nevermined = await Nevermined.getInstance(config, { + // // loadCore: true, + // // loadServiceAgreements: true, + // // loadNFTs1155: true, + // // loadNFTs721: false, + // // loadDispenser: true, + // // loadERC20Token: true, + // // loadAccessFlow: false, + // // loadDIDTransferFlow: false, + // // loadRewards: false, + // // loadRoyalties: true, + // // loadCompute: false, + // // }) + // // ;[, publisher] = await nevermined.accounts.list() + + // // const clientAssertion = await nevermined.utils.jwt.generateClientAssertion(publisher) + + // // await nevermined.services.marketplace.login(clientAssertion) + // // payload = decodeJwt(config.marketplaceAuthToken) + + // }) + // it('I want to search content from the app', async () => { + + // const nvmApp = await NvmApp.getInstance(NVMAppEnvironments.Local) + // // const wallet = ethers.Wallet.createRandom() + // // nvmApp.connect(await wallet.getAddress()) + // const results = await nvmApp.search.query({}) + // console.log(JSON.stringify(results, null, 2)) + // // nvmApp.subscriptions.createTimeSubscription() + // // nvmApp.subscriptions.createCreditsSubscription() + // // nvmApp.assets.createAgent() + // // nvmApp.assets.createDataset() + // }) + + // it.skip('I want to connect my account', async () => { + // }) + + // it.skip('Overwrite the default config with some parameters', async () => { + // }) + + // it.skip('Download the configuration', async () => { + // }) + + // it.skip('I want to create a time subscription', async () => { + // }) + + // it.skip('I want to create a credits subscription', async () => { + // }) + + // it.skip('I want to create an Agent', async () => { + // }) + + // it.skip('I want to create a Dataset', async () => { + // }) + + // it.skip('I want to order a subscription', async () => { + // }) + + // it.skip('I want to access a remote service', async () => { + // }) + + // it.skip('I want to download a file asset', async () => { + // }) + + // }) +}) diff --git a/integration/nevermined/NVMAppFlows.ts b/integration/nevermined/NVMAppFlows.test.ts similarity index 100% rename from integration/nevermined/NVMAppFlows.ts rename to integration/nevermined/NVMAppFlows.test.ts diff --git a/src/nevermined/NvmApp.ts b/src/nevermined/NvmApp.ts new file mode 100644 index 000000000..a03b8324e --- /dev/null +++ b/src/nevermined/NvmApp.ts @@ -0,0 +1,434 @@ +import { ZeroDevAccountSigner } from '@zerodev/sdk' +import { + Account, + AssetPrice, + ContractHandler, + DDO, + MetaData, + NFTAttributes, + Nevermined, + NeverminedInitializationOptions, + NeverminedOptions, + PublishMetadataOptions, + PublishOnChainOptions, + SearchApi, + SubscriptionToken, + Web3Error, +} from '../sdk' +import { + AppDeploymentArbitrum, + AppDeploymentGnosis, + AppDeploymentLocal, + AppDeploymentMatic, + AppDeploymentMumbai, + AppDeploymentStaging, + AppDeploymentTesting, + NeverminedAppOptions, +} from './resources/AppNetworks' +import { isAddress } from 'ethers' + +export enum NVMAppEnvironments { + Staging = 'staging', + Testing = 'testing', + Live = 'live', + Matic = 'matic', + Mumbai = 'mumbai', + Gnosis = 'gnosis', + Local = 'local', + Custom = 'custom', +} +export interface MetadataValidationResults { + isValid: boolean + messages: string[] +} + +export interface OperationResult { + agreementId: string + success: boolean +} + +export class NvmApp { + private configNVM: NeverminedAppOptions + private userAccount: Account | undefined + private searchSDK: Nevermined | undefined + private fullSDK: Nevermined | undefined + private useZeroDevSigner: boolean = false + private zeroDevSignerAccount?: ZeroDevAccountSigner<'ECDSA'> + private assetProviders: string[] = [] + private subscriptionNFTContractAddress: string | undefined + + static readonly defaultAppInitializatioinOptions: NeverminedInitializationOptions = { + loadCore: true, + loadServiceAgreements: true, + loadNFTs1155: true, + loadNFTs721: false, + loadDispenser: true, + loadERC20Token: true, + loadAccessFlow: false, + loadDIDTransferFlow: false, + loadRewards: false, + loadRoyalties: true, + loadCompute: false, + } + + static readonly publicationOptions = { + metadata: PublishMetadataOptions.OnlyMetadataAPI, + did: PublishOnChainOptions.DIDRegistry, + } + + public static async getInstance( + appEnv: NVMAppEnvironments, + config?: NeverminedOptions, + ): Promise { + const defaultEnvConfig = this.getConfigFromTagName(appEnv) + const mergedConfig = config ? { ...defaultEnvConfig, ...config } : defaultEnvConfig + const nvmApp = new NvmApp(mergedConfig as NeverminedAppOptions) + await nvmApp.initializeSearch() + return nvmApp + } + + private constructor(config: NeverminedAppOptions) { + this.configNVM = config + } + + public async initializeSearch(config?: NeverminedAppOptions) { + this.searchSDK = await Nevermined.getSearchOnlyInstance(config ? config : this.configNVM) + } + + public async connect( + account: string | ZeroDevAccountSigner<'ECDSA'> | Account, + config?: NeverminedOptions, + initOptions?: NeverminedInitializationOptions, + ) { + const ops = initOptions + ? { ...NvmApp.defaultAppInitializatioinOptions, ...initOptions } + : NvmApp.defaultAppInitializatioinOptions + this.fullSDK = await Nevermined.getInstance(config ? config : this.configNVM, ops) + + if (account instanceof ZeroDevAccountSigner) { + this.userAccount = await Account.fromZeroDevSigner(account) + this.zeroDevSignerAccount = account + this.useZeroDevSigner = true + } else if (account instanceof Account) { + this.userAccount = account + } else { + this.userAccount = this.fullSDK.accounts.getAccount(account) + } + + const clientAssertion = await this.fullSDK.utils.jwt.generateClientAssertion(this.userAccount) + this.fullSDK.services.marketplace.login(clientAssertion) + + const nodeInfo = await this.fullSDK.services.node.getNeverminedNodeInfo() + this.assetProviders = [nodeInfo['provider-address']] + + if (!isAddress(this.configNVM.nftContractAddress)) { + const contractABI = await ContractHandler.getABI( + 'NFT1155SubscriptionUpgradeable', + this.configNVM.artifactsFolder, + await this.fullSDK.keeper.getNetworkName(), + ) + this.subscriptionNFTContractAddress = contractABI.address + } else { + this.subscriptionNFTContractAddress = this.configNVM.nftContractAddress + } + if (!isAddress(this.subscriptionNFTContractAddress)) { + throw new Web3Error('Invalid Subscription NFT contract address') + } + } + + public async disconnect() { + if (this.fullSDK && this.isWeb3Connected()) { + this.fullSDK = undefined + this.userAccount = undefined + this.useZeroDevSigner = false + this.zeroDevSignerAccount = undefined + } + } + + public isWeb3Connected(): boolean { + return this.fullSDK ? this.fullSDK.isKeeperConnected : false + } + + public get config(): NeverminedOptions { + return this.configNVM + } + + public get search(): SearchApi { + return this.searchSDK.search + } + + public async createTimeSubscription( + susbcriptionMetadata: MetaData, + subscriptionPrice: AssetPrice, + duration: number, + ): Promise { + if (!this.isWeb3Connected()) + throw new Web3Error('Web3 not connected, try calling the connect method first') + + const validationResult = NvmApp.validateTimeSubscriptionMetadata(susbcriptionMetadata) + if (!validationResult.isValid) { + throw new Error(validationResult.messages.join(',')) + } + + this.fullSDK.services.node.getVersionInfo() + const nftAttributes = NFTAttributes.getCreditsSubscriptionInstance({ + metadata: susbcriptionMetadata, + services: [ + { + serviceType: 'nft-sales', + price: subscriptionPrice, + nft: { + duration, + amount: 1n, + nftTransfer: false, + }, + }, + ], + providers: this.assetProviders, + nftContractAddress: this.subscriptionNFTContractAddress, + preMint: false, + }) + + return await this.fullSDK.nfts1155.create( + nftAttributes, + this.userAccount, + NvmApp.publicationOptions, + { ...(this.useZeroDevSigner && { zeroDevSigner: this.zeroDevSignerAccount }) }, + ) + } + + public async createCreditsSubscription( + susbcriptionMetadata: MetaData, + subscriptionPrice: AssetPrice, + numberCredits: bigint, + ): Promise { + if (!this.isWeb3Connected()) + throw new Web3Error('Web3 not connected, try calling the connect method first') + + const validationResult = NvmApp.validateCreditsSubscriptionMetadata(susbcriptionMetadata) + if (!validationResult.isValid) { + throw new Error(validationResult.messages.join(',')) + } + + const nftAttributes = NFTAttributes.getCreditsSubscriptionInstance({ + metadata: susbcriptionMetadata, + services: [ + { + serviceType: 'nft-sales', + price: subscriptionPrice, + nft: { + amount: numberCredits, + nftTransfer: false, + }, + }, + ], + providers: this.assetProviders, + nftContractAddress: this.subscriptionNFTContractAddress, + preMint: false, + }) + + return await this.fullSDK.nfts1155.create( + nftAttributes, + this.userAccount, + NvmApp.publicationOptions, + { ...(this.useZeroDevSigner && { zeroDevSigner: this.zeroDevSignerAccount }) }, + ) + } + + public async orderSubscription( + subscriptionDid: string, + agreementId?: string, + ): Promise { + if (!this.isWeb3Connected()) + throw new Web3Error('Web3 not connected, try calling the connect method first') + + let numberCredits: bigint + let serviceIndex: number + let transferResult = false + try { + const ddo = await this.fullSDK.assets.resolve(subscriptionDid) + const salesService = ddo.findServiceByReference('nft-sales') + serviceIndex = salesService.index + numberCredits = salesService.attributes.main.nftAttributes.amount + + if (!agreementId) + agreementId = await this.fullSDK.nfts1155.order( + subscriptionDid, + numberCredits, + this.userAccount, + serviceIndex, + ) + const subscriptionOwner = await this.fullSDK.assets.owner(subscriptionDid) + transferResult = await this.fullSDK.nfts1155.claim( + agreementId, + subscriptionOwner, + this.userAccount.getId(), + numberCredits, + subscriptionDid, + serviceIndex, + ) + } catch (error) { + throw new Web3Error(`Error ordering subscription: ${error.message}`) + } + + return { agreementId, success: transferResult } + } + + public async getServiceAccessToken(serviceDid: string): Promise { + if (!this.isWeb3Connected()) + throw new Web3Error('Web3 not connected, try calling the connect method first') + + return await this.fullSDK.nfts1155.getSubscriptionToken(serviceDid, this.userAccount) + } + + public async downloadFiles( + fileAssetDid: string, + agreementId: string, + destinationPath: string, + ): Promise { + try { + const result = await this.fullSDK.nfts1155.access( + fileAssetDid, + this.userAccount, + destinationPath, + undefined, + agreementId, + ) + return { agreementId, success: result } + } catch (error) { + throw new Web3Error(`Error downloading files: ${error.message}`) + } + } + + public async registerServiceAsset( + metadata: MetaData, + subscriptionDid: string, + costInCredits = 1n, + minCreditsToCharge = 1n, + maxCreditsToCharge = 1n, + ): Promise { + if (!this.isWeb3Connected()) + throw new Web3Error('Web3 not connected, try calling the connect method first') + + const validationResult = NvmApp.validateServiceAssetMetadata(metadata) + if (!validationResult.isValid) { + throw new Error(validationResult.messages.join(',')) + } + + const nftAttributes = NFTAttributes.getCreditsSubscriptionInstance({ + metadata, + services: [ + { + serviceType: 'nft-access', + nft: { + tokenId: subscriptionDid, + amount: costInCredits, + maxCreditsToCharge, + minCreditsToCharge, + nftTransfer: false, + }, + }, + ], + providers: this.assetProviders, + nftContractAddress: this.subscriptionNFTContractAddress, + preMint: false, + }) + + return await this.fullSDK.nfts1155.create( + nftAttributes, + this.userAccount, + NvmApp.publicationOptions, + { ...(this.useZeroDevSigner && { zeroDevSigner: this.zeroDevSignerAccount }) }, + ) + } + + public async registerFileAsset( + metadata: MetaData, + subscriptionDid: string, + costInCredits = 1n, + ): Promise { + if (!this.isWeb3Connected()) + throw new Web3Error('Web3 not connected, try calling the connect method first') + + const validationResult = NvmApp.validateFileAssetMetadata(metadata) + if (!validationResult.isValid) { + throw new Error(validationResult.messages.join(',')) + } + + const nftAttributes = NFTAttributes.getCreditsSubscriptionInstance({ + metadata, + services: [ + { + serviceType: 'nft-access', + nft: { + tokenId: subscriptionDid, + amount: costInCredits, + nftTransfer: false, + }, + }, + ], + providers: this.assetProviders, + nftContractAddress: this.subscriptionNFTContractAddress, + preMint: false, + }) + + return await this.fullSDK.nfts1155.create( + nftAttributes, + this.userAccount, + NvmApp.publicationOptions, + { ...(this.useZeroDevSigner && { zeroDevSigner: this.zeroDevSignerAccount }) }, + ) + } + + // TODO: Implement subscription validations + public static validateTimeSubscriptionMetadata( + _susbcriptionMetadata: MetaData, + ): MetadataValidationResults { + return { isValid: true, messages: [] } + } + + // TODO: Implement subscription validations + public static validateCreditsSubscriptionMetadata( + _susbcriptionMetadata: MetaData, + ): MetadataValidationResults { + return { isValid: true, messages: [] } + } + + // TODO: Implement subscription validations + public static validateServiceAssetMetadata( + _susbcriptionMetadata: MetaData, + ): MetadataValidationResults { + return { isValid: true, messages: [] } + } + + // TODO: Implement subscription validations + public static validateFileAssetMetadata( + _susbcriptionMetadata: MetaData, + ): MetadataValidationResults { + return { isValid: true, messages: [] } + } + + private static getConfigFromTagName(appEnv: NVMAppEnvironments) { + const defaultDeploymentConfig = this.switchConfigBetweenEnvs(appEnv) + return defaultDeploymentConfig + } + + private static switchConfigBetweenEnvs(appEnv: NVMAppEnvironments): NeverminedOptions { + switch (appEnv) { + case NVMAppEnvironments.Staging: + return new AppDeploymentStaging() + case NVMAppEnvironments.Testing: + return new AppDeploymentTesting() + case NVMAppEnvironments.Live: + return new AppDeploymentArbitrum() + case NVMAppEnvironments.Matic: + return new AppDeploymentMatic() + case NVMAppEnvironments.Mumbai: + return new AppDeploymentMumbai() + case NVMAppEnvironments.Gnosis: + return new AppDeploymentGnosis() + default: + return new AppDeploymentLocal() + } + } +} diff --git a/src/nevermined/resources/AppNetworks.ts b/src/nevermined/resources/AppNetworks.ts new file mode 100644 index 000000000..571488415 --- /dev/null +++ b/src/nevermined/resources/AppNetworks.ts @@ -0,0 +1,104 @@ +import { NeverminedOptions } from '../../models' + +export class NeverminedAppOptions extends NeverminedOptions { + instanceName: string + nftContractAddress?: string + tokenAddress?: string // ERC-20 token address or zero address (0x000..) for native token +} + +export class AppDeploymentLocal extends NeverminedAppOptions { + instanceName = 'localnet' + web3ProviderUri = 'http://contracts.nevermined.localnet' + marketplaceUri = 'http://marketplace.nevermined.localnet' + graphHttpUri = 'http://localhost:9000/subgraphs/name/nevermined-io/development' + neverminedNodeUri = 'http://node.nevermined.localnet' + neverminedNodeAddress = '0x068ed00cf0441e4829d9784fcbe7b9e26d4bd8d0' + verbose = true + gasMultiplier = 0 + gasPriceMultiplier = 0 + nftContractAddress = undefined +} + +export class AppDeploymentStaging extends NeverminedAppOptions { + instanceName = 'appStaging' + web3ProviderUri = 'https://sepolia-rollup.arbitrum.io/rpc' + marketplaceUri = 'https://marketplace-api.staging.nevermined.app' + graphHttpUri = 'https://api.thegraph.com/subgraphs/name/nevermined-io/public' + neverminedNodeUri = 'https://node.staging.nevermined.app' + neverminedNodeAddress = '0x046d0698926aFa3ab6D6591f03063488F3Fb4327' + verbose = true + gasMultiplier = 0 + gasPriceMultiplier = 0 + nftContractAddress = undefined + tokenAddress = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d' +} + +export class AppDeploymentTesting extends NeverminedAppOptions { + instanceName = 'appTesting' + web3ProviderUri = 'https://sepolia-rollup.arbitrum.io/rpc' + marketplaceUri = 'https://marketplace-api.testing.nevermined.app' + graphHttpUri = 'https://api.thegraph.com/subgraphs/name/nevermined-io/public' + neverminedNodeUri = 'https://node.testing.nevermined.app' + neverminedNodeAddress = '0x046d0698926aFa3ab6D6591f03063488F3Fb4327' + verbose = true + gasMultiplier = 0 + gasPriceMultiplier = 0 + nftContractAddress = undefined + tokenAddress = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d' +} + +export class AppDeploymentArbitrum extends NeverminedAppOptions { + instanceName = 'appArbitrum' + web3ProviderUri = 'https://arb1.arbitrum.io/rpc' + marketplaceUri = 'https://marketplace-api.arbitrum.nevermined.app' + graphHttpUri = 'https://api.thegraph.com/subgraphs/name/nevermined-io/public' + neverminedNodeUri = 'https://node.arbitrum.nevermined.app' + neverminedNodeAddress = '0x0b5297b97655A29dE245700864F5591741e50d2c' + verbose = true + gasMultiplier = 0 + gasPriceMultiplier = 0 + nftContractAddress = undefined + tokenAddress = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' +} + +export class AppDeploymentGnosis extends NeverminedAppOptions { + instanceName = 'appGnosis' + web3ProviderUri = 'https://rpc.gnosischain.com/' + marketplaceUri = 'https://marketplace-api.gnosis.nevermined.app' + graphHttpUri = 'https://api.thegraph.com/subgraphs/name/nevermined-io/public' + neverminedNodeUri = 'https://node.gnosis.nevermined.app' + neverminedNodeAddress = '0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1' + verbose = true + gasMultiplier = 0 + gasPriceMultiplier = 0 + nftContractAddress = undefined + tokenAddress = '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83' +} + +export class AppDeploymentMumbai extends NeverminedAppOptions { + instanceName = 'appMumbai' + web3ProviderUri = 'https://matic-mumbai.chainstacklabs.com' + marketplaceUri = 'https://marketplace-api.mumbai.nevermined.app' + graphHttpUri = 'https://api.thegraph.com/subgraphs/name/nevermined-io/public' + neverminedNodeUri = 'https://node.mumbai.nevermined.app' + neverminedNodeAddress = '0x5838B5512cF9f12FE9f2beccB20eb47211F9B0bc' + verbose = true + gasMultiplier = 0 + gasPriceMultiplier = 0 + nftContractAddress = undefined + tokenAddress = '0x2058a9d7613eee744279e3856ef0eada5fcbaa7e' +} + +export class AppDeploymentMatic extends NeverminedAppOptions { + instanceName = 'appMatic' + web3ProviderUri = 'https://polygon-rpc.com' + marketplaceUri = 'https://marketplace-api.matic.nevermined.app' + graphHttpUri = 'https://api.thegraph.com/subgraphs/name/nevermined-io/public' + neverminedNodeUri = 'https://node.matic.nevermined.app' + neverminedNodeAddress = '0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1' + verbose = true + gasMultiplier = 1.2 + gasPriceMultiplier = 1.2 + nftContractAddress = undefined + tokenAddress = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174' +} From 5833a3f0b8d7371dc8fd27938baf4d6c1d962eb5 Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:43:43 +0100 Subject: [PATCH 2/7] feat: e2e staging tests --- .../external/NVMAppAPI.staging.test.ts | 256 ++++++++++++++++++ integration/nevermined/NVMAppAPI.test.ts | 93 +------ package.json | 2 +- .../templates/AgreementTemplate.abstract.ts | 1 + src/models/AssetPrice.ts | 41 ++- src/nevermined/NvmApp.ts | 100 +++++-- 6 files changed, 374 insertions(+), 119 deletions(-) create mode 100644 integration/external/NVMAppAPI.staging.test.ts diff --git a/integration/external/NVMAppAPI.staging.test.ts b/integration/external/NVMAppAPI.staging.test.ts new file mode 100644 index 000000000..06097a052 --- /dev/null +++ b/integration/external/NVMAppAPI.staging.test.ts @@ -0,0 +1,256 @@ +import chai, { assert } from 'chai' +import chaiAsPromised from 'chai-as-promised' + +import { + AssetPrice, + MetaData, + ResourceAuthentication, + convertEthersV6SignerToAccountSigner, + makeAccounts, +} from '../../src' +import TestContractHandler from '../../test/keeper/TestContractHandler' +import { NVMAppEnvironments, NVMAppSubscriptionType, NvmApp } from '../../src/nevermined/NvmApp' +import { ethers, isAddress } from 'ethers' +import { generateSubscriptionMetadata, generateWebServiceMetadata, getMetadata } from '../utils' +import { ZeroDevAccountSigner, ZeroDevEthersProvider } from '@zerodev/sdk' +import { AppDeploymentStaging } from '../../src/nevermined/resources/AppNetworks' + +chai.use(chaiAsPromised) + +// Execute first: +// ./scripts/download-artifacts.sh v3.5.6 arbitrum-sepolia public +// export PROJECT_ID=your_project_id +describe('NVM App API', () => { + let nvmAppPublisher: NvmApp + let nvmAppSubscriber: NvmApp + + let subscriptionNFTAddress: string + let subscriptionDid: string + let agentDid: string + let datasetDid: string + let agreementId + + const projectId = process.env.PROJECT_ID! + let zerodevProvider: ZeroDevEthersProvider<'ECDSA'> + let accountSigner: ZeroDevAccountSigner<'ECDSA'> + let publisherAddress: string + let subscriptionPrice: AssetPrice + let subscriptionPriceWithFees: AssetPrice + + // Agent/Service test configuration + const SERVICE_ENDPOINT = process.env.SERVICE_ENDPOINT || 'http://127.0.0.1:3000' + const OPEN_PATH = process.env.OPEN_PATH || '/openapi.json' + const OPEN_ENDPOINT = process.env.OPEN_ENDPOINT || `${SERVICE_ENDPOINT}${OPEN_PATH}` + const AUTHORIZATION_TYPE = (process.env.AUTHORIZATION_TYPE || + 'oauth') as ResourceAuthentication['type'] + const AUTHORIZATION_TOKEN = process.env.AUTHORIZATION_TOKEN || 'new_authorization_token' + const AUTHORIZATION_USER = process.env.AUTHORIZATION_USER || 'user' + const AUTHORIZATION_PASSWORD = process.env.AUTHORIZATION_PASSWORD || 'password' + + before(async () => { + const owner = ethers.Wallet.createRandom() + zerodevProvider = await ZeroDevEthersProvider.init('ECDSA', { + projectId, + owner: convertEthersV6SignerToAccountSigner(owner), + }) + + const contractABI = await TestContractHandler.getABI( + `NFT1155SubscriptionUpgradeable.arbitrum-sepolia`, + './artifacts/', + ) + + subscriptionNFTAddress = contractABI.address + console.debug(`Using ERC-1155 Subscription NFT on address: ${subscriptionNFTAddress}`) + + accountSigner = zerodevProvider.getAccountSigner() + publisherAddress = await accountSigner.getAddress() + + // Using USDC token address + // WARN: Make sure the subscriber account has balance to pay the gas and the subscription + subscriptionPrice = new AssetPrice(publisherAddress, 1000n).setTokenAddress( + new AppDeploymentStaging().tokenAddress, + ) + }) + + describe('As a PUBLISHER', () => { + it('I want to search content from the app', async () => { + nvmAppPublisher = await NvmApp.getInstance(NVMAppEnvironments.Staging) + const results = await nvmAppPublisher.search.query({}) + console.log(JSON.stringify(results.totalResults)) + + assert.isDefined(results) + }) + + it('Get the default configuration used', async () => { + const appConfig = nvmAppPublisher.config + assert.isDefined(appConfig) + assert.equal(appConfig.marketplaceUri, 'https://marketplace-api.staging.nevermined.app') + }) + + it('Overwrite the default config with some parameters', async () => { + assert.notEqual(nvmAppPublisher.config.artifactsFolder, './artifacts') + nvmAppPublisher = await NvmApp.getInstance(NVMAppEnvironments.Staging, { + artifactsFolder: './artifacts', + }) + assert.equal(nvmAppPublisher.config.artifactsFolder, './artifacts') + }) + + it('I want to connect my account', async () => { + assert.isFalse(nvmAppPublisher.isWeb3Connected()) + + await nvmAppPublisher.connect(accountSigner) + + assert.isTrue(nvmAppPublisher.isWeb3Connected()) + assert.isTrue(nvmAppPublisher.getLoginCredentials().length > 0) + }) + + it('I can get the network fees', async () => { + const networkFees = nvmAppPublisher.networkFees + assert.isDefined(networkFees) + console.log(`Network Fees: ${JSON.stringify(networkFees)}`) + assert.isTrue(isAddress(networkFees.receiver)) + assert.isTrue(networkFees.fee > 0) + }) + + it('I can calculate and include network fees', async () => { + // console.log(`AssetPrice object: ${subscriptionPrice.toString()}`) + + // assert.isFalse(nvmApp.isNetworkFeeIncluded(subscriptionPrice)) + + subscriptionPriceWithFees = nvmAppPublisher.addNetworkFee(subscriptionPrice) + console.log(`Asset Price with fees: ${subscriptionPriceWithFees.toString()}`) + + assert.isTrue(nvmAppPublisher.isNetworkFeeIncluded(subscriptionPriceWithFees)) + }) + + it('I want to create a time subscription', async () => { + const timeSubscriptionMetadata = generateSubscriptionMetadata( + 'NVM App Time only Subscription test', + ) + timeSubscriptionMetadata.additionalInformation.customData = { + subscriptionLimitType: NVMAppSubscriptionType.Time, + dateMeasure: 'hours', + } + + const ddo = await nvmAppPublisher.createTimeSubscription( + timeSubscriptionMetadata, + subscriptionPriceWithFees, + 100, // 100 blocks duration + ) + + assert.isDefined(ddo) + const ddoFound = await nvmAppPublisher.search.byDID(ddo.id) + assert.equal(ddo.id, ddoFound.id) + }) + + it('I want to create a credits subscription', async () => { + const creditsSubscriptionMetadata = generateSubscriptionMetadata( + 'NVM App Credits Subscription test', + ) + creditsSubscriptionMetadata.additionalInformation.customData = { + subscriptionLimitType: NVMAppSubscriptionType.Credits, + } + + const ddo = await nvmAppPublisher.createCreditsSubscription( + creditsSubscriptionMetadata, + subscriptionPriceWithFees, + 50n, // number of credits given to the subscribers + ) + + assert.isDefined(ddo) + const ddoFound = await nvmAppPublisher.search.byDID(ddo.id) + assert.equal(ddo.id, ddoFound.id) + subscriptionDid = ddo.id + }) + + it('I want to register an Agent', async () => { + const agentMetadata = generateWebServiceMetadata( + 'Nevermined Web Service Metadata', + `${SERVICE_ENDPOINT}(.*)`, + [OPEN_ENDPOINT], + AUTHORIZATION_TYPE, + AUTHORIZATION_TOKEN, + AUTHORIZATION_USER, + AUTHORIZATION_PASSWORD, + ) as MetaData + + const ddo = await nvmAppPublisher.registerServiceAsset( + agentMetadata, + subscriptionDid, + // We are gonna configure the agent usage costs in a dynamic manner: + // The cost in credits for every succesful query to the agent will be between 1 and 5 credits being 2 credits the default cost + 2n, // default cost in credits for every succesful query to the agent + 1n, // min amount of credits to be consumed + 5n, // max amount of credits to be consumed + ) + + assert.isDefined(ddo) + const ddoFound = await nvmAppPublisher.search.byDID(ddo.id) + assert.equal(ddo.id, ddoFound.id) + agentDid = ddo.id + }) + + it('I want to register a Dataset', async () => { + const datasetMetadata = getMetadata() + + const ddo = await nvmAppPublisher.registerFileAsset( + datasetMetadata, + subscriptionDid, + 1n, // every file download costs 1 credit to the subscriber + ) + + assert.isDefined(ddo) + const ddoFound = await nvmAppPublisher.search.byDID(ddo.id) + assert.equal(ddo.id, ddoFound.id) + datasetDid = ddo.id + }) + }) + + describe('As a SUBSCRIBER', () => { + it('I want to connect as subscriber', async () => { + nvmAppSubscriber = await NvmApp.getInstance(NVMAppEnvironments.Staging, { + artifactsFolder: './artifacts', + }) + const appConfig = nvmAppSubscriber.config + appConfig.accounts = makeAccounts(process.env.SEED_WORDS) + + const subscriberAddress = await appConfig.accounts[0].getAddress() + await nvmAppSubscriber.connect(subscriberAddress, appConfig) + + assert.isTrue(nvmAppSubscriber.isWeb3Connected()) + }) + + it('I want to order a subscription', async () => { + const orderResult = await nvmAppSubscriber.orderSubscription(subscriptionDid) + assert.isDefined(orderResult) + assert.isTrue(orderResult.success) + assert.isTrue(orderResult.agreementId.length > 0) + agreementId = orderResult.agreementId + }) + + it('I want to get the token giving access to a remote agent', async () => { + const token = await nvmAppSubscriber.getServiceAccessToken(agentDid) + console.log(`Token: ${JSON.stringify(token)}`) + assert.isDefined(token) + assert.isTrue(token.accessToken.length > 0) + assert.isTrue(token.neverminedProxyUri.length > 0) + }) + + it('I want to download a file asset', async () => { + const results = await nvmAppSubscriber.downloadFiles( + datasetDid, + agreementId, + `/tmp/.nevermined/downloads/${datasetDid}/`, + ) + + assert.isDefined(results) + assert.isTrue(results.success) + }) + + it('I can disconnect and still search', async () => { + await nvmAppSubscriber.disconnect() + const results = await nvmAppSubscriber.search.query({}) + assert.isTrue(results.totalResults.value > 0) + }) + }) +}) diff --git a/integration/nevermined/NVMAppAPI.test.ts b/integration/nevermined/NVMAppAPI.test.ts index 516747e3f..dde971903 100644 --- a/integration/nevermined/NVMAppAPI.test.ts +++ b/integration/nevermined/NVMAppAPI.test.ts @@ -29,6 +29,8 @@ describe('NVM App API', () => { let agentDid: string let datasetDid: string let agreementId + let subscriptionPrice: AssetPrice + let subscriptionPriceWithFees: AssetPrice // Agent/Service test configuration const SERVICE_ENDPOINT = process.env.SERVICE_ENDPOINT || 'http://127.0.0.1:3000' @@ -78,6 +80,7 @@ describe('NVM App API', () => { await subscriptionNFT.grantOperatorRole(neverminedNodeAddress, publisher) assert.equal(nevermined.nfts1155.getContract.address, subscriptionNFTAddress) + subscriptionPrice = new AssetPrice(publisher.getId(), 1000n).setTokenAddress(ZeroAddress) }) it('I want to search content from the app', async () => { @@ -110,11 +113,17 @@ describe('NVM App API', () => { assert.isTrue(nvmApp.isWeb3Connected()) }) + it('I can calculate and include network fees', async () => { + subscriptionPriceWithFees = nvmApp.addNetworkFee(subscriptionPrice) + console.log(`Asset Price with fees: ${subscriptionPriceWithFees.toString()}`) + + assert.isTrue(nvmApp.isNetworkFeeIncluded(subscriptionPriceWithFees)) + }) + it('I want to create a time subscription', async () => { const timeSubscriptionMetadata = generateSubscriptionMetadata( 'NVM App Time only Subscription test', ) - const subscriptionPrice = new AssetPrice(publisher.getId(), 10n).setTokenAddress(ZeroAddress) // Using native token const ddo = await nvmApp.createTimeSubscription( timeSubscriptionMetadata, @@ -131,7 +140,6 @@ describe('NVM App API', () => { const creditsSubscriptionMetadata = generateSubscriptionMetadata( 'NVM App Credits Subscription test', ) - const subscriptionPrice = new AssetPrice(publisher.getId(), 10n).setTokenAddress(ZeroAddress) // Using native token const ddo = await nvmApp.createCreditsSubscription( creditsSubscriptionMetadata, @@ -219,85 +227,4 @@ describe('NVM App API', () => { assert.isTrue(results.totalResults.value > 0) }) }) - - // describe.skip('TESTING: As NVM App integrator I want to initialize the api in different ways', () => { - - // let zerodevProvider: ZeroDevEthersProvider<'ECDSA'> - - // before(async () => { - // // TestContractHandler.setConfig(config) - - // // const projectId = process.env.PROJECT_ID! - // // const owner = ethers.Wallet.createRandom() - - // // zerodevProvider = await ZeroDevEthersProvider.init('ECDSA', { - // // projectId, - // // owner: convertEthersV6SignerToAccountSigner(owner), - // // }) - - // // nevermined = await Nevermined.getInstance(config, { - // // loadCore: true, - // // loadServiceAgreements: true, - // // loadNFTs1155: true, - // // loadNFTs721: false, - // // loadDispenser: true, - // // loadERC20Token: true, - // // loadAccessFlow: false, - // // loadDIDTransferFlow: false, - // // loadRewards: false, - // // loadRoyalties: true, - // // loadCompute: false, - // // }) - // // ;[, publisher] = await nevermined.accounts.list() - - // // const clientAssertion = await nevermined.utils.jwt.generateClientAssertion(publisher) - - // // await nevermined.services.marketplace.login(clientAssertion) - // // payload = decodeJwt(config.marketplaceAuthToken) - - // }) - // it('I want to search content from the app', async () => { - - // const nvmApp = await NvmApp.getInstance(NVMAppEnvironments.Local) - // // const wallet = ethers.Wallet.createRandom() - // // nvmApp.connect(await wallet.getAddress()) - // const results = await nvmApp.search.query({}) - // console.log(JSON.stringify(results, null, 2)) - // // nvmApp.subscriptions.createTimeSubscription() - // // nvmApp.subscriptions.createCreditsSubscription() - // // nvmApp.assets.createAgent() - // // nvmApp.assets.createDataset() - // }) - - // it.skip('I want to connect my account', async () => { - // }) - - // it.skip('Overwrite the default config with some parameters', async () => { - // }) - - // it.skip('Download the configuration', async () => { - // }) - - // it.skip('I want to create a time subscription', async () => { - // }) - - // it.skip('I want to create a credits subscription', async () => { - // }) - - // it.skip('I want to create an Agent', async () => { - // }) - - // it.skip('I want to create a Dataset', async () => { - // }) - - // it.skip('I want to order a subscription', async () => { - // }) - - // it.skip('I want to access a remote service', async () => { - // }) - - // it.skip('I want to download a file asset', async () => { - // }) - - // }) }) diff --git a/package.json b/package.json index 91eea383d..de93e9a2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nevermined-io/sdk", - "version": "2.1.3", + "version": "2.2.0-rc0", "description": "Javascript SDK for connecting with Nevermined Data Platform ", "main": "./dist/node/sdk.js", "typings": "./dist/node/sdk.d.ts", diff --git a/src/keeper/contracts/templates/AgreementTemplate.abstract.ts b/src/keeper/contracts/templates/AgreementTemplate.abstract.ts index 3ef0ce960..690c399e3 100644 --- a/src/keeper/contracts/templates/AgreementTemplate.abstract.ts +++ b/src/keeper/contracts/templates/AgreementTemplate.abstract.ts @@ -286,6 +286,7 @@ export abstract class AgreementTemplate extends ContractBase { timeouts.push(condition.timeout) timelocks.push(condition.timelock) }) + console.log(`-- Ordering with price: ${assetPrice.toString()}`) observer(OrderProgressStep.ApprovingPayment) diff --git a/src/models/AssetPrice.ts b/src/models/AssetPrice.ts index 6214f324e..838985fa9 100644 --- a/src/models/AssetPrice.ts +++ b/src/models/AssetPrice.ts @@ -13,7 +13,7 @@ export class AssetPrice { public constructor(address: string, amount: bigint, tokenAddress: string) public constructor(..._params: any[]) { this.totalPrice = 0n - this.rewards = new Map() + this.rewards = new Map() if (_params.length === 1) { const [rewards] = _params @@ -22,10 +22,12 @@ export class AssetPrice { } else if (_params.length === 2) { const [address, amount] = _params this.rewards.set(address, amount) + // this.rewards[address] = amount this.totalPrice = amount } else if (_params.length === 3) { const [address, amount, tokenAddress] = _params this.rewards.set(address, amount) + // this.rewards[address] = amount this.totalPrice = amount this.tokenAddress = tokenAddress } @@ -52,8 +54,10 @@ export class AssetPrice { } public setReceiver(receiver: string, amount: bigint): AssetPrice { - this.rewards.set(receiver, amount) - this.totalPrice = AssetPrice.sumAmounts(this.getAmounts()) + if (amount > 0) { + this.rewards.set(receiver, amount) + this.totalPrice = AssetPrice.sumAmounts(this.getAmounts()) + } return this } @@ -73,10 +77,8 @@ export class AssetPrice { * @returns the asset rewards object */ public addNetworkFees(feeReceiver: string, networkFeePercent: bigint): AssetPrice { - return this.setReceiver( - feeReceiver, - (this.totalPrice * networkFeePercent) / AssetPrice.NETWORK_FEE_DENOMINATOR / 100n, - ) + const amount = (this.totalPrice * networkFeePercent) / AssetPrice.NETWORK_FEE_DENOMINATOR / 100n + return this.setReceiver(feeReceiver, amount) } /** @@ -89,13 +91,15 @@ export class AssetPrice { const feesToInclude = (this.totalPrice * networkFeePercent) / AssetPrice.NETWORK_FEE_DENOMINATOR / 100n - const newRewards: Map = new Map() - this.rewards.forEach((k, v) => { - newRewards.set(v, k - (k * networkFeePercent) / AssetPrice.NETWORK_FEE_DENOMINATOR / 100n) - }) - newRewards.set(feeReceiver, feesToInclude) - this.rewards = newRewards - this.totalPrice = AssetPrice.sumAmounts(this.getAmounts()) + if (feesToInclude > 0) { + const newRewards: Map = new Map() + this.rewards.forEach((k, v) => { + newRewards.set(v, k - (k * networkFeePercent) / AssetPrice.NETWORK_FEE_DENOMINATOR / 100n) + }) + newRewards.set(feeReceiver, feesToInclude) + this.rewards = newRewards + this.totalPrice = AssetPrice.sumAmounts(this.getAmounts()) + } return this } @@ -112,6 +116,13 @@ export class AssetPrice { } public toString(): string { - return this.getAmountsString() + this.getReceiversString() + return JSON.stringify({ + rewards: { + amounts: this.getAmounts(), + receivers: this.getReceivers(), + }, + totalPrice: this.totalPrice, + token: this.tokenAddress, + }) } } diff --git a/src/nevermined/NvmApp.ts b/src/nevermined/NvmApp.ts index a03b8324e..dc8d50ce1 100644 --- a/src/nevermined/NvmApp.ts +++ b/src/nevermined/NvmApp.ts @@ -37,6 +37,12 @@ export enum NVMAppEnvironments { Local = 'local', Custom = 'custom', } + +export enum NVMAppSubscriptionType { + Time = 'time', + Credits = 'credits', +} + export interface MetadataValidationResults { isValid: boolean messages: string[] @@ -55,7 +61,10 @@ export class NvmApp { private useZeroDevSigner: boolean = false private zeroDevSignerAccount?: ZeroDevAccountSigner<'ECDSA'> private assetProviders: string[] = [] + private loginCredentials: string | undefined private subscriptionNFTContractAddress: string | undefined + private networkFeeReceiver: string | undefined + private networkFee: bigint | undefined static readonly defaultAppInitializatioinOptions: NeverminedInitializationOptions = { loadCore: true, @@ -78,7 +87,7 @@ export class NvmApp { public static async getInstance( appEnv: NVMAppEnvironments, - config?: NeverminedOptions, + config?: Partial, ): Promise { const defaultEnvConfig = this.getConfigFromTagName(appEnv) const mergedConfig = config ? { ...defaultEnvConfig, ...config } : defaultEnvConfig @@ -116,7 +125,8 @@ export class NvmApp { } const clientAssertion = await this.fullSDK.utils.jwt.generateClientAssertion(this.userAccount) - this.fullSDK.services.marketplace.login(clientAssertion) + + this.loginCredentials = await this.fullSDK.services.marketplace.login(clientAssertion) const nodeInfo = await this.fullSDK.services.node.getNeverminedNodeInfo() this.assetProviders = [nodeInfo['provider-address']] @@ -134,6 +144,9 @@ export class NvmApp { if (!isAddress(this.subscriptionNFTContractAddress)) { throw new Web3Error('Invalid Subscription NFT contract address') } + + this.networkFeeReceiver = await this.fullSDK.keeper.nvmConfig.getFeeReceiver() + this.networkFee = await this.fullSDK.keeper.nvmConfig.getNetworkFee() } public async disconnect() { @@ -142,6 +155,7 @@ export class NvmApp { this.userAccount = undefined this.useZeroDevSigner = false this.zeroDevSignerAccount = undefined + this.loginCredentials = undefined } } @@ -149,6 +163,10 @@ export class NvmApp { return this.fullSDK ? this.fullSDK.isKeeperConnected : false } + public getLoginCredentials(): string | undefined { + return this.loginCredentials + } + public get config(): NeverminedOptions { return this.configNVM } @@ -157,6 +175,10 @@ export class NvmApp { return this.searchSDK.search } + public get networkFees(): { receiver: string; fee: bigint } { + return { receiver: this.networkFeeReceiver, fee: this.networkFee } + } + public async createTimeSubscription( susbcriptionMetadata: MetaData, subscriptionPrice: AssetPrice, @@ -165,7 +187,11 @@ export class NvmApp { if (!this.isWeb3Connected()) throw new Web3Error('Web3 not connected, try calling the connect method first') - const validationResult = NvmApp.validateTimeSubscriptionMetadata(susbcriptionMetadata) + const validationResult = this.validateSubscription( + susbcriptionMetadata, + subscriptionPrice, + NVMAppSubscriptionType.Time, + ) if (!validationResult.isValid) { throw new Error(validationResult.messages.join(',')) } @@ -205,7 +231,11 @@ export class NvmApp { if (!this.isWeb3Connected()) throw new Web3Error('Web3 not connected, try calling the connect method first') - const validationResult = NvmApp.validateCreditsSubscriptionMetadata(susbcriptionMetadata) + const validationResult = this.validateSubscription( + susbcriptionMetadata, + subscriptionPrice, + NVMAppSubscriptionType.Credits, + ) if (!validationResult.isValid) { throw new Error(validationResult.messages.join(',')) } @@ -257,6 +287,7 @@ export class NvmApp { numberCredits, this.userAccount, serviceIndex, + { ...(this.useZeroDevSigner && { zeroDevSigner: this.zeroDevSignerAccount }) }, ) const subscriptionOwner = await this.fullSDK.assets.owner(subscriptionDid) transferResult = await this.fullSDK.nfts1155.claim( @@ -310,7 +341,7 @@ export class NvmApp { if (!this.isWeb3Connected()) throw new Web3Error('Web3 not connected, try calling the connect method first') - const validationResult = NvmApp.validateServiceAssetMetadata(metadata) + const validationResult = this.validateServiceAssetMetadata(metadata) if (!validationResult.isValid) { throw new Error(validationResult.messages.join(',')) } @@ -350,7 +381,7 @@ export class NvmApp { if (!this.isWeb3Connected()) throw new Web3Error('Web3 not connected, try calling the connect method first') - const validationResult = NvmApp.validateFileAssetMetadata(metadata) + const validationResult = this.validateFileAssetMetadata(metadata) if (!validationResult.isValid) { throw new Error(validationResult.messages.join(',')) } @@ -380,31 +411,58 @@ export class NvmApp { ) } - // TODO: Implement subscription validations - public static validateTimeSubscriptionMetadata( - _susbcriptionMetadata: MetaData, - ): MetadataValidationResults { - return { isValid: true, messages: [] } + public addNetworkFee(price: AssetPrice): AssetPrice { + if (!this.isNetworkFeeIncluded(price)) { + return price.adjustToIncludeNetworkFees(this.networkFeeReceiver, this.networkFee) + } + return price + } + + public isNetworkFeeIncluded(price: AssetPrice): boolean { + // If there are no network fees everything is okay + if (this.networkFee === 0n || price.getTotalPrice() === 0n) return true + if (!price.getRewards().has(this.networkFeeReceiver)) return false + + const networkFee = price.getRewards().get(this.networkFeeReceiver) + const expectedFee = + (price.getTotalPrice() * this.networkFee) / AssetPrice.NETWORK_FEE_DENOMINATOR / 100n + if (networkFee !== expectedFee) return false + return true } // TODO: Implement subscription validations - public static validateCreditsSubscriptionMetadata( - _susbcriptionMetadata: MetaData, + public validateSubscription( + metadata: MetaData, + price: AssetPrice, + subscriptionType: NVMAppSubscriptionType, ): MetadataValidationResults { + const errorMessages: string[] = [] + if (!this.isNetworkFeeIncluded(price)) errorMessages.push('Network fee not included') + if (!metadata.additionalInformation?.customData) errorMessages.push('Custom Data not included') + if (!metadata.additionalInformation?.customData?.subscriptionLimitType) + errorMessages.push('customData.subscriptionLimitType not included') + if ( + metadata.additionalInformation?.customData?.subscriptionLimitType !== + subscriptionType.toString() + ) + errorMessages.push('invalid customData.subscriptionLimitType value') + + if (subscriptionType === NVMAppSubscriptionType.Time) { + if (!metadata.additionalInformation?.customData?.dateMeasure) + errorMessages.push('customData.dateMeasure not included') + } + + if (errorMessages.length > 0) return { isValid: false, messages: errorMessages } return { isValid: true, messages: [] } } // TODO: Implement subscription validations - public static validateServiceAssetMetadata( - _susbcriptionMetadata: MetaData, - ): MetadataValidationResults { + public validateServiceAssetMetadata(_susbcriptionMetadata: MetaData): MetadataValidationResults { return { isValid: true, messages: [] } } // TODO: Implement subscription validations - public static validateFileAssetMetadata( - _susbcriptionMetadata: MetaData, - ): MetadataValidationResults { + public validateFileAssetMetadata(_susbcriptionMetadata: MetaData): MetadataValidationResults { return { isValid: true, messages: [] } } @@ -427,8 +485,10 @@ export class NvmApp { return new AppDeploymentMumbai() case NVMAppEnvironments.Gnosis: return new AppDeploymentGnosis() - default: + case NVMAppEnvironments.Local: return new AppDeploymentLocal() + default: + throw new Error('Invalid environment') } } } From 205a47ef50712d26a297cbc71c876d74a972f752 Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Wed, 14 Feb 2024 11:00:24 +0100 Subject: [PATCH 3/7] feat: nvm app metadata helper --- src/ddo/NvmAppMetadata.ts | 135 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/ddo/NvmAppMetadata.ts diff --git a/src/ddo/NvmAppMetadata.ts b/src/ddo/NvmAppMetadata.ts new file mode 100644 index 000000000..72c282df9 --- /dev/null +++ b/src/ddo/NvmAppMetadata.ts @@ -0,0 +1,135 @@ +import { MetadataValidationResults, NVMAppSubscriptionType } from '../nevermined/NvmApp' +import { MetaData, MetaDataMain, ResourceAuthentication } from './types' + +export class NvmAppMetadata { + public static getTimeSubscriptionMetadataTemplate( + name: string, + author: string, + dateMeasure: string, + ): MetaData { + const metadata = NvmAppMetadata.getSubscriptionMetadataTemplate(name, author) + metadata.additionalInformation.customData = { + subscriptionLimitType: NVMAppSubscriptionType.Time, + dateMeasure, + } + return metadata + } + + public static getCreditsSubscriptionMetadataTemplate(name: string, author: string): MetaData { + const metadata = NvmAppMetadata.getSubscriptionMetadataTemplate(name, author) + metadata.additionalInformation.customData = { + subscriptionLimitType: NVMAppSubscriptionType.Credits, + } + return metadata + } + + public static getSubscriptionMetadataTemplate(name: string, author: string): MetaData { + const _metadata = { + main: { + name, + type: 'subscription', + dateCreated: new Date().toISOString().replace(/\.[0-9]{3}/, ''), + datePublished: new Date().toISOString().replace(/\.[0-9]{3}/, ''), + author, + license: '', + files: [], + paymentAttributes: [ + { + paymentType: 'serviceAgreements', + paymentEnabled: true, + }, + ], + } as MetaDataMain, + additionalInformation: {}, + } + + return _metadata + } + + public static getServiceMetadataTemplate( + name: string, + author: string, + endpoints: { [verb: string]: string }[], + openEndpoints: string[], + openApiEndpoint: string | undefined, + serviceType: string = 'RESTful', + authType: ResourceAuthentication['type'], + authToken?: string, + authUser?: string, + authPassword?: string, + nonce: string | number = Math.random(), + ): MetaData { + const serviceMetadata = { + main: { + name, + type: 'service', + dateCreated: new Date().toISOString().replace(/\.[0-9]{3}/, ''), + datePublished: new Date().toISOString().replace(/\.[0-9]{3}/, ''), + author, + license: '', + files: [], + webService: { + type: serviceType, + endpoints, + openEndpoints, + internalAttributes: {}, + }, + ...({ nonce } as any), + }, + additionalInformation: { + customData: {}, + }, + } + + serviceMetadata.main.webService.endpoints[0] = { '(.*)': endpoint } + + if (authType === 'basic') { + serviceMetadata.main.webService.internalAttributes.authentication = { + type: 'basic', + username: authUser, + password: authPassword, + } + } else if (authType === 'oauth') { + serviceMetadata.main.webService.internalAttributes.authentication = { + type: 'oauth', + token: authToken, + } + serviceMetadata.main.webService.internalAttributes.headers = [ + { Authorization: `Bearer ${authToken}` }, + ] + } else { + serviceMetadata.main.webService.internalAttributes.authentication = { + type: 'none', + } + } + + if (openEndpoints) { + serviceMetadata.main.webService.openEndpoints = openEndpoints + } + return serviceMetadata + } + + public static validateSubscription( + metadata: MetaData, + subscriptionType: NVMAppSubscriptionType, + ): MetadataValidationResults { + const errorMessages: string[] = [] + + if (!metadata.additionalInformation?.customData) errorMessages.push('Custom Data not included') + if (!metadata.additionalInformation?.customData?.subscriptionLimitType) + errorMessages.push('customData.subscriptionLimitType not included') + if ( + metadata.additionalInformation?.customData?.subscriptionLimitType !== + subscriptionType.toString() + ) + errorMessages.push('invalid customData.subscriptionLimitType value') + + if (subscriptionType === NVMAppSubscriptionType.Time) { + if (!metadata.additionalInformation?.customData?.dateMeasure) + errorMessages.push('customData.dateMeasure not included') + } + + if (errorMessages.length > 0) return { isValid: false, messages: errorMessages } + return { isValid: true, messages: [] } + } +} From 74a2f9b76964c5419ecfe381355bdfbaf5f5edcc Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:45:25 +0100 Subject: [PATCH 4/7] feat: metadata support --- .../external/NVMAppAPI.staging.test.ts | 37 +++++++++---- integration/nevermined/NVMAppAPI.test.ts | 30 +++++++--- src/ddo/NvmAppMetadata.ts | 55 ++++++++++++++----- src/ddo/types.ts | 13 +++++ src/nevermined/NvmApp.ts | 14 ++--- 5 files changed, 106 insertions(+), 43 deletions(-) diff --git a/integration/external/NVMAppAPI.staging.test.ts b/integration/external/NVMAppAPI.staging.test.ts index 06097a052..6f4999604 100644 --- a/integration/external/NVMAppAPI.staging.test.ts +++ b/integration/external/NVMAppAPI.staging.test.ts @@ -3,15 +3,15 @@ import chaiAsPromised from 'chai-as-promised' import { AssetPrice, - MetaData, ResourceAuthentication, + SubscriptionType, convertEthersV6SignerToAccountSigner, makeAccounts, } from '../../src' import TestContractHandler from '../../test/keeper/TestContractHandler' -import { NVMAppEnvironments, NVMAppSubscriptionType, NvmApp } from '../../src/nevermined/NvmApp' +import { NVMAppEnvironments, NvmApp } from '../../src/nevermined/NvmApp' +import { NvmAppMetadata } from '../../src/ddo/NvmAppMetadata' import { ethers, isAddress } from 'ethers' -import { generateSubscriptionMetadata, generateWebServiceMetadata, getMetadata } from '../utils' import { ZeroDevAccountSigner, ZeroDevEthersProvider } from '@zerodev/sdk' import { AppDeploymentStaging } from '../../src/nevermined/resources/AppNetworks' @@ -124,11 +124,13 @@ describe('NVM App API', () => { }) it('I want to create a time subscription', async () => { - const timeSubscriptionMetadata = generateSubscriptionMetadata( + const timeSubscriptionMetadata = NvmAppMetadata.getTimeSubscriptionMetadataTemplate( 'NVM App Time only Subscription test', + 'Nevermined', + 'hours', ) timeSubscriptionMetadata.additionalInformation.customData = { - subscriptionLimitType: NVMAppSubscriptionType.Time, + subscriptionLimitType: SubscriptionType.Time, dateMeasure: 'hours', } @@ -144,11 +146,12 @@ describe('NVM App API', () => { }) it('I want to create a credits subscription', async () => { - const creditsSubscriptionMetadata = generateSubscriptionMetadata( + const creditsSubscriptionMetadata = NvmAppMetadata.getCreditsSubscriptionMetadataTemplate( 'NVM App Credits Subscription test', + 'Nevermined', ) creditsSubscriptionMetadata.additionalInformation.customData = { - subscriptionLimitType: NVMAppSubscriptionType.Credits, + subscriptionLimitType: SubscriptionType.Credits, } const ddo = await nvmAppPublisher.createCreditsSubscription( @@ -164,15 +167,22 @@ describe('NVM App API', () => { }) it('I want to register an Agent', async () => { - const agentMetadata = generateWebServiceMetadata( - 'Nevermined Web Service Metadata', - `${SERVICE_ENDPOINT}(.*)`, + const agentMetadata = NvmAppMetadata.getServiceMetadataTemplate( + 'Nevermined Ageeeent', + 'Nevermined', + [ + { + GET: `${SERVICE_ENDPOINT}(.*)`, + }, + ], [OPEN_ENDPOINT], + OPEN_ENDPOINT, + 'RESTful', AUTHORIZATION_TYPE, AUTHORIZATION_TOKEN, AUTHORIZATION_USER, AUTHORIZATION_PASSWORD, - ) as MetaData + ) const ddo = await nvmAppPublisher.registerServiceAsset( agentMetadata, @@ -191,7 +201,10 @@ describe('NVM App API', () => { }) it('I want to register a Dataset', async () => { - const datasetMetadata = getMetadata() + const datasetMetadata = NvmAppMetadata.getFileMetadataTemplate( + 'NVM App Dataset test', + 'Nevermined', + ) const ddo = await nvmAppPublisher.registerFileAsset( datasetMetadata, diff --git a/integration/nevermined/NVMAppAPI.test.ts b/integration/nevermined/NVMAppAPI.test.ts index dde971903..c512834aa 100644 --- a/integration/nevermined/NVMAppAPI.test.ts +++ b/integration/nevermined/NVMAppAPI.test.ts @@ -4,7 +4,6 @@ import chaiAsPromised from 'chai-as-promised' import { Account, AssetPrice, - MetaData, Nevermined, ResourceAuthentication, SubscriptionCreditsNFTApi, @@ -14,7 +13,7 @@ import { config } from '../config' import TestContractHandler from '../../test/keeper/TestContractHandler' import { NVMAppEnvironments, NvmApp } from '../../src/nevermined/NvmApp' import { Signer } from 'ethers' -import { generateSubscriptionMetadata, generateWebServiceMetadata, getMetadata } from '../utils' +import { NvmAppMetadata } from '../../src/ddo/NvmAppMetadata' chai.use(chaiAsPromised) @@ -121,8 +120,10 @@ describe('NVM App API', () => { }) it('I want to create a time subscription', async () => { - const timeSubscriptionMetadata = generateSubscriptionMetadata( + const timeSubscriptionMetadata = NvmAppMetadata.getTimeSubscriptionMetadataTemplate( 'NVM App Time only Subscription test', + 'Nevermined', + 'hours', ) const ddo = await nvmApp.createTimeSubscription( @@ -137,8 +138,9 @@ describe('NVM App API', () => { }) it('I want to create a credits subscription', async () => { - const creditsSubscriptionMetadata = generateSubscriptionMetadata( + const creditsSubscriptionMetadata = NvmAppMetadata.getCreditsSubscriptionMetadataTemplate( 'NVM App Credits Subscription test', + 'Nevermined', ) const ddo = await nvmApp.createCreditsSubscription( @@ -154,15 +156,22 @@ describe('NVM App API', () => { }) it('I want to register an Agent', async () => { - const agentMetadata = generateWebServiceMetadata( - 'Nevermined Web Service Metadata', - `${SERVICE_ENDPOINT}(.*)`, + const agentMetadata = NvmAppMetadata.getServiceMetadataTemplate( + 'Nevermined Ageeeent', + 'Nevermined', + [ + { + GET: `${SERVICE_ENDPOINT}(.*)`, + }, + ], [OPEN_ENDPOINT], + OPEN_ENDPOINT, + 'RESTful', AUTHORIZATION_TYPE, AUTHORIZATION_TOKEN, AUTHORIZATION_USER, AUTHORIZATION_PASSWORD, - ) as MetaData + ) const ddo = await nvmApp.registerServiceAsset( agentMetadata, @@ -181,7 +190,10 @@ describe('NVM App API', () => { }) it('I want to register a Dataset', async () => { - const datasetMetadata = getMetadata() + const datasetMetadata = NvmAppMetadata.getFileMetadataTemplate( + 'NVM App Dataset test', + 'Nevermined', + ) const ddo = await nvmApp.registerFileAsset( datasetMetadata, diff --git a/src/ddo/NvmAppMetadata.ts b/src/ddo/NvmAppMetadata.ts index 72c282df9..63a7b1dc5 100644 --- a/src/ddo/NvmAppMetadata.ts +++ b/src/ddo/NvmAppMetadata.ts @@ -1,24 +1,31 @@ -import { MetadataValidationResults, NVMAppSubscriptionType } from '../nevermined/NvmApp' -import { MetaData, MetaDataMain, ResourceAuthentication } from './types' +import { MetadataValidationResults } from '../nevermined/NvmApp' +import { MetaData, MetaDataMain, ResourceAuthentication, SubscriptionType } from './types' export class NvmAppMetadata { public static getTimeSubscriptionMetadataTemplate( name: string, author: string, - dateMeasure: string, + timeMeasure: string, ): MetaData { const metadata = NvmAppMetadata.getSubscriptionMetadataTemplate(name, author) + metadata.main.subscription = { + subscriptionType: SubscriptionType.Time, + timeMeasure, + } metadata.additionalInformation.customData = { - subscriptionLimitType: NVMAppSubscriptionType.Time, - dateMeasure, + subscriptionLimitType: SubscriptionType.Time, + dateMeasure: timeMeasure, } return metadata } public static getCreditsSubscriptionMetadataTemplate(name: string, author: string): MetaData { const metadata = NvmAppMetadata.getSubscriptionMetadataTemplate(name, author) + metadata.main.subscription = { + subscriptionType: SubscriptionType.Credits, + } metadata.additionalInformation.customData = { - subscriptionLimitType: NVMAppSubscriptionType.Credits, + subscriptionLimitType: SubscriptionType.Credits, } return metadata } @@ -80,8 +87,15 @@ export class NvmAppMetadata { customData: {}, }, } - - serviceMetadata.main.webService.endpoints[0] = { '(.*)': endpoint } + if (openApiEndpoint) { + serviceMetadata.main.webService.openEndpoints.push(openApiEndpoint) + serviceMetadata.main.additionalInformation = { + ...serviceMetadata.main.additionalInformation, + customData: { + openApi: openApiEndpoint, + }, + } + } if (authType === 'basic') { serviceMetadata.main.webService.internalAttributes.authentication = { @@ -103,15 +117,30 @@ export class NvmAppMetadata { } } - if (openEndpoints) { - serviceMetadata.main.webService.openEndpoints = openEndpoints - } return serviceMetadata } + public static getFileMetadataTemplate(name: string, author: string): MetaData { + const _metadata = { + main: { + name, + type: 'dataset', + dateCreated: new Date().toISOString().replace(/\.[0-9]{3}/, ''), + datePublished: new Date().toISOString().replace(/\.[0-9]{3}/, ''), + author, + license: '', + files: [], + paymentAttributes: [], + } as MetaDataMain, + additionalInformation: {}, + } + + return _metadata + } + public static validateSubscription( metadata: MetaData, - subscriptionType: NVMAppSubscriptionType, + subscriptionType: SubscriptionType, ): MetadataValidationResults { const errorMessages: string[] = [] @@ -124,7 +153,7 @@ export class NvmAppMetadata { ) errorMessages.push('invalid customData.subscriptionLimitType value') - if (subscriptionType === NVMAppSubscriptionType.Time) { + if (subscriptionType === SubscriptionType.Time) { if (!metadata.additionalInformation?.customData?.dateMeasure) errorMessages.push('customData.dateMeasure not included') } diff --git a/src/ddo/types.ts b/src/ddo/types.ts index 799d16967..1862bda45 100644 --- a/src/ddo/types.ts +++ b/src/ddo/types.ts @@ -209,6 +209,17 @@ export interface WebService { encryptedAttributes?: string } +export enum SubscriptionType { + Time = 'time', + Credits = 'credits', + Both = 'both', +} + +export interface SubscriptionMetadata { + subscriptionType: SubscriptionType + timeMeasure?: string +} + export interface WebServiceInternalAttributes { authentication?: ResourceAuthentication @@ -291,6 +302,8 @@ export interface MetaDataMain { */ files?: MetaDataExternalResource[] + subscription?: SubscriptionMetadata + webService?: WebService encryptedService?: any diff --git a/src/nevermined/NvmApp.ts b/src/nevermined/NvmApp.ts index dc8d50ce1..5ecc1d19c 100644 --- a/src/nevermined/NvmApp.ts +++ b/src/nevermined/NvmApp.ts @@ -13,6 +13,7 @@ import { PublishOnChainOptions, SearchApi, SubscriptionToken, + SubscriptionType, Web3Error, } from '../sdk' import { @@ -38,11 +39,6 @@ export enum NVMAppEnvironments { Custom = 'custom', } -export enum NVMAppSubscriptionType { - Time = 'time', - Credits = 'credits', -} - export interface MetadataValidationResults { isValid: boolean messages: string[] @@ -190,7 +186,7 @@ export class NvmApp { const validationResult = this.validateSubscription( susbcriptionMetadata, subscriptionPrice, - NVMAppSubscriptionType.Time, + SubscriptionType.Time, ) if (!validationResult.isValid) { throw new Error(validationResult.messages.join(',')) @@ -234,7 +230,7 @@ export class NvmApp { const validationResult = this.validateSubscription( susbcriptionMetadata, subscriptionPrice, - NVMAppSubscriptionType.Credits, + SubscriptionType.Credits, ) if (!validationResult.isValid) { throw new Error(validationResult.messages.join(',')) @@ -434,7 +430,7 @@ export class NvmApp { public validateSubscription( metadata: MetaData, price: AssetPrice, - subscriptionType: NVMAppSubscriptionType, + subscriptionType: SubscriptionType, ): MetadataValidationResults { const errorMessages: string[] = [] if (!this.isNetworkFeeIncluded(price)) errorMessages.push('Network fee not included') @@ -447,7 +443,7 @@ export class NvmApp { ) errorMessages.push('invalid customData.subscriptionLimitType value') - if (subscriptionType === NVMAppSubscriptionType.Time) { + if (subscriptionType === SubscriptionType.Time) { if (!metadata.additionalInformation?.customData?.dateMeasure) errorMessages.push('customData.dateMeasure not included') } From 97313fd522dd468afa088f48f880a4f3cea602e0 Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:05:58 +0100 Subject: [PATCH 5/7] chore: comments --- src/keeper/contracts/templates/AgreementTemplate.abstract.ts | 1 - src/models/AssetPrice.ts | 2 -- src/nevermined/resources/AppNetworks.ts | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/keeper/contracts/templates/AgreementTemplate.abstract.ts b/src/keeper/contracts/templates/AgreementTemplate.abstract.ts index 690c399e3..3ef0ce960 100644 --- a/src/keeper/contracts/templates/AgreementTemplate.abstract.ts +++ b/src/keeper/contracts/templates/AgreementTemplate.abstract.ts @@ -286,7 +286,6 @@ export abstract class AgreementTemplate extends ContractBase { timeouts.push(condition.timeout) timelocks.push(condition.timelock) }) - console.log(`-- Ordering with price: ${assetPrice.toString()}`) observer(OrderProgressStep.ApprovingPayment) diff --git a/src/models/AssetPrice.ts b/src/models/AssetPrice.ts index 838985fa9..5abddbfe0 100644 --- a/src/models/AssetPrice.ts +++ b/src/models/AssetPrice.ts @@ -22,12 +22,10 @@ export class AssetPrice { } else if (_params.length === 2) { const [address, amount] = _params this.rewards.set(address, amount) - // this.rewards[address] = amount this.totalPrice = amount } else if (_params.length === 3) { const [address, amount, tokenAddress] = _params this.rewards.set(address, amount) - // this.rewards[address] = amount this.totalPrice = amount this.tokenAddress = tokenAddress } diff --git a/src/nevermined/resources/AppNetworks.ts b/src/nevermined/resources/AppNetworks.ts index 571488415..919f731fc 100644 --- a/src/nevermined/resources/AppNetworks.ts +++ b/src/nevermined/resources/AppNetworks.ts @@ -10,7 +10,7 @@ export class AppDeploymentLocal extends NeverminedAppOptions { instanceName = 'localnet' web3ProviderUri = 'http://contracts.nevermined.localnet' marketplaceUri = 'http://marketplace.nevermined.localnet' - graphHttpUri = 'http://localhost:9000/subgraphs/name/nevermined-io/development' + graphHttpUri = undefined neverminedNodeUri = 'http://node.nevermined.localnet' neverminedNodeAddress = '0x068ed00cf0441e4829d9784fcbe7b9e26d4bd8d0' verbose = true From 5067ec81c1204c1750e1d07eeafa847f085aa430 Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:58:25 +0100 Subject: [PATCH 6/7] test: integrating with the nvm app --- integration/external/NVMAppAPI.staging.test.ts | 16 +++++++++++++++- integration/nevermined/NVMAppAPI.test.ts | 12 ++++++++++++ src/ddo/NvmAppMetadata.ts | 7 ++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/integration/external/NVMAppAPI.staging.test.ts b/integration/external/NVMAppAPI.staging.test.ts index 6f4999604..1e9bd436d 100644 --- a/integration/external/NVMAppAPI.staging.test.ts +++ b/integration/external/NVMAppAPI.staging.test.ts @@ -172,7 +172,7 @@ describe('NVM App API', () => { 'Nevermined', [ { - GET: `${SERVICE_ENDPOINT}(.*)`, + GET: `${SERVICE_ENDPOINT}/(.*)`, }, ], [OPEN_ENDPOINT], @@ -205,6 +205,20 @@ describe('NVM App API', () => { 'NVM App Dataset test', 'Nevermined', ) + datasetMetadata.main.files = [ + { + index: 0, + contentType: 'application/json', + name: 'ddo-example.json', + url: 'https://storage.googleapis.com/nvm-static-assets/files/ci/ddo-example.json', + }, + { + index: 1, + contentType: 'text/plain', + name: 'README.md', + url: 'https://storage.googleapis.com/nvm-static-assets/files/ci/README.md', + }, + ] const ddo = await nvmAppPublisher.registerFileAsset( datasetMetadata, diff --git a/integration/nevermined/NVMAppAPI.test.ts b/integration/nevermined/NVMAppAPI.test.ts index c512834aa..10a003348 100644 --- a/integration/nevermined/NVMAppAPI.test.ts +++ b/integration/nevermined/NVMAppAPI.test.ts @@ -194,6 +194,18 @@ describe('NVM App API', () => { 'NVM App Dataset test', 'Nevermined', ) + datasetMetadata.main.files = [ + { + index: 0, + contentType: 'application/json', + url: 'https://storage.googleapis.com/nvm-static-assets/files/ci/ddo-example.json', + }, + { + index: 1, + contentType: 'text/plain', + url: 'https://storage.googleapis.com/nvm-static-assets/files/ci/README.md', + }, + ] const ddo = await nvmApp.registerFileAsset( datasetMetadata, diff --git a/src/ddo/NvmAppMetadata.ts b/src/ddo/NvmAppMetadata.ts index 63a7b1dc5..d278492a5 100644 --- a/src/ddo/NvmAppMetadata.ts +++ b/src/ddo/NvmAppMetadata.ts @@ -132,7 +132,12 @@ export class NvmAppMetadata { files: [], paymentAttributes: [], } as MetaDataMain, - additionalInformation: {}, + additionalInformation: { + customData: { + dataSchema: '', + filesFormat: '', + }, + }, } return _metadata From 1f296b7cafd7671c8fc9ba8ab3691d3137a981f4 Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Wed, 14 Feb 2024 18:30:18 +0100 Subject: [PATCH 7/7] feat: adding metadata attribute for flagging charge type --- integration/external/NVMAppAPI.staging.test.ts | 1 + integration/nevermined/NVMAppAPI.test.ts | 1 + src/ddo/NvmAppMetadata.ts | 10 +++++++++- src/ddo/types.ts | 7 +++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/integration/external/NVMAppAPI.staging.test.ts b/integration/external/NVMAppAPI.staging.test.ts index 1e9bd436d..bc2ec4c71 100644 --- a/integration/external/NVMAppAPI.staging.test.ts +++ b/integration/external/NVMAppAPI.staging.test.ts @@ -182,6 +182,7 @@ describe('NVM App API', () => { AUTHORIZATION_TOKEN, AUTHORIZATION_USER, AUTHORIZATION_PASSWORD, + true, ) const ddo = await nvmAppPublisher.registerServiceAsset( diff --git a/integration/nevermined/NVMAppAPI.test.ts b/integration/nevermined/NVMAppAPI.test.ts index 10a003348..5c3239d79 100644 --- a/integration/nevermined/NVMAppAPI.test.ts +++ b/integration/nevermined/NVMAppAPI.test.ts @@ -171,6 +171,7 @@ describe('NVM App API', () => { AUTHORIZATION_TOKEN, AUTHORIZATION_USER, AUTHORIZATION_PASSWORD, + true, ) const ddo = await nvmApp.registerServiceAsset( diff --git a/src/ddo/NvmAppMetadata.ts b/src/ddo/NvmAppMetadata.ts index d278492a5..fa98d1ec1 100644 --- a/src/ddo/NvmAppMetadata.ts +++ b/src/ddo/NvmAppMetadata.ts @@ -1,5 +1,11 @@ import { MetadataValidationResults } from '../nevermined/NvmApp' -import { MetaData, MetaDataMain, ResourceAuthentication, SubscriptionType } from './types' +import { + ChargeType, + MetaData, + MetaDataMain, + ResourceAuthentication, + SubscriptionType, +} from './types' export class NvmAppMetadata { public static getTimeSubscriptionMetadataTemplate( @@ -64,6 +70,7 @@ export class NvmAppMetadata { authToken?: string, authUser?: string, authPassword?: string, + isPriceDynamic: boolean = false, nonce: string | number = Math.random(), ): MetaData { const serviceMetadata = { @@ -80,6 +87,7 @@ export class NvmAppMetadata { endpoints, openEndpoints, internalAttributes: {}, + chargeType: isPriceDynamic ? ChargeType.Dynamic : ChargeType.Fixed, }, ...({ nonce } as any), }, diff --git a/src/ddo/types.ts b/src/ddo/types.ts index 1862bda45..944bb46ca 100644 --- a/src/ddo/types.ts +++ b/src/ddo/types.ts @@ -207,6 +207,8 @@ export interface WebService { internalAttributes?: WebServiceInternalAttributes encryptedAttributes?: string + + chargeType?: ChargeType } export enum SubscriptionType { @@ -215,6 +217,11 @@ export enum SubscriptionType { Both = 'both', } +export enum ChargeType { + Fixed = 'fixed', + Dynamic = 'dynamic', +} + export interface SubscriptionMetadata { subscriptionType: SubscriptionType timeMeasure?: string