1
0
mirror of https://github.com/oceanprotocol/ocean.js.git synced 2024-11-26 20:39:05 +01:00
ocean.js/test/integration/ComputeExamples.test.ts
2024-06-25 09:36:49 +01:00

714 lines
22 KiB
TypeScript

/// # Compute-to-Data (C2D) Code Examples
/// Here are the steps:
/// 0. [Prerequisites](#0-prerequisites)
/// 1. [Initialize services](#1-initialize-services)
/// 2. [Create a new node.js project](#2-create-a-new-nodejs-project-with-typescript)
/// 3. [Install dependencies](#3-install-dependencies)
/// 4. [Import dependencies and add variables and constants](#4-import-dependencies-and-add-variables-constants-and-helper-methods)
/// 5. [Initialize accounts and deploy contracts](#-initialize-accounts-and-deploy-contracts)
/// 6. [Publish a dataset and an algorithm](#6-publish-assets-dataset-and-algorithm)
/// 7. [Resolve published datasets and algorithms](#7-resolve-assets)
/// 8. [Send datatokens to consumer](#8-send-datatokens-to-consumer)
/// 9. [Consumer fetches compute environment](#9-get-compute-environments)
/// 10. [Consumer starts a compute job using a free C2D environment](#10-consumer-starts-a-compute-job)
/// 11. [Check compute status and get download compute results url](#11-check-compute-status-and-get-download-compute-results-url)
/// Let's go through each step.
/// ## 0. Prerequisites
/// Before we start it is important that you have all of the necessary prerequisites installed on your computer.
/// - **A Unix based operating system (Linux or Mac)**. If you are a Windows user you can try to run linux inside a virtual machine but this is outside of the scope of this article.
/// - **Git**. Instructions for installing Git can be found here: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
/// - **Node.js** can be downloaded from here: https://nodejs.org/en/download/
/// - **Docker** can be installed from here: https://docs.docker.com/get-docker/. Please note that Docker must run as a non-root user, you can set this up by following these instructions: https://docs.docker.com/engine/install/linux-postinstall/
/// ## 1. Initialize services
/// Ocean.js uses off-chain services for metadata (Aquarius) and consuming datasets (Provider).
/// We start by initializing the services. To do this, we clone the Barge repository and run it. This will run the current default versions of [Aquarius](https://github.com/oceanprotocol/aquarius), [Provider](https://github.com/oceanprotocol/provider), and [Ganache](https://github.com/trufflesuite/ganache) with [our contracts](https://github.com/oceanprotocol/contracts) deployed to it.
/// ```bash
/// git clone https://github.com/oceanprotocol/barge.git
/// cd barge/
/// ./start_ocean.sh --with-provider2 --no-dashboard --with-c2d
/// ```
/// ## 2. Create a new Node.js project with TypeScript
/// Start by creating a new Node.js project. Open a new terminal and enter the following commands:
/// ```bash
/// mkdir compute-quickstart
/// cd compute-quickstart
/// npm init
/// # Answer the questions in the command line prompt
/// touch compute.ts
/// # On linux press CTRL + D to save
/// ```
/// Next, we need to setup our TypeScript compiler options. Create a new file called `tsconfig.json` in the root of the `compute-quickstart` directory.
/// ```bash
/// touch tsconfig.json
/// # Copy the following json content into the file, On linux press CTRL + D to save
/// ```
/// ```json
/// {
/// "compilerOptions": {
/// "lib": ["es6", "es7"],
/// "module": "CommonJS",
/// "target": "ES5",
/// "esModuleInterop": true,
/// "allowSyntheticDefaultImports": true,
/// "outDir": "./dist/",
/// "declaration": true,
/// "declarationDir": "./dist/"
/// },
/// "include": [
/// "compute.ts"
/// ],
/// "exclude": [ "node_modules", "dist" ]
/// }
/// ```
/// Now you can compile your TypeScript project.
/// If you have TypeScript installed use the following command:
/// ```bash
/// tsc
/// ```
/// If you don't have TypeScript installed you can install it using the command below and then compile using the above command:
/// ```bash
/// npm install -g typescript
/// ```
/// Or if you don't want to install TypeScript use the following command to compile your file:
/// ```bash
/// npx tsc compute.ts
/// ```
/// To run your script as we go along, compile the script then you can use the following command:
/// ```bash
/// node dist/compute.js
/// ```
/// ## 3. Install dependencies
/// Install dependencies running the following command in your terminal:
/// ```bash
/// npm install @oceanprotocol/lib crypto-js ethers@5.7.2 typescript @types/node ts-node
/// ```
/// ## 4. Import dependencies and add variables, constants and helper methods
/// Now open the `compute.ts` file in your text editor.
/// ### 4.1. Dependencies
/// Start by importing all of the necessary dependencies
/// ```Typescript
import fs from 'fs'
import { homedir } from 'os'
import { assert } from 'chai'
import { SHA256 } from 'crypto-js'
import { ethers, providers, Signer } from 'ethers'
import {
ProviderInstance,
Aquarius,
NftFactory,
Datatoken,
Nft,
ZERO_ADDRESS,
transfer,
sleep,
approveWei,
ProviderComputeInitialize,
ConsumeMarketFee,
ComputeAlgorithm,
ComputeAsset,
Config,
Files,
DDO,
NftCreateData,
DatatokenCreateParams,
sendTx,
configHelperNetworks,
ConfigHelper,
getEventFromTx,
amountToUnits
} from '../../src'
/// ```
/// ### 4.2. Constants and variables
/// We will need two files to publish, one as dataset and one as algorithm, so here we define the files that we intend to publish.
/// ```Typescript
const DATASET_ASSET_URL: Files = {
datatokenAddress: '0x0',
nftAddress: '0x0',
files: [
{
type: 'url',
url: 'https://raw.githubusercontent.com/oceanprotocol/testdatasets/main/shs_dataset_test.txt',
method: 'GET'
}
]
}
const ALGORITHM_ASSET_URL: Files = {
datatokenAddress: '0x0',
nftAddress: '0x0',
files: [
{
type: 'url',
url: 'https://raw.githubusercontent.com/oceanprotocol/testdatasets/main/shs_dataset_test.txt',
method: 'GET'
}
]
}
/// ```
/// Next, we define the metadata for the dataset and algorithm that will describe our data assets. This is what we call the DDOs
/// ```Typescript
const DATASET_DDO: DDO = {
'@context': ['https://w3id.org/did/v1'],
id: 'id:op:efba17455c127a885ec7830d687a8f6e64f5ba559f8506f8723c1f10f05c049c',
version: '4.1.0',
chainId: 5,
nftAddress: '0x0',
metadata: {
created: '2021-12-20T14:35:20Z',
updated: '2021-12-20T14:35:20Z',
type: 'dataset',
name: 'dataset-name',
description: 'Ocean protocol test dataset description',
author: 'oceanprotocol-team',
license: 'https://market.oceanprotocol.com/terms',
additionalInformation: {
termsAndConditions: true
}
},
services: [
{
id: 'notAnId',
type: 'compute',
files: '',
datatokenAddress: '0xa15024b732A8f2146423D14209eFd074e61964F3',
serviceEndpoint: 'https://v4.provider.goerli.oceanprotocol.com/',
timeout: 300,
compute: {
publisherTrustedAlgorithmPublishers: [],
publisherTrustedAlgorithms: [],
allowRawAlgorithm: true,
allowNetworkAccess: true
}
}
]
}
const ALGORITHM_DDO: DDO = {
'@context': ['https://w3id.org/did/v1'],
id: 'did:op:efba17455c127a885ec7830d687a8f6e64f5ba559f8506f8723c1f10f05c049c',
version: '4.1.0',
chainId: 5,
nftAddress: '0x0',
metadata: {
created: '2021-12-20T14:35:20Z',
updated: '2021-12-20T14:35:20Z',
type: 'algorithm',
name: 'algorithm-name',
description: 'Ocean protocol test algorithm description',
author: 'oceanprotocol-team',
license: 'https://market.oceanprotocol.com/terms',
additionalInformation: {
termsAndConditions: true
},
algorithm: {
language: 'Node.js',
version: '1.0.0',
container: {
entrypoint: 'node $ALGO',
image: 'ubuntu',
tag: 'latest',
checksum:
'sha256:2d7ecc9c5e08953d586a6e50c29b91479a48f69ac1ba1f9dc0420d18a728dfc5'
}
}
},
services: [
{
id: 'notAnId',
type: 'access',
files: '',
datatokenAddress: '0xa15024b732A8f2146423D14209eFd074e61964F3',
serviceEndpoint: 'https://v4.provider.goerli.oceanprotocol.com',
timeout: 300
}
]
}
/// ```
/// Now we define the variables which we will need later
/// ```Typescript
let config: Config
let aquariusInstance: Aquarius
let datatoken: Datatoken
let providerUrl: string
let publisherAccount: Signer
let consumerAccount: Signer
let addresses
let computeEnvs
let datasetId: string
let algorithmId: string
let resolvedDatasetDdo: DDO
let resolvedAlgorithmDdo: DDO
let computeJobId: string
let agreementId: string
/// ```
/// ### 4.3 Helper methods
/// Now we define the helper methods which we will use later to publish the dataset and algorithm, and also order them
/// Add a `createAsset()`function.
/// ```Typescript
async function createAsset(
name: string,
symbol: string,
owner: Signer,
assetUrl: Files,
ddo: DDO,
providerUrl: string
) {
const nft = new Nft(owner, (await owner.provider.getNetwork()).chainId)
const nftFactory = new NftFactory(addresses.ERC721Factory, owner)
const chain = (await owner.provider.getNetwork()).chainId
ddo.chainId = parseInt(chain.toString(10))
const nftParamsAsset: NftCreateData = {
name,
symbol,
templateIndex: 1,
tokenURI: 'aaa',
transferable: true,
owner: await owner.getAddress()
}
const datatokenParams: DatatokenCreateParams = {
templateIndex: 1,
cap: '100000',
feeAmount: '0',
paymentCollector: ZERO_ADDRESS,
feeToken: ZERO_ADDRESS,
minter: await owner.getAddress(),
mpFeeAddress: ZERO_ADDRESS
}
const bundleNFT = await nftFactory.createNftWithDatatoken(
nftParamsAsset,
datatokenParams
)
const trxReceipt = await bundleNFT.wait()
// events have been emitted
const nftCreatedEvent = getEventFromTx(trxReceipt, 'NFTCreated')
const tokenCreatedEvent = getEventFromTx(trxReceipt, 'TokenCreated')
const nftAddress = nftCreatedEvent.args.newTokenAddress
const datatokenAddressAsset = tokenCreatedEvent.args.newTokenAddress
// create the files encrypted string
assetUrl.datatokenAddress = datatokenAddressAsset
assetUrl.nftAddress = nftAddress
ddo.services[0].files = await ProviderInstance.encrypt(assetUrl, chain, providerUrl)
ddo.services[0].datatokenAddress = datatokenAddressAsset
ddo.services[0].serviceEndpoint = 'http://172.15.0.4:8030' // put back proviederUrl
ddo.nftAddress = nftAddress
ddo.id = 'did:op:' + SHA256(ethers.utils.getAddress(nftAddress) + chain.toString(10))
const encryptedResponse = await ProviderInstance.encrypt(ddo, chain, providerUrl)
const validateResult = await aquariusInstance.validate(ddo)
await nft.setMetadata(
nftAddress,
await owner.getAddress(),
0,
'http://172.15.0.4:8030', // put back proviederUrl
'',
ethers.utils.hexlify(2),
encryptedResponse,
validateResult.hash
)
return ddo.id
}
/// ```
/// Add a `handleOrder()`function.
/// ```Typescript
async function handleOrder(
order: ProviderComputeInitialize,
datatokenAddress: string,
payerAccount: Signer,
consumerAccount: string,
serviceIndex: number,
consumeMarkerFee?: ConsumeMarketFee
) {
/* We do have 3 possible situations:
- have validOrder and no providerFees -> then order is valid, providerFees are valid, just use it in startCompute
- have validOrder and providerFees -> then order is valid but providerFees are not valid, we need to call reuseOrder and pay only providerFees
- no validOrder -> we need to call startOrder, to pay 1 DT & providerFees
*/
if (order.providerFee && order.providerFee.providerFeeAmount) {
await approveWei(
payerAccount,
config,
await payerAccount.getAddress(),
order.providerFee.providerFeeToken,
datatokenAddress,
order.providerFee.providerFeeAmount
)
}
if (order.validOrder) {
if (!order.providerFee) return order.validOrder
const tx = await datatoken.reuseOrder(
datatokenAddress,
order.validOrder,
order.providerFee
)
const reusedTx = await tx.wait()
const orderReusedTx = getEventFromTx(reusedTx, 'OrderReused')
return orderReusedTx.transactionHash
}
const tx = await datatoken.startOrder(
datatokenAddress,
consumerAccount,
serviceIndex,
order.providerFee,
consumeMarkerFee
)
const orderTx = await tx.wait()
const orderStartedTx = getEventFromTx(orderTx, 'OrderStarted')
return orderStartedTx.transactionHash
}
/// ```
/// At the end of your compute.ts file define `async function run(){ }`. We will use this function to add and test the following chunks of code.
/// <!--
describe('Compute-to-data example tests', async () => {
/// -->
/// We need to load the configuration. Add the following code into your `run(){ }` function
/// ```Typescript
before(async () => {
const provider = new providers.JsonRpcProvider(
process.env.NODE_URI || configHelperNetworks[1].nodeUri
)
publisherAccount = (await provider.getSigner(0)) as Signer
consumerAccount = (await provider.getSigner(1)) as Signer
const config = new ConfigHelper().getConfig(
parseInt(String((await publisherAccount.provider.getNetwork()).chainId))
)
config.providerUri = process.env.PROVIDER_URL || config.providerUri
aquariusInstance = new Aquarius(config?.metadataCacheUri)
providerUrl = config?.providerUri
addresses = JSON.parse(
// eslint-disable-next-line security/detect-non-literal-fs-filename
fs.readFileSync(
process.env.ADDRESS_FILE ||
`${homedir}/.ocean/ocean-contracts/artifacts/address.json`,
'utf8'
)
).development
/// ```
/// As we go along it's a good idea to console log the values so that you check they are right. At the end of your `run(){ ... }` function add the following logs:
/// ```Typescript
console.log(`Aquarius URL: ${config.metadataCacheUri}`)
console.log(`Provider URL: ${providerUrl}`)
console.log(`Deployed contracts address: ${addresses}`)
console.log(`Publisher account address: ${publisherAccount}`)
console.log(`Consumer account address: ${consumerAccount}`)
}) ///
/// ```
/// Now at the end of your compute.ts file call you `run()` function. Next, let's compile the file with the `tsc` command in the console and run `node dist/compute.js`.
/// If everything is working you should see the logs in the console and no errors.
/// We will use all of the following code snippets in the same way. Add the code snippet and the logs to the end of your `run(){ ... }` function as well as the logs.
/// Then compile your file with the `tsc` command and run it with `node dist/compute.js`
it('5.1 Mint OCEAN to publisher account', async () => {
/// You can skip this step if you are running your script against a remote network,
/// you need to mint oceans to mentioned accounts only if you are using barge to test your script
/// ```Typescript
const minAbi = [
{
constant: false,
inputs: [
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' }
],
name: 'mint',
outputs: [{ name: '', type: 'bool' }],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
}
]
const tokenContract = new ethers.Contract(addresses.Ocean, minAbi, publisherAccount)
const estGasPublisher = await tokenContract.estimateGas.mint(
await publisherAccount.getAddress(),
amountToUnits(null, null, '1000', 18)
)
await sendTx(
estGasPublisher,
publisherAccount,
1,
tokenContract.mint,
await publisherAccount.getAddress(),
amountToUnits(null, null, '1000', 18)
)
}) ///
/// ```
it('5.2 Send some OCEAN to consumer account', async () => {
/// ```Typescript
transfer(
publisherAccount,
config,
addresses.Ocean,
await consumerAccount.getAddress(),
'100'
)
}) ///
/// ```
/// ## 6. Publish assets dataset and algorithm
it('6.1 Publish a dataset (create NFT + Datatoken) and set dataset metadata', async () => {
/// ```Typescript
datasetId = await createAsset(
'D1Min',
'D1M',
publisherAccount,
DATASET_ASSET_URL,
DATASET_DDO,
providerUrl
)
/// ```
/// Now, let's check that we successfully published a dataset (create NFT + Datatoken)
/// ```Typescript
console.log(`dataset id: ${datasetId}`)
}) ///
/// ```
it('6.2 Publish an algorithm (create NFT + Datatoken) and set algorithm metadata', async () => {
/// ```Typescript
algorithmId = await createAsset(
'D1Min',
'D1M',
publisherAccount,
ALGORITHM_ASSET_URL,
ALGORITHM_DDO,
providerUrl
)
/// ```
/// Now, let's check that we successfully published a algorithm (create NFT + Datatoken)
/// ```Typescript
console.log(`algorithm id: ${algorithmId}`)
}) ///
/// ```
/// ## 7. Resolve assets
it('7.1 Resolve published datasets and algorithms', async () => {
/// ```Typescript
resolvedDatasetDdo = await aquariusInstance.waitForAqua(datasetId)
resolvedAlgorithmDdo = await aquariusInstance.waitForAqua(algorithmId)
/// ```
/// <!--
assert(resolvedDatasetDdo, 'Cannot fetch DDO from Aquarius')
assert(resolvedAlgorithmDdo, 'Cannot fetch DDO from Aquarius')
/// -->
}) ///
/// ## 8. Send datatokens to consumer
it('8.1 Mint dataset and algorithm datatokens to publisher', async () => {
/// ```Typescript
const datatoken = new Datatoken(
publisherAccount,
(await publisherAccount.provider.getNetwork()).chainId
)
await datatoken.mint(
resolvedDatasetDdo.services[0].datatokenAddress,
await publisherAccount.getAddress(),
'10',
await consumerAccount.getAddress()
)
await datatoken.mint(
resolvedAlgorithmDdo.services[0].datatokenAddress,
await publisherAccount.getAddress(),
'10',
await consumerAccount.getAddress()
)
}) ///
/// ```
/// ## 9. Get compute environments
it('9.1 Fetch compute environments from provider', async () => {
/// ```Typescript
computeEnvs = await ProviderInstance.getComputeEnvironments(providerUrl)
/// ```
/// <!--
assert(computeEnvs, 'No Compute environments found')
/// -->
}) ///
/// ## 10. Consumer starts a compute job
it('10.1 Start a compute job using a free C2D environment', async () => {
datatoken = new Datatoken(
consumerAccount,
(await consumerAccount.provider.getNetwork()).chainId
)
/// let's check the free compute environment
/// ```Typescript
const computeEnv = computeEnvs[resolvedDatasetDdo.chainId].find(
(ce) => ce.priceMin === 0
)
console.log('Free compute environment = ', computeEnv)
/// ```
/// <!--
assert(computeEnv, 'Cannot find the free compute env')
/// -->
/// Let's have 5 minute of compute access
/// ```Typescript
const mytime = new Date()
const computeMinutes = 5
mytime.setMinutes(mytime.getMinutes() + computeMinutes)
const computeValidUntil = Math.floor(mytime.getTime() / 1000)
const assets: ComputeAsset[] = [
{
documentId: resolvedDatasetDdo.id,
serviceId: resolvedDatasetDdo.services[0].id
}
]
const dtAddressArray = [resolvedDatasetDdo.services[0].datatokenAddress]
const algo: ComputeAlgorithm = {
documentId: resolvedAlgorithmDdo.id,
serviceId: resolvedAlgorithmDdo.services[0].id
}
const providerInitializeComputeResults = await ProviderInstance.initializeCompute(
assets,
algo,
computeEnv.id,
computeValidUntil,
providerUrl,
await consumerAccount.getAddress()
)
/// ```
/// <!--
assert(!('error' in providerInitializeComputeResults), 'Cannot order algorithm')
/// -->
/// ```Typescript
algo.transferTxId = await handleOrder(
providerInitializeComputeResults.algorithm,
resolvedAlgorithmDdo.services[0].datatokenAddress,
consumerAccount,
computeEnv.consumerAddress,
0
)
for (let i = 0; i < providerInitializeComputeResults.datasets.length; i++) {
assets[i].transferTxId = await handleOrder(
providerInitializeComputeResults.datasets[i],
dtAddressArray[i],
consumerAccount,
computeEnv.consumerAddress,
0
)
}
const computeJobs = await ProviderInstance.computeStart(
providerUrl,
consumerAccount,
computeEnv.id,
assets[0],
algo
)
/// ```
/// <!--
assert(computeJobs, 'Cannot start compute job')
/// -->
/// Let's save the compute job it, we re going to use later
/// ```Typescript
computeJobId = computeJobs[0].jobId
// eslint-disable-next-line prefer-destructuring
agreementId = computeJobs[0].agreementId
}) ///
/// ```
/// ## 11. Check compute status and get download compute results URL
it('11.1 Check compute status', async () => {
/// You can also add various delays so you see the various states of the compute job
/// ```Typescript
const jobStatus = await ProviderInstance.computeStatus(
providerUrl,
await consumerAccount.getAddress(),
computeJobId,
agreementId
)
/// ```
/// <!--
assert(jobStatus, 'Cannot retrieve compute status!')
/// -->
/// Now, let's see the current status of the previously started computer job
/// ```Typescript
console.log('Current status of the compute job: ', jobStatus)
}) ///
/// ```
it('11.2 Get download compute results URL', async () => {
/// ```Typescript
await sleep(10000)
const downloadURL = await ProviderInstance.getComputeResultUrl(
providerUrl,
consumerAccount,
computeJobId,
0
)
/// ```
/// <!--
assert(downloadURL, 'Provider getComputeResultUrl failed!')
/// -->
/// Let's check the compute results url for the specified index
/// ```Typescript
console.log(`Compute results URL: ${downloadURL}`)
}) ///
/// ```
}) ///
/// ## Editing this file
/// Please note that ComputeExamples.md is an autogenerated file, you should not edit it directly.
/// Updates should be done in `test/integration/ComputeExamples.test.ts` and all markdown should have three forward slashes before it
/// e.g. `/// # H1 Title`